The recipes.blue monorepo recipes.blue
recipes appview atproto
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: read & list recipes via web app

+480 -173
-2
apps/api/src/index.ts
··· 3 3 import { rootLogger } from "./logger.js"; 4 4 import { newIngester } from "./ingest.js"; 5 5 import env from "./config/env.js"; 6 - import { recipeApp } from "./recipes/index.js"; 7 6 import { xrpcApp } from "./xrpc/index.js"; 8 7 import { cors } from "hono/cors"; 9 8 ··· 14 13 credentials: true, 15 14 })); 16 15 17 - app.route('/recipes', recipeApp); 18 16 app.route('/xrpc', xrpcApp); 19 17 20 18 newIngester().start();
-53
apps/api/src/recipes/index.ts
··· 1 - import { Hono } from "hono"; 2 - import { db } from "../db/index.js"; 3 - import { and, eq, sql } from "drizzle-orm"; 4 - import { recipeTable } from "../db/schema.js"; 5 - import { parseDid } from "../util/did.js"; 6 - import { apiLogger } from "../logger.js"; 7 - import { RecipeCollection } from "@cookware/lexicons"; 8 - 9 - export const recipeApp = new Hono(); 10 - 11 - recipeApp.get('/', async ctx => { 12 - const recipes = await db.query.recipeTable.findMany({ 13 - columns: { 14 - rkey: true, 15 - title: true, 16 - description: true, 17 - authorDid: true, 18 - createdAt: true, 19 - }, 20 - extras: { 21 - uri: sql`concat(${recipeTable.authorDid}, "/", ${recipeTable.rkey})`.as('uri'), 22 - } 23 - }); 24 - return ctx.json({ recipes }); 25 - }); 26 - 27 - recipeApp.get('/:authorDid/:rkey', async ctx => { 28 - const { authorDid, rkey } = ctx.req.param(); 29 - const did = parseDid(authorDid); 30 - if (!did) { 31 - return ctx.json({ 32 - error: 'invalid_did', 33 - message: 'The author DID you passed was invalid.', 34 - }, 400); 35 - } 36 - 37 - const recipe = await db.query.recipeTable.findFirst({ 38 - where: and( 39 - eq(recipeTable.rkey, rkey), 40 - eq(recipeTable.authorDid, did) 41 - ), 42 - columns: { id: false }, 43 - }); 44 - 45 - if (!recipe) { 46 - return ctx.json({ 47 - error: 'recipe_not_found', 48 - message: 'That recipe was not found.', 49 - }, 404); 50 - } 51 - 52 - return ctx.json({ recipe }); 53 - });
+48 -18
apps/api/src/xrpc/index.ts
··· 1 1 import { Hono } from 'hono'; 2 2 import { db } from '../db/index.js'; 3 3 import { recipeTable } from '../db/schema.js'; 4 - import { sql } from 'drizzle-orm'; 5 - import { Recipe, RecipeCollection } from '@cookware/lexicons'; 4 + import { and, eq, sql } from 'drizzle-orm'; 5 + import { parseDid } from '../util/did.js'; 6 6 7 7 export const xrpcApp = new Hono(); 8 8 9 9 xrpcApp.get('/moe.hayden.cookware.getRecipes', async ctx => { 10 - const recipes = await db.query.recipeTable.findMany({ 11 - columns: { 12 - rkey: true, 13 - title: true, 14 - description: true, 15 - ingredients: true, 16 - steps: true, 17 - createdAt: true, 18 - authorDid: true, 19 - }, 20 - extras: { 21 - uri: sql`concat(${recipeTable.authorDid}, "/", ${recipeTable.rkey})`.as('uri'), 22 - } 23 - }); 10 + const recipes = await db.select({ 11 + rkey: recipeTable.rkey, 12 + title: recipeTable.title, 13 + description: recipeTable.description, 14 + ingredientsCount: sql`json_array_length(${recipeTable.ingredients})`, 15 + stepsCount: sql`json_array_length(${recipeTable.steps})`, 16 + createdAt: recipeTable.createdAt, 17 + authorDid: recipeTable.authorDid, 18 + uri: sql`concat(${recipeTable.authorDid}, "/", ${recipeTable.rkey})`.as('uri'), 19 + }).from(recipeTable); 20 + 24 21 return ctx.json({ 25 22 recipes: recipes.map(r => ({ 26 23 rkey: r.rkey, 27 24 did: r.authorDid, 28 25 title: r.title, 29 26 description: r.description, 30 - ingredients: r.ingredients, 31 - steps: r.steps, 27 + ingredients: r.ingredientsCount, 28 + steps: r.stepsCount , 32 29 })), 30 + }); 31 + }); 32 + 33 + xrpcApp.get('/moe.hayden.cookware.getRecipe', async ctx => { 34 + const { did, rkey } = ctx.req.query(); 35 + if (!did) throw new Error('Invalid DID'); 36 + if (!rkey) throw new Error('Invalid rkey'); 37 + 38 + const parsedDid = parseDid(did); 39 + if (!parsedDid) throw new Error('Invalid DID'); 40 + 41 + const recipe = await db.query.recipeTable.findFirst({ 42 + where: and( 43 + eq(recipeTable.authorDid, parsedDid), 44 + eq(recipeTable.rkey, rkey), 45 + ), 46 + }); 47 + 48 + if (!recipe) { 49 + ctx.status(404); 50 + return ctx.json({ 51 + error: 'not_found', 52 + message: 'No such recipe was found in the index.', 53 + }); 54 + } 55 + 56 + return ctx.json({ 57 + recipe: { 58 + title: recipe.title, 59 + description: recipe.description, 60 + ingredients: recipe.ingredients, 61 + steps: recipe.steps, 62 + }, 33 63 }); 34 64 }); 35 65
+56
apps/web/src/components/query-placeholder.tsx
··· 1 + import type { UseQueryResult } from '@tanstack/react-query'; 2 + import { PropsWithChildren } from 'react'; 3 + import { Skeleton } from './ui/skeleton'; 4 + import { Alert, AlertDescription, AlertTitle } from './ui/alert'; 5 + import { AlertCircle } from 'lucide-react'; 6 + import { XRPCError } from '@atcute/client'; 7 + 8 + type QueryPlaceholderProps<TData, TError> = PropsWithChildren<{ 9 + query: UseQueryResult<TData, TError>; 10 + cards?: boolean; 11 + cardsCount?: number; 12 + }>; 13 + 14 + const QueryPlaceholder = <TData = {}, TError = Error>( 15 + { 16 + query, 17 + children, 18 + cards = false, 19 + cardsCount = 3, 20 + }: QueryPlaceholderProps<TData, TError> 21 + ) => { 22 + if (query.isPending || query.isLoading) { 23 + if (cards) { 24 + return [...Array(cardsCount)].map((_, i) => <Skeleton key={i} className="h-[200px] w-full rounded-lg" />); 25 + } 26 + 27 + return ( 28 + <p>Loading...</p> 29 + ) 30 + } else if (query.isError) { 31 + const { error } = query; 32 + let errMsg = 'Unknown'; 33 + if (error instanceof XRPCError) { 34 + errMsg = error.kind ?? `HTTP_${error.status}`; 35 + } if (error instanceof Error) { 36 + errMsg = `${error.message} (${error.name})`; 37 + } 38 + 39 + return ( 40 + <Alert variant="destructive"> 41 + <AlertCircle className="size-4" /> 42 + 43 + <AlertTitle>Error fetching data.</AlertTitle> 44 + <AlertDescription> 45 + The data you were trying to see failed to fetch.<br/> 46 + <b>Error code: {errMsg}</b> 47 + </AlertDescription> 48 + </Alert> 49 + ) 50 + } else if (query.data) { 51 + return children; 52 + } 53 + return <></>; 54 + }; 55 + 56 + export default QueryPlaceholder;
+59
apps/web/src/components/ui/alert.tsx
··· 1 + import * as React from "react" 2 + import { cva, type VariantProps } from "class-variance-authority" 3 + 4 + import { cn } from "@/lib/utils" 5 + 6 + const alertVariants = cva( 7 + "relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7", 8 + { 9 + variants: { 10 + variant: { 11 + default: "bg-background text-foreground", 12 + destructive: 13 + "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", 14 + }, 15 + }, 16 + defaultVariants: { 17 + variant: "default", 18 + }, 19 + } 20 + ) 21 + 22 + const Alert = React.forwardRef< 23 + HTMLDivElement, 24 + React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants> 25 + >(({ className, variant, ...props }, ref) => ( 26 + <div 27 + ref={ref} 28 + role="alert" 29 + className={cn(alertVariants({ variant }), className)} 30 + {...props} 31 + /> 32 + )) 33 + Alert.displayName = "Alert" 34 + 35 + const AlertTitle = React.forwardRef< 36 + HTMLParagraphElement, 37 + React.HTMLAttributes<HTMLHeadingElement> 38 + >(({ className, ...props }, ref) => ( 39 + <h5 40 + ref={ref} 41 + className={cn("mb-1 font-medium leading-none tracking-tight", className)} 42 + {...props} 43 + /> 44 + )) 45 + AlertTitle.displayName = "AlertTitle" 46 + 47 + const AlertDescription = React.forwardRef< 48 + HTMLParagraphElement, 49 + React.HTMLAttributes<HTMLParagraphElement> 50 + >(({ className, ...props }, ref) => ( 51 + <div 52 + ref={ref} 53 + className={cn("text-sm [&_p]:leading-relaxed", className)} 54 + {...props} 55 + /> 56 + )) 57 + AlertDescription.displayName = "AlertDescription" 58 + 59 + export { Alert, AlertTitle, AlertDescription }
+45 -15
apps/web/src/routeTree.gen.ts
··· 8 8 // You should NOT make any changes in this file as it will be overwritten. 9 9 // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. 10 10 11 + import { createFileRoute } from '@tanstack/react-router' 12 + 11 13 // Import Routes 12 14 13 15 import { Route as rootRoute } from './routes/__root' 14 - import { Route as appIndexImport } from './routes/(app)/index' 16 + import { Route as appRecipesDidRkeyImport } from './routes/(app)/recipes.$did.$rkey' 17 + 18 + // Create Virtual Routes 19 + 20 + const appIndexLazyImport = createFileRoute('/(app)/')() 15 21 16 22 // Create/Update Routes 17 23 18 - const appIndexRoute = appIndexImport.update({ 19 - id: '/(app)/', 20 - path: '/', 24 + const appIndexLazyRoute = appIndexLazyImport 25 + .update({ 26 + id: '/(app)/', 27 + path: '/', 28 + getParentRoute: () => rootRoute, 29 + } as any) 30 + .lazy(() => import('./routes/(app)/index.lazy').then((d) => d.Route)) 31 + 32 + const appRecipesDidRkeyRoute = appRecipesDidRkeyImport.update({ 33 + id: '/(app)/recipes/$did/$rkey', 34 + path: '/recipes/$did/$rkey', 21 35 getParentRoute: () => rootRoute, 22 36 } as any) 23 37 ··· 29 43 id: '/(app)/' 30 44 path: '/' 31 45 fullPath: '/' 32 - preLoaderRoute: typeof appIndexImport 46 + preLoaderRoute: typeof appIndexLazyImport 47 + parentRoute: typeof rootRoute 48 + } 49 + '/(app)/recipes/$did/$rkey': { 50 + id: '/(app)/recipes/$did/$rkey' 51 + path: '/recipes/$did/$rkey' 52 + fullPath: '/recipes/$did/$rkey' 53 + preLoaderRoute: typeof appRecipesDidRkeyImport 33 54 parentRoute: typeof rootRoute 34 55 } 35 56 } ··· 38 59 // Create and export the route tree 39 60 40 61 export interface FileRoutesByFullPath { 41 - '/': typeof appIndexRoute 62 + '/': typeof appIndexLazyRoute 63 + '/recipes/$did/$rkey': typeof appRecipesDidRkeyRoute 42 64 } 43 65 44 66 export interface FileRoutesByTo { 45 - '/': typeof appIndexRoute 67 + '/': typeof appIndexLazyRoute 68 + '/recipes/$did/$rkey': typeof appRecipesDidRkeyRoute 46 69 } 47 70 48 71 export interface FileRoutesById { 49 72 __root__: typeof rootRoute 50 - '/(app)/': typeof appIndexRoute 73 + '/(app)/': typeof appIndexLazyRoute 74 + '/(app)/recipes/$did/$rkey': typeof appRecipesDidRkeyRoute 51 75 } 52 76 53 77 export interface FileRouteTypes { 54 78 fileRoutesByFullPath: FileRoutesByFullPath 55 - fullPaths: '/' 79 + fullPaths: '/' | '/recipes/$did/$rkey' 56 80 fileRoutesByTo: FileRoutesByTo 57 - to: '/' 58 - id: '__root__' | '/(app)/' 81 + to: '/' | '/recipes/$did/$rkey' 82 + id: '__root__' | '/(app)/' | '/(app)/recipes/$did/$rkey' 59 83 fileRoutesById: FileRoutesById 60 84 } 61 85 62 86 export interface RootRouteChildren { 63 - appIndexRoute: typeof appIndexRoute 87 + appIndexLazyRoute: typeof appIndexLazyRoute 88 + appRecipesDidRkeyRoute: typeof appRecipesDidRkeyRoute 64 89 } 65 90 66 91 const rootRouteChildren: RootRouteChildren = { 67 - appIndexRoute: appIndexRoute, 92 + appIndexLazyRoute: appIndexLazyRoute, 93 + appRecipesDidRkeyRoute: appRecipesDidRkeyRoute, 68 94 } 69 95 70 96 export const routeTree = rootRoute ··· 77 103 "__root__": { 78 104 "filePath": "__root.tsx", 79 105 "children": [ 80 - "/(app)/" 106 + "/(app)/", 107 + "/(app)/recipes/$did/$rkey" 81 108 ] 82 109 }, 83 110 "/(app)/": { 84 - "filePath": "(app)/index.tsx" 111 + "filePath": "(app)/index.lazy.tsx" 112 + }, 113 + "/(app)/recipes/$did/$rkey": { 114 + "filePath": "(app)/recipes.$did.$rkey.tsx" 85 115 } 86 116 } 87 117 }
+91
apps/web/src/routes/(app)/index.lazy.tsx
··· 1 + import { createLazyFileRoute, Link } from '@tanstack/react-router' 2 + import { 3 + Breadcrumb, 4 + BreadcrumbItem, 5 + BreadcrumbLink, 6 + BreadcrumbList, 7 + BreadcrumbPage, 8 + BreadcrumbSeparator, 9 + } from '@/components/ui/breadcrumb' 10 + import { Separator } from '@/components/ui/separator' 11 + import { SidebarTrigger } from '@/components/ui/sidebar' 12 + import { useQuery } from '@tanstack/react-query' 13 + import { 14 + Card, 15 + CardContent, 16 + CardFooter, 17 + CardHeader, 18 + CardTitle, 19 + } from '@/components/ui/card' 20 + import { useXrpc } from '@/hooks/use-xrpc' 21 + import { Clock, CookingPot, ListIcon } from 'lucide-react' 22 + import QueryPlaceholder from '@/components/query-placeholder' 23 + 24 + export const Route = createLazyFileRoute('/(app)/')({ 25 + component: RouteComponent, 26 + }) 27 + 28 + function RouteComponent() { 29 + const { rpc } = useXrpc() 30 + 31 + const query = useQuery({ 32 + queryKey: ['moe.hayden.cookware.getRecipes', { cursor: '' }], 33 + queryFn: () => 34 + rpc.get('moe.hayden.cookware.getRecipes', { 35 + params: { cursor: '' }, 36 + }), 37 + }) 38 + 39 + return ( 40 + <> 41 + <header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12"> 42 + <div className="flex items-center gap-2 px-4"> 43 + <SidebarTrigger className="-ml-1" /> 44 + <Separator orientation="vertical" className="mr-2 h-4" /> 45 + <Breadcrumb> 46 + <BreadcrumbList> 47 + <BreadcrumbItem className="hidden md:block"> 48 + <BreadcrumbLink asChild><Link href="/">Community</Link></BreadcrumbLink> 49 + </BreadcrumbItem> 50 + <BreadcrumbSeparator className="hidden md:block" /> 51 + <BreadcrumbItem> 52 + <BreadcrumbPage>Browse Recipes</BreadcrumbPage> 53 + </BreadcrumbItem> 54 + </BreadcrumbList> 55 + </Breadcrumb> 56 + </div> 57 + </header> 58 + <div className="flex flex-1 flex-col gap-4 p-4 pt-0"> 59 + <div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-4"> 60 + <QueryPlaceholder query={query} cards cardsCount={12}> 61 + {query.data?.data.recipes.map((v, idx) => ( 62 + <Link key={idx} href={`/recipes/${v.did}/${v.rkey}`}> 63 + <Card> 64 + <CardHeader> 65 + <CardTitle>{v.title}</CardTitle> 66 + </CardHeader> 67 + <CardContent> 68 + <p>{v.description}</p> 69 + </CardContent> 70 + <CardFooter className="flex gap-6 text-sm text-muted-foreground"> 71 + <span className="flex items-center gap-2"> 72 + <ListIcon className="size-4" /> <span>{v.steps}</span> 73 + </span> 74 + 75 + <span className="flex items-center gap-2"> 76 + <CookingPot className="size-4" /> <span>{v.ingredients}</span> 77 + </span> 78 + 79 + <span className="flex items-center gap-2"> 80 + <Clock className="size-4" /> <span>30min.</span> 81 + </span> 82 + </CardFooter> 83 + </Card> 84 + </Link> 85 + ))} 86 + </QueryPlaceholder> 87 + </div> 88 + </div> 89 + </> 90 + ) 91 + }
-69
apps/web/src/routes/(app)/index.tsx
··· 1 - import { createFileRoute } from '@tanstack/react-router' 2 - import { 3 - Breadcrumb, 4 - BreadcrumbItem, 5 - BreadcrumbLink, 6 - BreadcrumbList, 7 - BreadcrumbPage, 8 - BreadcrumbSeparator, 9 - } from '@/components/ui/breadcrumb' 10 - import { Separator } from '@/components/ui/separator' 11 - import { SidebarTrigger } from '@/components/ui/sidebar'; 12 - import { useQuery } from '@tanstack/react-query'; 13 - import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; 14 - import { useXrpc } from '@/hooks/use-xrpc'; 15 - 16 - export const Route = createFileRoute('/(app)/')({ 17 - component: RouteComponent, 18 - }) 19 - 20 - function RouteComponent() { 21 - const { rpc } = useXrpc(); 22 - 23 - const { isPending, data } = useQuery({ 24 - queryKey: ['moe.hayden.cookware.getRecipes', { cursor: '' }], 25 - queryFn: () => rpc.get('moe.hayden.cookware.getRecipes', { 26 - params: { cursor: '' }, 27 - }), 28 - }); 29 - 30 - return ( 31 - <> 32 - <header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12"> 33 - <div className="flex items-center gap-2 px-4"> 34 - <SidebarTrigger className="-ml-1" /> 35 - <Separator orientation="vertical" className="mr-2 h-4" /> 36 - <Breadcrumb> 37 - <BreadcrumbList> 38 - <BreadcrumbItem className="hidden md:block"> 39 - <BreadcrumbLink href="#"> 40 - Community 41 - </BreadcrumbLink> 42 - </BreadcrumbItem> 43 - <BreadcrumbSeparator className="hidden md:block" /> 44 - <BreadcrumbItem> 45 - <BreadcrumbPage>Browse Recipes</BreadcrumbPage> 46 - </BreadcrumbItem> 47 - </BreadcrumbList> 48 - </Breadcrumb> 49 - </div> 50 - </header> 51 - <div className="flex flex-1 flex-col gap-4 p-4 pt-0"> 52 - <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> 53 - {data && data.data.recipes.map((v, idx) => ( 54 - <Card key={idx}> 55 - <CardHeader> 56 - <CardTitle> 57 - <a href={`/recipe/${v.did}/${v.rkey}`}>{v.title}</a> 58 - </CardTitle> 59 - </CardHeader> 60 - <CardContent> 61 - <p>{v.description}</p> 62 - </CardContent> 63 - </Card> 64 - ))} 65 - </div> 66 - </div> 67 - </> 68 - ); 69 - }
+96
apps/web/src/routes/(app)/recipes.$did.$rkey.tsx
··· 1 + import { createFileRoute, Link } from '@tanstack/react-router' 2 + import { 3 + Breadcrumb, 4 + BreadcrumbItem, 5 + BreadcrumbLink, 6 + BreadcrumbList, 7 + BreadcrumbPage, 8 + BreadcrumbSeparator, 9 + } from '@/components/ui/breadcrumb' 10 + import { Separator } from '@/components/ui/separator' 11 + import { SidebarTrigger } from '@/components/ui/sidebar' 12 + import { useQuery } from '@tanstack/react-query' 13 + import { useXrpc } from '@/hooks/use-xrpc' 14 + import QueryPlaceholder from '@/components/query-placeholder' 15 + import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' 16 + 17 + export const Route = createFileRoute('/(app)/recipes/$did/$rkey')({ 18 + component: RouteComponent, 19 + }) 20 + 21 + function RouteComponent() { 22 + const { did, rkey } = Route.useParams() 23 + const { rpc } = useXrpc(); 24 + 25 + const query = useQuery({ 26 + queryKey: ['moe.hayden.cookware.getRecipe', { did, rkey }], 27 + queryFn: () => 28 + rpc.get('moe.hayden.cookware.getRecipe', { params: { did, rkey } }), 29 + }); 30 + 31 + return ( 32 + <> 33 + <header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12"> 34 + <div className="flex items-center gap-2 px-4"> 35 + <SidebarTrigger className="-ml-1" /> 36 + <Separator orientation="vertical" className="mr-2 h-4" /> 37 + <Breadcrumb> 38 + <BreadcrumbList> 39 + <BreadcrumbItem className="hidden md:block"> 40 + <BreadcrumbLink asChild><Link href="/">Community</Link></BreadcrumbLink> 41 + </BreadcrumbItem> 42 + <BreadcrumbSeparator className="hidden md:block" /> 43 + <BreadcrumbItem className="hidden md:block"> 44 + <BreadcrumbLink asChild><Link href="/">Browse Recipes</Link></BreadcrumbLink> 45 + </BreadcrumbItem> 46 + <BreadcrumbSeparator className="hidden md:block" /> 47 + <BreadcrumbItem className="hidden md:block"> 48 + <BreadcrumbLink asChild><Link href={`/profiles/${did}`}>{did}</Link></BreadcrumbLink> 49 + </BreadcrumbItem> 50 + <BreadcrumbSeparator className="hidden md:block" /> 51 + <BreadcrumbItem> 52 + <BreadcrumbPage>{rkey}</BreadcrumbPage> 53 + </BreadcrumbItem> 54 + </BreadcrumbList> 55 + </Breadcrumb> 56 + </div> 57 + </header> 58 + <div className="flex flex-1 flex-col gap-4 p-4 pt-0"> 59 + <QueryPlaceholder query={query}> 60 + <div className="max-w-6xl"> 61 + <h1 className="scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl">{query.data?.data.recipe.title}</h1> 62 + <p className="leading-7 [&:not(:first-child)]:mt-6">{query.data?.data.recipe.description}</p> 63 + </div> 64 + 65 + <div className="grid lg:grid-cols-3 gap-4"> 66 + <Card className="lg:col-start-3"> 67 + <CardHeader> 68 + <CardTitle>Ingredients</CardTitle> 69 + </CardHeader> 70 + <CardContent> 71 + <ul> 72 + {query.data?.data.recipe.ingredients.map((ing, idx) => ( 73 + <li key={idx}>{ing.name} ({ing.amount} {ing.unit})</li> 74 + ))} 75 + </ul> 76 + </CardContent> 77 + </Card> 78 + 79 + <Card className="lg:col-start-1 lg:row-start-1 lg:col-span-2"> 80 + <CardHeader> 81 + <CardTitle>Steps</CardTitle> 82 + </CardHeader> 83 + <CardContent> 84 + <ol> 85 + {query.data?.data.recipe.steps.map((ing, idx) => ( 86 + <li key={idx}>{ing.text}</li> 87 + ))} 88 + </ol> 89 + </CardContent> 90 + </Card> 91 + </div> 92 + </QueryPlaceholder> 93 + </div> 94 + </> 95 + ) 96 + }
+58
lexicons/moe/hayden/cookware/getRecipe.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "moe.hayden.cookware.getRecipe", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "Gets a recipe from the index by author DID and rkey.", 8 + "parameters": { 9 + "type": "params", 10 + "required": ["did", "rkey"], 11 + "properties": { 12 + "did": { 13 + "type": "string", 14 + "format": "at-identifier" 15 + }, 16 + "rkey": { 17 + "type": "string" 18 + } 19 + } 20 + }, 21 + "output": { 22 + "encoding": "application/json", 23 + "schema": { 24 + "type": "object", 25 + "required": ["recipe"], 26 + "properties": { 27 + "recipe": { 28 + "type": "ref", 29 + "ref": "#result" 30 + } 31 + } 32 + } 33 + } 34 + }, 35 + "result": { 36 + "type": "object", 37 + "required": ["title", "ingredients", "steps"], 38 + "properties": { 39 + "title": { "type": "string" }, 40 + "description": { "type": "string" }, 41 + "ingredients": { 42 + "type": "array", 43 + "items": { 44 + "type": "ref", 45 + "ref": "moe.hayden.cookware.defs#ingredient" 46 + } 47 + }, 48 + "steps": { 49 + "type": "array", 50 + "items": { 51 + "type": "ref", 52 + "ref": "moe.hayden.cookware.defs#step" 53 + } 54 + } 55 + } 56 + } 57 + } 58 + }
+2 -14
lexicons/moe/hayden/cookware/getRecipes.json
··· 44 44 "did": { "type": "string" }, 45 45 "title": { "type": "string" }, 46 46 "description": { "type": "string" }, 47 - "ingredients": { 48 - "type": "array", 49 - "items": { 50 - "type": "ref", 51 - "ref": "moe.hayden.cookware.defs#ingredient" 52 - } 53 - }, 54 - "steps": { 55 - "type": "array", 56 - "items": { 57 - "type": "ref", 58 - "ref": "moe.hayden.cookware.defs#step" 59 - } 60 - } 47 + "ingredients": { "type": "integer" }, 48 + "steps": { "type": "integer" } 61 49 } 62 50 } 63 51 }
+25 -2
libs/lexicons/src/atcute.ts
··· 38 38 } 39 39 } 40 40 41 + /** Gets a recipe from the index by author DID and rkey. */ 42 + namespace MoeHaydenCookwareGetRecipe { 43 + interface Params { 44 + did: string; 45 + rkey: string; 46 + } 47 + type Input = undefined; 48 + interface Output { 49 + recipe: Result; 50 + } 51 + interface Result { 52 + [Brand.Type]?: "moe.hayden.cookware.getRecipe#result"; 53 + ingredients: MoeHaydenCookwareDefs.Ingredient[]; 54 + steps: MoeHaydenCookwareDefs.Step[]; 55 + title: string; 56 + description?: string; 57 + } 58 + } 59 + 41 60 /** Gets recipes from the index. */ 42 61 namespace MoeHaydenCookwareGetRecipes { 43 62 interface Params { ··· 52 71 [Brand.Type]?: "moe.hayden.cookware.getRecipes#result"; 53 72 description: string; 54 73 did: string; 55 - ingredients: MoeHaydenCookwareDefs.Ingredient[]; 74 + ingredients: number; 56 75 rkey: string; 57 - steps: MoeHaydenCookwareDefs.Step[]; 76 + steps: number; 58 77 title: string; 59 78 type?: string; 60 79 } ··· 86 105 } 87 106 88 107 interface Queries { 108 + "moe.hayden.cookware.getRecipe": { 109 + params: MoeHaydenCookwareGetRecipe.Params; 110 + output: MoeHaydenCookwareGetRecipe.Output; 111 + }; 89 112 "moe.hayden.cookware.getRecipes": { 90 113 params: MoeHaydenCookwareGetRecipes.Params; 91 114 output: MoeHaydenCookwareGetRecipes.Output;