Skip to content

Commit 7da7913

Browse files
committed
Fetch and explore are.na collections
1 parent 8e54b5b commit 7da7913

File tree

2 files changed

+326
-0
lines changed

2 files changed

+326
-0
lines changed

typescript/packages/lookslike-high-level/src/data.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
} from "@commontools/common-runner";
2727
import { fetchCollections } from "./recipes/fetchCollections.js";
2828
import { iframeExample } from "./recipes/iframeExample.js";
29+
import { arenaRecipes } from "./recipes/arena.js";
2930

3031
export type Charm = {
3132
[ID]: number;
@@ -59,6 +60,7 @@ export function addCharms(newCharms: CellImpl<any>[]) {
5960
addCharms([
6061
run(iframeExample, { title: "two way binding counter", prompt: "counter example using write and subscribe with key `counter`", data: { counter: 0 } }),
6162
run(iframeExample, { title: "breakout", prompt: "playable breakout/arkanoid, use `score` to write score, click to start, reset score at start", data: { score: 0, counter: 0 } }),
63+
run(arenaRecipes.fetchChannels, { title: "are.na", page: 1, per: 25 }),
6264
run(fetchExample, {
6365
url: "https://anotherjesse-restfuljsonblobapi.web.val.run/items",
6466
}),
Lines changed: 324 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,324 @@
1+
import { html } from "@commontools/common-html";
2+
import {
3+
recipe,
4+
fetchData,
5+
UI,
6+
NAME,
7+
ifElse,
8+
lift,
9+
handler,
10+
str
11+
} from "@commontools/common-builder";
12+
import { launch } from "../data.js";
13+
14+
interface Channel {
15+
id: number;
16+
title: string;
17+
created_at: string;
18+
updated_at: string;
19+
published: boolean;
20+
open: boolean;
21+
collaboration: boolean;
22+
slug: string;
23+
length: number;
24+
kind: string;
25+
status: string;
26+
user_id: number;
27+
class: string;
28+
base_class: string;
29+
user: {
30+
id: number;
31+
slug: string;
32+
first_name: string;
33+
last_name: string;
34+
full_name: string;
35+
avatar: string;
36+
email: string;
37+
channel_count: number;
38+
following_count: number;
39+
follower_count: number;
40+
profile_id: number;
41+
};
42+
total_pages: number;
43+
current_page: number;
44+
per: number;
45+
follower_count: number;
46+
contents: any[] | null;
47+
collaborators: any[] | null;
48+
}
49+
50+
const API_BASE_URL = "http://api.are.na/v2";
51+
52+
// Move all handlers to the top
53+
const onViewChannel = handler<{}, { slug: string }>(
54+
(_, { slug }) => {
55+
console.log("view channel", slug);
56+
launch(viewChannel, { slug });
57+
}
58+
);
59+
60+
const onChangePage = handler<{}, { page: number; per: number }>(
61+
(_, { page, per }) => {
62+
console.log("change page", page);
63+
launch(fetchChannels, { page, per });
64+
}
65+
);
66+
67+
const onAddBlock = handler<{}, { channelSlug: string }>(
68+
(_, { channelSlug }) => {
69+
const content = prompt("Enter block content:");
70+
if (content) {
71+
launch(addBlock, { channelSlug, content });
72+
}
73+
}
74+
);
75+
76+
const tap = lift(x => {
77+
console.log(x);
78+
return x;
79+
})
80+
81+
const getCollectionRows = lift((result: any) => (result?.channels || []).map(c => ({ title: c.title, id: c.id, slug: c.slug, status: c.status, length: c.length })));
82+
const getItemRows = lift((result: any) => (result?.contents || []).map(c => ({ title: c.title, id: c.id, slug: c.slug, status: c.status, length: c.length })));
83+
84+
const buildUrl = lift(({ base, page, per }:{ base: string, page: number, per: number }) =>
85+
`${base}/channels?page=${page}&per=${per}`
86+
);
87+
88+
const fetchChannels = recipe<{ page?: number; per?: number }>(
89+
"Fetch Channels",
90+
({ page = 1, per = 25 }) => {
91+
const { result } = fetchData<{ channels: Channel[], total_pages: number, current_page: number }>({
92+
url: buildUrl({ base: API_BASE_URL, page, per }),
93+
schema: {
94+
type: "object",
95+
properties: {
96+
channels: {
97+
type: "array",
98+
items: {
99+
type: "object",
100+
properties: {
101+
id: { type: "number" },
102+
title: { type: "string" },
103+
slug: { type: "string" },
104+
status: { type: "string" },
105+
length: { type: "number" },
106+
},
107+
},
108+
},
109+
total_pages: { type: "number" },
110+
current_page: { type: "number" },
111+
},
112+
},
113+
});
114+
115+
const rows = getCollectionRows(result);
116+
tap(rows);
117+
118+
return {
119+
[NAME]: "Fetch Channels",
120+
[UI]: html`
121+
<div>
122+
${ifElse(
123+
result,
124+
html`
125+
<div>
126+
<table>
127+
<thead>
128+
<tr>
129+
<th>Action</th>
130+
<th>ID</th>
131+
<th>Title</th>
132+
<th>Slug</th>
133+
<th>Status</th>
134+
<th>Length</th>
135+
</tr>
136+
</thead>
137+
<tbody>
138+
${getCollectionRows(result).map(
139+
(channel) => html`
140+
<tr>
141+
<td>
142+
<common-button onclick=${onViewChannel({ slug: channel.slug })}>
143+
View Channel
144+
</common-button>
145+
</td>
146+
<td>${channel.id}</td>
147+
<td>${channel.title}</td>
148+
<td>${channel.slug}</td>
149+
<td>${channel.status}</td>
150+
<td>${channel.length}</td>
151+
</tr>
152+
`
153+
)}
154+
</tbody>
155+
</table>
156+
</div>
157+
`,
158+
html`<div>Loading...</div>`
159+
)}
160+
</div>
161+
`,
162+
result,
163+
};
164+
}
165+
);
166+
const buildChannelUrl = lift(({ base, slug }: { base: string, slug: string }) =>
167+
`${base}/channels/${slug}`
168+
);
169+
170+
const getChannelContents = lift((result: Channel) => (result ? ({
171+
id: result.id,
172+
contents: result.contents?.map(content => ({
173+
id: content.id,
174+
title: content.title
175+
})),
176+
}): result));
177+
178+
const viewChannel = recipe<{ slug: string }>(
179+
"View Channel",
180+
({ slug }) => {
181+
const { result } = fetchData<Channel>({
182+
url: buildChannelUrl({ base: API_BASE_URL, slug }),
183+
schema: {
184+
type: "object",
185+
properties: {
186+
id: { type: "number" },
187+
title: { type: "string" },
188+
slug: { type: "string" },
189+
status: { type: "string" },
190+
length: { type: "number" },
191+
contents: { type: "array" },
192+
},
193+
},
194+
});
195+
196+
tap(result);
197+
const data = getChannelContents(result)
198+
199+
return {
200+
[NAME]: str`Channel: ${result?.title || "(unknown)"}`,
201+
[UI]: html`
202+
<div>
203+
${ifElse(
204+
result,
205+
html`
206+
<div>
207+
<h2>${result.title}</h2>
208+
<p>Status: ${result.status}</p>
209+
<p>Length: ${result.length}</p>
210+
<h3>Contents:</h3>
211+
<ul>
212+
${data.contents.map(
213+
(item: any) => html`
214+
<li>${item.title}</li>
215+
`
216+
)}
217+
</ul>
218+
<common-button onclick=${onAddBlock({ channelSlug: result.slug })}>
219+
Add Block
220+
</common-button>
221+
</div>
222+
`,
223+
html`<div>Loading...</div>`
224+
)}
225+
</div>
226+
`,
227+
result,
228+
};
229+
}
230+
);
231+
232+
const createChannel = recipe<{ title: string; status?: string }>(
233+
"Create Channel",
234+
({ title, status = "public" }) => {
235+
const { result } = fetchData<Channel>({
236+
url: `${API_BASE_URL}/channels`,
237+
method: "POST",
238+
body: JSON.stringify({ title, status }),
239+
headers: { "Content-Type": "application/json" },
240+
schema: {
241+
type: "object",
242+
properties: {
243+
id: { type: "number" },
244+
title: { type: "string" },
245+
slug: { type: "string" },
246+
status: { type: "string" },
247+
},
248+
},
249+
});
250+
251+
return {
252+
[NAME]: "Create Channel",
253+
[UI]: html`
254+
<div>
255+
${ifElse(
256+
result,
257+
html`
258+
<div>
259+
<h2>Channel Created Successfully</h2>
260+
<p>Title: ${result.title}</p>
261+
<p>Slug: ${result.slug}</p>
262+
<p>Status: ${result.status}</p>
263+
<common-button onclick=${onViewChannel({ slug: result.slug })}>
264+
View Channel
265+
</common-button>
266+
</div>
267+
`,
268+
html`<div>Creating channel...</div>`
269+
)}
270+
</div>
271+
`,
272+
result,
273+
};
274+
}
275+
);
276+
277+
const addBlock = recipe<{ channelSlug: string; content: string }>(
278+
"Add Block",
279+
({ channelSlug, content }) => {
280+
const { result } = fetchData<any>({
281+
url: `${API_BASE_URL}/channels/${channelSlug}/blocks`,
282+
method: "POST",
283+
body: JSON.stringify({ content }),
284+
headers: { "Content-Type": "application/json" },
285+
schema: {
286+
type: "object",
287+
properties: {
288+
id: { type: "number" },
289+
title: { type: "string" },
290+
content: { type: "string" },
291+
},
292+
},
293+
});
294+
295+
return {
296+
[NAME]: "Add Block",
297+
[UI]: html`
298+
<div>
299+
${ifElse(
300+
result,
301+
html`
302+
<div>
303+
<h2>Block Added Successfully</h2>
304+
<p>Content: ${result.content}</p>
305+
<common-button onclick=${onViewChannel({ slug: channelSlug })}>
306+
Back to Channel
307+
</common-button>
308+
</div>
309+
`,
310+
html`<div>Adding block...</div>`
311+
)}
312+
</div>
313+
`,
314+
result,
315+
};
316+
}
317+
);
318+
319+
export const arenaRecipes = {
320+
fetchChannels,
321+
viewChannel,
322+
createChannel,
323+
addBlock,
324+
};

0 commit comments

Comments
 (0)