Zettel API
Search…
⌃K

Tableland App

In this tutorial we are going to create a simple web app that uses the tableland API to retrieve the public data of Zettel and show them in a way similar to the Zettel Terminal.

Create 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:

Using the Tableland

in order to query the tables in Tableland, based on their docs (https://docs.tableland.xyz/rest-api), we are going to use the query REST API which has a URL like this:
https://testnet.tableland.network/query?s=[SQL_QUERY]
for example, we can select all rows of the pages table using this URL:
https://testnet.tableland.network/query?s=SELECT * FROM zettel_pages_5_262
which will return a data wit a structure like this:
{
"columns": [
{
"name": "id"
},
{
"name": "owner_id"
},
{
"name": "name"
},
...
],
"rows": [
[
"51affd78-25f1-57f5-a9e4-721b550a4be2",
"7440c54c-320f-492f-b89c-0ffe047dd638",
"t1",
...
],
...
]
}

Adding the Page list

In this step we are going to add the first section of this simple app which is the list of the pages that is shown at left side of the app in a sidebar style. first change the content of the App component using the following source code:
import { useEffect, useState } from "react";
import "./App.css";
function App() {
const [pages, setPages] = useState<any>([]);
const [pagesLoading, setPagesLoading] = useState(false);
useEffect(() => {
fetchPages();
}, []);
return (
<div className="container">
<div className="pages-container">
<div className="pages-title">Pages in Tableland</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" key={p.id}>
{p.name}
</div>
))
)}
</div>
</div>
</div>
</div>
);
async function fetchPages() {
setPagesLoading(true);
try {
const pagesResult = await fetch(
"https://testnet.tableland.network/query?s=SELECT * FROM zettel_pages_5_262"
);
const { columns, rows } = await pagesResult.json();
const pages = mapTableland(columns, rows);
setPages(pages);
} finally {
setPagesLoading(false);
}
}
}
function mapTableland(columns: any[], rows: any[]) {
return (rows || []).map((row: any) => {
const result = {} as any;
columns.forEach((col: any, i: number) => (result[col.name] = row[i]));
return result;
});
}
export default App;

Adding the Page content

After adding the Page list, it is now time for the page content which is the cards list. In order to achieve it just change the app component to this:
import { useEffect, useState } from "react";
import "./App.css";
function App() {
const [pages, setPages] = useState<any>([]);
const [cards, setCards] = useState<any>([]);
const [selected, setSelected] = useState<any>({});
const [pagesLoading, setPagesLoading] = useState(false);
const [cardsLoading, setCardsLoading] = useState(false);
useEffect(() => {
fetchPages();
}, []);
return (
<div className="container">
<div className="pages-container">
<div className="pages-title">Pages in Tableland</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}
</div>
))
)}
</div>
</div>
</div>
</div>
);
async function fetchPages() {
setPagesLoading(true);
try {
const pagesResult = await fetch(
"https://testnet.tableland.network/query?s=SELECT * FROM zettel_pages_5_262"
);
const { columns, rows } = await pagesResult.json();
const pages = mapTableland(columns, rows);
setPages(pages);
} finally {
setPagesLoading(false);
}
}
async function fetchCards(page: any) {
setCardsLoading(true);
try {
setSelected(page);
const cardsResult = await fetch(
`https://testnet.tableland.network/query?s=SELECT * FROM zettel_cards_5_263 WHERE page_id='${page.id}'`
);
const { columns, rows } = await cardsResult.json();
const cards = mapTableland(columns, rows);
setCards(cards);
} finally {
setCardsLoading(false);
}
}
}
function mapTableland(columns: any[], rows: any[]) {
return (rows || []).map((row: any) => {
const result = {} as any;
columns.forEach((col: any, i: number) => (result[col.name] = row[i]));
return result;
});
}
export default App;
after selecting a page a request will be sent to Tableland to retrieve the cards of the selected page and then render each card in the page content panel. for simplicity just the card id is rendered in this sample.

Adding styles

To have a better look for the app, clear the default content of the App.css file and use the following code:
.container {
height: 100%;
display: flex;
color: white;
align-items: center;
justify-content: center;
background-color: #222;
}
.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: 200px;
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;
}