diff --git a/typescript/packages/lookslike-high-level/src/data.ts b/typescript/packages/lookslike-high-level/src/data.ts index 85ce0f5c1..2a12801de 100644 --- a/typescript/packages/lookslike-high-level/src/data.ts +++ b/typescript/packages/lookslike-high-level/src/data.ts @@ -26,6 +26,7 @@ import { } from "@commontools/common-runner"; import { fetchCollections } from "./recipes/fetchCollections.js"; import { iframeExample } from "./recipes/iframeExample.js"; +import { arenaRecipes } from "./recipes/arena.js"; export type Charm = { [ID]: number; @@ -59,6 +60,7 @@ export function addCharms(newCharms: CellImpl[]) { addCharms([ run(iframeExample, { title: "two way binding counter", prompt: "counter example using write and subscribe with key `counter`", data: { counter: 0 } }), 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 } }), + run(arenaRecipes.fetchChannels, { title: "are.na", page: 1, per: 25 }), run(fetchExample, { url: "https://anotherjesse-restfuljsonblobapi.web.val.run/items", }), diff --git a/typescript/packages/lookslike-high-level/src/recipes/arena.ts b/typescript/packages/lookslike-high-level/src/recipes/arena.ts new file mode 100644 index 000000000..0db3a9423 --- /dev/null +++ b/typescript/packages/lookslike-high-level/src/recipes/arena.ts @@ -0,0 +1,324 @@ +import { html } from "@commontools/common-html"; +import { + recipe, + fetchData, + UI, + NAME, + ifElse, + lift, + handler, + str +} from "@commontools/common-builder"; +import { launch } from "../data.js"; + +interface Channel { + id: number; + title: string; + created_at: string; + updated_at: string; + published: boolean; + open: boolean; + collaboration: boolean; + slug: string; + length: number; + kind: string; + status: string; + user_id: number; + class: string; + base_class: string; + user: { + id: number; + slug: string; + first_name: string; + last_name: string; + full_name: string; + avatar: string; + email: string; + channel_count: number; + following_count: number; + follower_count: number; + profile_id: number; + }; + total_pages: number; + current_page: number; + per: number; + follower_count: number; + contents: any[] | null; + collaborators: any[] | null; +} + +const API_BASE_URL = "http://api.are.na/v2"; + +// Move all handlers to the top +const onViewChannel = handler<{}, { slug: string }>( + (_, { slug }) => { + console.log("view channel", slug); + launch(viewChannel, { slug }); + } +); + +const onChangePage = handler<{}, { page: number; per: number }>( + (_, { page, per }) => { + console.log("change page", page); + launch(fetchChannels, { page, per }); + } +); + +const onAddBlock = handler<{}, { channelSlug: string }>( + (_, { channelSlug }) => { + const content = prompt("Enter block content:"); + if (content) { + launch(addBlock, { channelSlug, content }); + } + } +); + +const tap = lift(x => { + console.log(x); + return x; +}) + +const getCollectionRows = lift((result: any) => (result?.channels || []).map(c => ({ title: c.title, id: c.id, slug: c.slug, status: c.status, length: c.length }))); +const getItemRows = lift((result: any) => (result?.contents || []).map(c => ({ title: c.title, id: c.id, slug: c.slug, status: c.status, length: c.length }))); + +const buildUrl = lift(({ base, page, per }:{ base: string, page: number, per: number }) => + `${base}/channels?page=${page}&per=${per}` +); + +const fetchChannels = recipe<{ page?: number; per?: number }>( + "Fetch Channels", + ({ page = 1, per = 25 }) => { + const { result } = fetchData<{ channels: Channel[], total_pages: number, current_page: number }>({ + url: buildUrl({ base: API_BASE_URL, page, per }), + schema: { + type: "object", + properties: { + channels: { + type: "array", + items: { + type: "object", + properties: { + id: { type: "number" }, + title: { type: "string" }, + slug: { type: "string" }, + status: { type: "string" }, + length: { type: "number" }, + }, + }, + }, + total_pages: { type: "number" }, + current_page: { type: "number" }, + }, + }, + }); + + const rows = getCollectionRows(result); + tap(rows); + + return { + [NAME]: "Fetch Channels", + [UI]: html` +
+ ${ifElse( + result, + html` +
+ + + + + + + + + + + + + ${getCollectionRows(result).map( + (channel) => html` + + + + + + + + + ` + )} + +
ActionIDTitleSlugStatusLength
+ + View Channel + + ${channel.id}${channel.title}${channel.slug}${channel.status}${channel.length}
+
+ `, + html`
Loading...
` + )} +
+ `, + result, + }; + } +); +const buildChannelUrl = lift(({ base, slug }: { base: string, slug: string }) => + `${base}/channels/${slug}` +); + +const getChannelContents = lift((result: Channel) => (result ? ({ + id: result.id, + contents: result.contents?.map(content => ({ + id: content.id, + title: content.title + })), +}): result)); + +const viewChannel = recipe<{ slug: string }>( + "View Channel", + ({ slug }) => { + const { result } = fetchData({ + url: buildChannelUrl({ base: API_BASE_URL, slug }), + schema: { + type: "object", + properties: { + id: { type: "number" }, + title: { type: "string" }, + slug: { type: "string" }, + status: { type: "string" }, + length: { type: "number" }, + contents: { type: "array" }, + }, + }, + }); + + tap(result); + const data = getChannelContents(result) + + return { + [NAME]: str`Channel: ${result?.title || "(unknown)"}`, + [UI]: html` +
+ ${ifElse( + result, + html` +
+

${result.title}

+

Status: ${result.status}

+

Length: ${result.length}

+

Contents:

+
    + ${data.contents.map( + (item: any) => html` +
  • ${item.title}
  • + ` + )} +
+ + Add Block + +
+ `, + html`
Loading...
` + )} +
+ `, + result, + }; + } +); + +const createChannel = recipe<{ title: string; status?: string }>( + "Create Channel", + ({ title, status = "public" }) => { + const { result } = fetchData({ + url: `${API_BASE_URL}/channels`, + method: "POST", + body: JSON.stringify({ title, status }), + headers: { "Content-Type": "application/json" }, + schema: { + type: "object", + properties: { + id: { type: "number" }, + title: { type: "string" }, + slug: { type: "string" }, + status: { type: "string" }, + }, + }, + }); + + return { + [NAME]: "Create Channel", + [UI]: html` +
+ ${ifElse( + result, + html` +
+

Channel Created Successfully

+

Title: ${result.title}

+

Slug: ${result.slug}

+

Status: ${result.status}

+ + View Channel + +
+ `, + html`
Creating channel...
` + )} +
+ `, + result, + }; + } +); + +const addBlock = recipe<{ channelSlug: string; content: string }>( + "Add Block", + ({ channelSlug, content }) => { + const { result } = fetchData({ + url: `${API_BASE_URL}/channels/${channelSlug}/blocks`, + method: "POST", + body: JSON.stringify({ content }), + headers: { "Content-Type": "application/json" }, + schema: { + type: "object", + properties: { + id: { type: "number" }, + title: { type: "string" }, + content: { type: "string" }, + }, + }, + }); + + return { + [NAME]: "Add Block", + [UI]: html` +
+ ${ifElse( + result, + html` +
+

Block Added Successfully

+

Content: ${result.content}

+ + Back to Channel + +
+ `, + html`
Adding block...
` + )} +
+ `, + result, + }; + } +); + +export const arenaRecipes = { + fetchChannels, + viewChannel, + createChannel, + addBlock, +};