Zettel API
Search…
⌃K

Public API App

In this tutorial we are going to develop a simple app that uses almost all of the Zettel's public API to mimic the real Zettel terminal app.

Create the project

We are going to use vite as our main development tool here. Use the following command to create a project with vite cli:
follow the instructions to create a new react project as shown in the following image:
at the next step you can choose the language to develop the app:
and at the end you can follow the instructions to install the dependencies and run the app:

Authentication

In this tutorial we are going to use the SIWE (Sign In With Ethereum) authentication to create a JWT to call authorized APIs later. we are going to use the web3modal and ethers npm packages to handle the communication with external utilities to connect to a wallet (Metamask chrome extension here).
After connecting to the wallect and accessing to the wallet address, we try to ask for a new nonce generated from the Zettel API using the following URL:
https://api-stage.zettel.ai/auth/siwe/nonce?walletAddress=${walletAddress}
the walletAddress is coming from the previous process of communication with Metamask. the next step is going to be the signing process which will be done by the same tools that we already used in the first step:
this whole process is shown in the following part. clear the App.tsx file and use the this source code instead:
import { useState } from "react";
import { ethers } from "ethers";
import Web3Modal from "web3modal";
import { Pages } from "./Pages";
import "./App.css";
function App() {
const [walletAddress, setWalletAddress] = useState("");
const [token, setToken] = useState("");
const [signing, setSigning] = useState(false);
return (
<div className="container">
{!token && (
<div className="signin">
<div className="signin-title">Welcome to Zettel api test app</div>
<button className="signin-button" onClick={signIn}>
{signing ? "Signing In..." : "Sign In"}
</button>
</div>
)}
{token && <Pages token={token} account={walletAddress} />}
</div>
);
async function getWeb3Modal() {
const web3Modal = new Web3Modal({
network: "mainnet",
cacheProvider: false,
providerOptions: {
},
});
return web3Modal;
}
async function signIn() {
if (signing) return;
setSigning(true);
try {
const web3Modal = await getWeb3Modal();
const connection = await web3Modal.connect();
const provider = new ethers.providers.Web3Provider(connection);
const [walletAddress] = await provider.listAccounts();
setWalletAddress(walletAddress);
const authResponse = await fetch(
`https://api-stage.zettel.ai/auth/siwe/nonce?walletAddress=${walletAddress}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
},
}
);
const { nonceMessage } = await authResponse.json();
const signer = provider.getSigner();
const signedNonceMessage = await signer.signMessage(
nonceMessage.toString()
);
const verifyResponse = await fetch(
"https://api-stage.zettel.ai/auth/siwe/verify",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
deviceId: "111",
walletAddress,
nonceMessage,
signedNonceMessage,
}),
}
);
const { accessToken } = await verifyResponse.json();
setToken(accessToken);
} finally {
setSigning(false);
}
}
}
export default App;
and also clear the content of the App.css file and use this content:
.container {
height: 100%;
display: flex;
color: white;
align-items: center;
justify-content: center;
background-color: #222;
}
.signin {
display: flex;
flex-direction: column;
align-items: center;
}
.signin-title {
font-size: 2rem;
margin-bottom: 2rem;
}
.signin-button {
border: none;
width: 15rem;
outline: none;
padding: 2rem;
color: inherit;
cursor: pointer;
font-size: 1.5rem;
border-radius: 1rem;
transition: all 0.2s;
background-color: black;
}
.button:hover {
background-color: #111;
}
after signin the app will try to show the page list and also by clicing on each page the cards of the selected page will be shown. the code to achieve that should be put in a new file named Pages.tsx with this content:
import { useEffect, useState } from "react";
import "./Pages.css";
interface PagesProps {
token: string;
account: string;
}
export function Pages({ token, account }: PagesProps) {
const [pages, setPages] = useState([]);
const [cards, setCards] = useState([]);
const [selected, setSelected] = useState<any>({});
const [pagesLoading, setPagesLoading] = useState(false);
const [cardsLoading, setCardsLoading] = useState(false);
useEffect(() => {
fetchPages();
}, [token]);
return (
<div className="pages-container">
<div className="pages-title">{account}</div>
<div className="pages-cards">
<div className="pages">
<div className="pages-title2">Pages</div>
{pagesLoading ? (
<div className="page">Loading...</div>
) : (
pages.map((p: any) => (
<div
className={`page ${selected.id === p.id ? "selected" : ""}`}
key={p.id}
onClick={(e) => fetchCards(p)}
>
{p.name}
</div>
))
)}
</div>
<div className="cards">
<div className="cards-title">{selected.name}</div>
{cardsLoading ? (
<div className="card">Loading...</div>
) : (
cards.map((c: any) => (
<div className="card" key={c.id}>
{c.id}
{c.blocks.map((b: any) => (
<div>{b.styledText?.text}</div>
))}
</div>
))
)}
</div>
</div>
</div>
);
async function fetchPages() {
setPagesLoading(true);
try {
const pagesResult = await fetch("https://api-stage.zettel.ai/pages", {
headers: {
Authorization: `Bearer ${token}`,
},
});
const { pages } = await pagesResult.json();
setPages(pages);
} finally {
setPagesLoading(false);
}
}
async function fetchCards(page: any) {
setCardsLoading(true);
try {
setSelected(page);
const cardsResult = await fetch(
`https://api-stage.zettel.ai/cards?pageId=${page.id}`,
{
headers: {
Authorization: `Bearer ${token}`,
},
}
);
const { cards } = await cardsResult.json();
console.log(cards);
setCards(cards);
} finally {
setCardsLoading(false);
}
}
}
and also add a new file named Pages.css and put the following content in it:
.pages-container {
width: 100%;
height: 100%;
padding: 2rem;
display: flex;
flex-direction: column;
}
.pages-title {
font-size: 2rem;
margin-bottom: 1em;
}
.pages-cards {
flex-grow: 1;
display: flex;
}
.pages {
width: 250px;
display: flex;
margin-right: 1rem;
padding-right: 1rem;
flex-direction: column;
border-right: 1px solid gray;
}
.pages-title2 {
font-size: 1.5rem;
padding-bottom: 1rem;
margin-bottom: 1rem;
border-bottom: 1px solid gray;
}
.page {
padding: 1rem;
cursor: pointer;
font-size: 1.5rem;
border-radius: 1rem;
transition: all 0.2s;
margin-bottom: 1rem;
}
.page:hover,
.page.selected:hover {
background-color: #333;
}
.page.selected {
background-color: #444;
}
.cards {
flex-grow: 1;
display: flex;
flex-direction: column;
}
.cards-title {
font-weight: bold;
font-size: 1.5rem;
margin-bottom: 2rem;
}
.card {
padding: 1rem;
margin-bottom: 1rem;
border-radius: 1rem;
background-color: #333;
}