From b4a022dfd9963f43b83129fc1e1b26d93befc8ea Mon Sep 17 00:00:00 2001 From: Jordi Enric Date: Mon, 21 Oct 2024 22:46:12 +0200 Subject: [PATCH] docs --- apps/api/contract/index.ts | 2 +- apps/web/app/api/public/[...route]/route.ts | 134 ++++++++++++++++-- .../app/api/public/[...route]/route.types.ts | 23 +++ apps/web/app/docs/page.tsx | 82 +++++++++++ apps/web/app/pub/queries.ts | 6 +- .../app/pub/themes/default/blog-post-item.tsx | 4 +- apps/web/app/pub/themes/garden/home.tsx | 2 +- apps/web/app/pub/themes/instrument/home.tsx | 4 +- apps/web/app/types.ts | 2 +- .../web/src/components/Editor/ZendoEditor.tsx | 10 +- apps/web/src/layouts/AppLayout.tsx | 8 ++ apps/web/src/types/supabase.ts | 42 +----- todo/postmvp.md | 2 +- 13 files changed, 259 insertions(+), 62 deletions(-) create mode 100644 apps/web/app/api/public/[...route]/route.types.ts create mode 100644 apps/web/app/docs/page.tsx diff --git a/apps/api/contract/index.ts b/apps/api/contract/index.ts index 7106a23..0296534 100644 --- a/apps/api/contract/index.ts +++ b/apps/api/contract/index.ts @@ -21,7 +21,7 @@ const BasePostSchema = z.object({ description: "The cover image of the post", example: "https://example.com/cover.jpg", }), - abstract: z.string().optional().openapi({ + excerpt: z.string().optional().openapi({ description: "The excerpt of the post", example: "This is my first post!", }), diff --git a/apps/web/app/api/public/[...route]/route.ts b/apps/web/app/api/public/[...route]/route.ts index eb251e5..c62f0fc 100644 --- a/apps/web/app/api/public/[...route]/route.ts +++ b/apps/web/app/api/public/[...route]/route.ts @@ -4,6 +4,7 @@ import { logger } from "hono/logger"; import { prettyJSON } from "hono/pretty-json"; import { Hono } from "hono"; import bcrypt from "bcrypt"; +import { Endpoint } from "./route.types"; async function verifyAPIKey(header: string, blogId: string) { const supabase = createClient(); @@ -32,13 +33,64 @@ async function verifyAPIKey(header: string, blogId: string) { return isValid; } -const app = new Hono() +export const app = new Hono() .basePath("/api/public") .use("*", logger()) .use("*", prettyJSON()); -// GET POSTS -app.get("/blogs/:blogId/posts", async (c) => { +const BASE_HEADERS = [ + { + key: "Authorization", + required: true, + description: "The API key for the blog", + }, +]; + +// Get posts +const posts: Endpoint = { + id: "posts", + path: "/blogs/:blogId/posts", + method: "GET", + title: "Post list", + description: "Get posts for a blog", + headers: [ + ...BASE_HEADERS, + { + key: "offset", + required: false, + description: "The offset for the posts", + }, + { + key: "limit", + required: false, + description: "The limit for the posts", + }, + ], + response: { + 200: { + description: "The posts", + type: "object", + example: ` + { + posts: { + title: "string", + html_content: "string", + slug: "string", + category_name: "string", // nullable + category_slug: "string", // nullable + tags: "object", + excerpt: "string", // nullable + published_at: "string", + }, + total: "number", // The total number of posts + offset: "number", // The offset + limit: "number", // The limit + } + `, + }, + }, +}; +app.get(posts.path, async (c) => { const blogId = c.req.param("blogId"); const offset = parseInt(c.req.query("offset") || "0"); const limit = parseInt(c.req.query("limit") || "30"); @@ -87,7 +139,33 @@ app.get("/blogs/:blogId/posts", async (c) => { }); // Get post by slug -app.get("/blogs/:blogId/posts/:slug", async (c) => { +const postBySlug: Endpoint = { + id: "postBySlug", + path: "/blogs/:blogId/posts/:slug", + method: "GET", + title: "Post detail", + description: "Get a post by its slug", + headers: [...BASE_HEADERS], + response: { + 200: { + description: "The post", + type: "object", + example: ` + { + title: "string", + html_content: "string", + slug: "string", + category_name: "string", + category_slug: "string", + tags: "object", + excerpt: "string", + published_at: "string", + } + `, + }, + }, +}; +app.get(postBySlug.path, async (c) => { const blogId = c.req.param("blogId"); const slug = c.req.param("slug"); const supabase = createClient(); @@ -120,8 +198,27 @@ app.get("/blogs/:blogId/posts/:slug", async (c) => { return c.json(post); }); -// Get blog categories -app.get("/blogs/:blogId/categories", async (c) => { +const categories: Endpoint = { + id: "categories", + path: "/blogs/:blogId/categories", + method: "GET", + title: "Categories list", + description: "Get the categories for a blog", + headers: [...BASE_HEADERS], + response: { + 200: { + description: "The categories", + type: "object", + example: ` + [{ + name: "string", + slug: "string", + }] + `, + }, + }, +}; +app.get(categories.path, async (c) => { const blogId = c.req.param("blogId"); const supabase = createClient(); const authHeader = c.req.header("Authorization"); @@ -153,8 +250,27 @@ app.get("/blogs/:blogId/categories", async (c) => { return c.json(categories); }); -// Get blog tags -app.get("/blogs/:blogId/tags", async (c) => { +const tags: Endpoint = { + id: "tags", + path: "/blogs/:blogId/tags", + method: "GET", + title: "Tags list", + description: "Get the tags for a blog", + headers: [...BASE_HEADERS], + response: { + 200: { + description: "The tags", + type: "object", + example: ` + [{ + name: "string", + slug: "string", + }] + `, + }, + }, +}; +app.get(tags.path, async (c) => { const blogId = c.req.param("blogId"); const supabase = createClient(); const authHeader = c.req.header("Authorization"); @@ -191,3 +307,5 @@ export const POST = handle(app); export const PUT = handle(app); export const PATCH = handle(app); export const DELETE = handle(app); + +export const endpoints = [posts, postBySlug, categories, tags]; diff --git a/apps/web/app/api/public/[...route]/route.types.ts b/apps/web/app/api/public/[...route]/route.types.ts new file mode 100644 index 0000000..008a29b --- /dev/null +++ b/apps/web/app/api/public/[...route]/route.types.ts @@ -0,0 +1,23 @@ +export type Endpoint = { + id: string; + path: string; + method: string; + title: string; + description: string; + headers: Header[]; + response: Response; +}; + +export type Header = { + key: string; + required: boolean; + description: string; +}; + +export type Response = { + [200]: { + description: string; + type: string; + example: string; + }; +}; diff --git a/apps/web/app/docs/page.tsx b/apps/web/app/docs/page.tsx new file mode 100644 index 0000000..b9daf14 --- /dev/null +++ b/apps/web/app/docs/page.tsx @@ -0,0 +1,82 @@ +import { endpoints } from "app/api/public/[...route]/route"; +import Link from "next/link"; + +export default function Docs() { + return ( +
+
+ + +
+ {endpoints.map((endpoint) => ( +
+

+ {endpoint.title} +

+

{endpoint.description}

+
+
+

{endpoint.method}

+

+ {endpoint.path} +

+
+
+
+

Headers

+
    + {endpoint.headers.map((header) => ( +
  • +
    +

    {header.key}

    +

    {header.required ? "Required" : "Optional"}

    +
    +

    {header.description}

    +
  • + ))} +
+
+
+
+

Response

+
+ {Object.entries(endpoint.response).map( + ([status, response]) => ( +
+
+

{status}

+

{response.description}

+
+
+                          {response.example}
+                        
+
+ ) + )} +
+
+
+ ))} +
+
+
+ ); +} diff --git a/apps/web/app/pub/queries.ts b/apps/web/app/pub/queries.ts index 9ab618c..ce66366 100644 --- a/apps/web/app/pub/queries.ts +++ b/apps/web/app/pub/queries.ts @@ -17,8 +17,8 @@ export async function getBlog(subdomain: string) { export async function getPosts(subdomain: string, sort: string = "desc") { const supa = createClient(); const res = await supa - .from("posts_v4") - .select("title, slug, published_at, cover_image, abstract") + .from("posts_v5") + .select("title, slug, published_at, cover_image, excerpt") .eq("blog_slug", subdomain) .eq("published", true) .order("published_at", { ascending: sort === "asc" }); @@ -37,7 +37,7 @@ export async function getPosts(subdomain: string, sort: string = "desc") { export async function getPost(subdomain: string, slug: string) { const supa = createClient(); const { data: post } = await supa - .from("posts_v4") + .from("posts_v5") .select( "title, content, cover_image, published_at, created_at, html_content" ) diff --git a/apps/web/app/pub/themes/default/blog-post-item.tsx b/apps/web/app/pub/themes/default/blog-post-item.tsx index 7a79966..8707e60 100644 --- a/apps/web/app/pub/themes/default/blog-post-item.tsx +++ b/apps/web/app/pub/themes/default/blog-post-item.tsx @@ -58,8 +58,8 @@ export function BlogPostItem({ {rightText} - {post.abstract && ( -

{post.abstract}

+ {post.excerpt && ( +

{post.excerpt}

)} diff --git a/apps/web/app/pub/themes/garden/home.tsx b/apps/web/app/pub/themes/garden/home.tsx index ba87672..900be90 100644 --- a/apps/web/app/pub/themes/garden/home.tsx +++ b/apps/web/app/pub/themes/garden/home.tsx @@ -95,7 +95,7 @@ export function GardenHome({ blog, posts, disableLinks }: BlogHomeProps) {

{post.title}

-

{post.abstract}

+

{post.excerpt}

{formatPostDate(post.published_at)}

diff --git a/apps/web/app/pub/themes/instrument/home.tsx b/apps/web/app/pub/themes/instrument/home.tsx index 67279ea..aecb65f 100644 --- a/apps/web/app/pub/themes/instrument/home.tsx +++ b/apps/web/app/pub/themes/instrument/home.tsx @@ -53,8 +53,8 @@ export function InstrumentHome({ posts, blog, disableLinks }: BlogHomeProps) {

- {post.abstract && ( -

{post.abstract}

+ {post.excerpt && ( +

{post.excerpt}

)}
diff --git a/apps/web/app/types.ts b/apps/web/app/types.ts index 614202b..10e34ed 100644 --- a/apps/web/app/types.ts +++ b/apps/web/app/types.ts @@ -13,7 +13,7 @@ export type Post = { title: string; published_at: string; slug: string; - abstract?: string; + excerpt?: string; }; export type BlogHomeProps = { diff --git a/apps/web/src/components/Editor/ZendoEditor.tsx b/apps/web/src/components/Editor/ZendoEditor.tsx index ce57255..eddbd49 100644 --- a/apps/web/src/components/Editor/ZendoEditor.tsx +++ b/apps/web/src/components/Editor/ZendoEditor.tsx @@ -51,7 +51,7 @@ const formSchema = z.object({ slug: z.string(), cover_image: z.string().optional(), content: z.any(), - abstract: z.string().optional(), + excerpt: z.string().optional(), category_id: z.number().nullable(), }); @@ -65,7 +65,7 @@ type OnSaveData = { cover_image?: string; published: boolean; metadata?: any; - abstract?: string; + excerpt?: string; category_id: number | null; tags?: { id: string; @@ -92,7 +92,7 @@ export const ZendoEditor = (props: Props) => { title: props.post?.title || "", slug: props.post?.slug || "", cover_image: props.post?.cover_image || "", - abstract: props.post?.abstract || "", + excerpt: props.post?.excerpt || "", category_id: props.post?.category_id || null, }, }); @@ -222,7 +222,7 @@ export const ZendoEditor = (props: Props) => { published_at: publishedAt || new Date().toISOString(), metadata, tags, - abstract: data.abstract, + excerpt: data.excerpt, category_id, }); }); @@ -559,7 +559,7 @@ export const ZendoEditor = (props: Props) => {