The recipes.blue monorepo recipes.blue
recipes appview atproto

feat: lexicon & ui updates

hayden.moe 45308949 50c5f49b

verified
Changed files
+82 -107
apps
libs
lexicons
lexicons
feed
lib
types
blue
recipes
feed
+5 -2
apps/api/src/xrpc/blue.recipes.feed.getRecipe.ts
··· 1 1 import { json, XRPCRouter, XRPCError } from '@atcute/xrpc-server'; 2 - import { BlueRecipesFeedGetRecipe, BlueRecipesFeedRecipe } from '@cookware/lexicons'; 2 + import { BlueRecipesFeedDefs, BlueRecipesFeedGetRecipe, BlueRecipesFeedRecipe } from '@cookware/lexicons'; 3 3 import { db, and, or, eq } from '@cookware/database'; 4 4 import { buildProfileViewBasic, parseDid } from '../util/api.js'; 5 5 import { Logger } from 'pino'; ··· 7 7 import { recipeTable } from '@cookware/database/schema'; 8 8 import { isLegacyBlob } from '@atcute/lexicons/interfaces'; 9 9 import { RedisClient } from 'bun'; 10 + import { buildCdnUrl } from '../util/cdn.js'; 10 11 11 12 const invalidUriError = (uri: string) => new XRPCError({ 12 13 status: 400, ··· 55 56 uri: recipe.uri as ResourceUri, 56 57 author: await buildProfileViewBasic(author, redis), 57 58 cid: recipe.cid, 59 + rkey: recipe.rkey, 60 + imageUrl: recipe.imageRef ? buildCdnUrl('post_image', recipe.did, recipe.imageRef) : undefined, 58 61 indexedAt: recipe.ingestedAt.toISOString(), 59 62 record: { 60 63 $type: BlueRecipesFeedRecipe.mainSchema.object.shape.$type.expected, ··· 67 70 image: isLegacyBlob(recipe.imageRef) ? undefined : recipe.imageRef ?? undefined, 68 71 createdAt: recipe.createdAt.toISOString(), 69 72 }, 70 - })) 73 + })), 71 74 ), 72 75 }); 73 76 },
+1
apps/api/src/xrpc/blue.recipes.feed.getRecipes.ts
··· 58 58 createdAt: recipe.author.createdAt.toISOString(), 59 59 }, 60 60 cid: recipe.cid, 61 + rkey: recipe.rkey, 61 62 indexedAt: recipe.ingestedAt.toISOString(), 62 63 record: { 63 64 $type: BlueRecipesFeedRecipe.mainSchema.object.shape.$type.expected,
+2 -2
apps/web/src/components/nav-user-opts.tsx
··· 7 7 SidebarMenuButton, 8 8 SidebarMenuItem, 9 9 } from "@/components/ui/sidebar" 10 - import { useAuth } from "@/state/auth" 10 + import { useSession } from "@/state/auth" 11 11 import { Link } from "@tanstack/react-router"; 12 12 import { LifeBuoy, Pencil, Send } from "lucide-react"; 13 13 14 14 export function NavUserOpts() { 15 - const { isLoggedIn } = useAuth(); 15 + const { isLoggedIn } = useSession(); 16 16 17 17 if (!isLoggedIn) { 18 18 return (
+5 -5
apps/web/src/components/nav-user.tsx
··· 20 20 } from "@/components/ui/sidebar" 21 21 import { Button } from "./ui/button" 22 22 import { Link } from "@tanstack/react-router" 23 - import { useAuth } from "@/state/auth" 23 + import { useSession } from "@/state/auth" 24 24 import { Skeleton } from "./ui/skeleton" 25 25 import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar" 26 26 import { useUserQuery } from "@/queries/self" 27 27 28 28 export function NavUser() { 29 29 const { isMobile } = useSidebar() 30 - const { isLoggedIn, agent, logOut } = useAuth(); 30 + const { isLoggedIn, agent, signOut } = useSession(); 31 31 32 32 const userQuery = useUserQuery(); 33 33 ··· 74 74 className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground" 75 75 > 76 76 <Avatar className="h-8 w-8 rounded-lg"> 77 - <AvatarImage src={`https://cdn.bsky.app/img/avatar_thumbnail/plain/${agent.sub}/${userQuery.data.avatar?.ref.$link}@jpeg`} alt={userQuery.data.displayName} /> 77 + <AvatarImage src={userQuery.data.avatar} alt={userQuery.data.displayName} /> 78 78 <AvatarFallback className="rounded-lg">{userQuery.data.displayName}</AvatarFallback> 79 79 </Avatar> 80 80 <div className="grid flex-1 text-left text-sm leading-tight"> ··· 92 92 <DropdownMenuLabel className="p-0 font-normal"> 93 93 <div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm"> 94 94 <Avatar className="h-8 w-8 rounded-lg"> 95 - <AvatarImage src={`https://cdn.bsky.app/img/avatar_thumbnail/plain/${agent.sub}/${userQuery.data.avatar?.ref.$link}@jpeg`} alt={userQuery.data.displayName} /> 95 + <AvatarImage src={userQuery.data.avatar} alt={userQuery.data.displayName} /> 96 96 <AvatarFallback className="rounded-lg">{userQuery.data.displayName}</AvatarFallback> 97 97 </Avatar> 98 98 <div className="grid flex-1 text-left text-sm leading-tight"> ··· 101 101 </div> 102 102 </DropdownMenuLabel> 103 103 <DropdownMenuSeparator /> 104 - <DropdownMenuItem className="cursor-pointer" onClick={() => logOut()}> 104 + <DropdownMenuItem className="cursor-pointer" onClick={() => signOut()}> 105 105 <LogOut /> 106 106 Log out 107 107 </DropdownMenuItem>
+7 -3
apps/web/src/components/query-placeholder.tsx
··· 1 1 import type { UseQueryResult } from '@tanstack/react-query'; 2 - import { PropsWithChildren, ReactNode } from 'react'; 2 + import { ReactNode } from 'react'; 3 3 import { Skeleton } from './ui/skeleton'; 4 4 import { Alert, AlertDescription, AlertTitle } from './ui/alert'; 5 5 import { AlertCircle } from 'lucide-react'; 6 6 import { isXRPCErrorPayload } from '@atcute/client'; 7 7 8 - type QueryPlaceholderProps<TData, TError> = PropsWithChildren<{ 8 + type QueryPlaceholderProps<TData, TError> = { 9 9 query: UseQueryResult<TData, TError>; 10 10 cards?: boolean; 11 11 cardsCount?: number; 12 12 noData?: ReactNode; 13 - }>; 13 + children: ReactNode | ReactNode[] | ((data: TData) => ReactNode | ReactNode[]); 14 + }; 14 15 15 16 const QueryPlaceholder = <TData = {}, TError = Error>( 16 17 { ··· 50 51 </Alert> 51 52 ) 52 53 } else if (query.data) { 54 + if (typeof children === 'function') { 55 + return children(query.data); 56 + } 53 57 return children; 54 58 } 55 59 return noData;
+11 -11
apps/web/src/components/recipe-card.tsx
··· 1 - import { BlueRecipesFeedGetRecipes } from "@atcute/client/lexicons"; 2 1 import { Card, CardContent, CardFooter, CardHeader } from "./ui/card"; 3 2 import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar"; 4 3 import { Link } from "@tanstack/react-router"; 5 4 import { Clock, ListOrdered, Users, Utensils } from "lucide-react"; 5 + import { BlueRecipesFeedGetRecipes } from "@cookware/lexicons"; 6 6 7 7 type RecipeCardProps = { 8 - recipe: BlueRecipesFeedGetRecipes.Result; 8 + recipe: BlueRecipesFeedGetRecipes.$output['recipes'][0]; 9 9 }; 10 10 11 11 function truncateDescription(description: string, maxLength: number = 120) { ··· 18 18 <Link to="/recipes/$author/$rkey" params={{ author: recipe.author.handle, rkey: recipe.rkey }} className="w-full"> 19 19 <Card className="overflow-hidden"> 20 20 <CardHeader className="p-0"> 21 - { recipe.imageUrl && 21 + { recipe.record.image && 22 22 <div className="relative h-48 w-full"> 23 23 <img 24 24 src={recipe.imageUrl} 25 - alt={recipe.title} 25 + alt={recipe.record.title} 26 26 className="h-full w-full object-cover" 27 27 /> 28 28 </div> 29 29 } 30 30 </CardHeader> 31 31 <CardContent className="p-4"> 32 - <h3 className="text-lg font-semibold mb-2">{recipe.title}</h3> 32 + <h3 className="text-lg font-semibold mb-2">{recipe.record.title}</h3> 33 33 <p className="text-sm text-muted-foreground mb-4"> 34 - {truncateDescription(recipe.description || '')} 34 + {truncateDescription(recipe.record.description || '')} 35 35 </p> 36 36 </CardContent> 37 37 <CardFooter className="p-4 pt-0"> 38 38 <div className="w-full flex items-center justify-between"> 39 39 <div className="flex items-center"> 40 40 <Avatar className="h-8 w-8 mr-2"> 41 - <AvatarImage src={recipe.author.avatarUrl} alt={recipe.author.displayName} /> 41 + <AvatarImage src={recipe.author.avatar} alt={recipe.author.displayName} /> 42 42 <AvatarFallback className="rounded-lg">{recipe.author.displayName?.charAt(0)}</AvatarFallback> 43 43 </Avatar> 44 44 <span className="text-sm text-muted-foreground">{recipe.author.displayName}</span> ··· 46 46 <div className="flex gap-6 justify-between items-center text-sm text-muted-foreground"> 47 47 <div className="flex items-center"> 48 48 <Utensils className="w-4 h-4 mr-1" /> 49 - <span>{recipe.ingredients}</span> 49 + <span>{recipe.record.ingredients.length}</span> 50 50 </div> 51 51 52 52 <div className="flex items-center"> 53 53 <ListOrdered className="w-4 h-4 mr-1" /> 54 - <span>{recipe.steps}</span> 54 + <span>{recipe.record.steps.length}</span> 55 55 </div> 56 56 57 57 <div className="flex items-center"> 58 58 <Users className="w-4 h-4 mr-1" /> 59 - <span>{recipe.serves}</span> 59 + <span>{recipe.record.serves}</span> 60 60 </div> 61 61 62 62 <div className="flex items-center"> 63 63 <Clock className="w-4 h-4 mr-1" /> 64 - <span>{recipe.time} min</span> 64 + <span>{recipe.record.time} min</span> 65 65 </div> 66 66 </div> 67 67 </div>
+6 -8
apps/web/src/queries/recipe.ts
··· 1 - import { useXrpc } from "@/hooks/use-xrpc"; 2 - import { useAuth } from "@/state/auth"; 3 1 import { queryOptions, useMutation, useQuery } from "@tanstack/react-query"; 4 2 import { Client } from "@atcute/client"; 5 3 import { notFound } from "@tanstack/react-router"; ··· 23 21 const res = await client.get('blue.recipes.feed.getRecipes', { 24 22 params: { cursor, did }, 25 23 }); 24 + if (!res.ok) throw res.data; 26 25 return res.data; 27 26 }, 28 27 }); 29 28 }; 30 29 31 - export const recipeQueryOptions = (rpc: Client, did: Did, rkey: string) => { 30 + export const recipeQueryOptions = (rpc: Client, actor: ActorIdentifier, rkey: string) => { 32 31 return queryOptions({ 33 - queryKey: RQKEY('', did, rkey), 32 + queryKey: RQKEY('', actor, rkey), 34 33 queryFn: async () => { 35 34 const { ok, data } = await rpc.get('blue.recipes.feed.getRecipe', { 36 - params: { did, rkey }, 35 + params: { uris: [`at://${actor}/blue.recipes.feed.recipe/${rkey}`] }, 37 36 }); 38 37 39 38 if (!ok) { ··· 51 50 }; 52 51 53 52 export const useRecipeQuery = (did: Did, rkey: string) => { 54 - const rpc = useXrpc(); 53 + const rpc = useClient(); 55 54 return useQuery(recipeQueryOptions(rpc, did, rkey)); 56 55 }; 57 56 58 57 export const useNewRecipeMutation = (form: UseFormReturn<z.infer<typeof recipeSchema>>) => { 59 - const { agent } = useAuth(); 60 - const rpc = useXrpc(); 58 + const rpc = useClient(); 61 59 return useMutation({ 62 60 mutationKey: ['recipes.new'], 63 61 mutationFn: async ({ recipe: { image, ...recipe } }: { recipe: z.infer<typeof recipeSchema> }) => {
+7 -11
apps/web/src/queries/self.ts
··· 1 - import { useXrpc } from "@/hooks/use-xrpc"; 2 - import { useAuth } from "@/state/auth"; 3 - import { AppBskyActorProfile } from "@atcute/client/lexicons"; 4 - import { At } from "@atcute/client/lexicons"; 1 + import { useClient, useSession } from "@/state/auth"; 2 + import { BlueRecipesActorDefs } from "@cookware/lexicons"; 5 3 import { useQuery } from "@tanstack/react-query"; 6 4 7 5 export const useUserQuery = () => { 8 - const { isLoggedIn, agent } = useAuth(); 9 - const rpc = useXrpc(); 6 + const { isLoggedIn, agent } = useSession(); 7 + const rpc = useClient(); 10 8 11 9 return useQuery({ 12 10 queryKey: ['self'], 13 11 queryFn: async () => { 14 - const res = await rpc.get('com.atproto.repo.getRecord', { 12 + const res = await rpc.get('blue.recipes.actor.getProfile', { 15 13 params: { 16 - repo: agent?.sub as At.DID, 17 - collection: 'app.bsky.actor.profile', 18 - rkey: 'self', 14 + actor: agent?.sub! 19 15 }, 20 16 }); 21 17 22 - return res.data.value as AppBskyActorProfile.Record; 18 + return res.data as BlueRecipesActorDefs.ProfileViewDetailed; 23 19 }, 24 20 enabled: isLoggedIn, 25 21 });
+3 -3
apps/web/src/routes/_.(app)/index.lazy.tsx
··· 30 30 <BreadcrumbList> 31 31 <BreadcrumbItem className="hidden md:block"> 32 32 <BreadcrumbLink asChild> 33 - <Link href="/">Community</Link> 33 + <Link to="/">Community</Link> 34 34 </BreadcrumbLink> 35 35 </BreadcrumbItem> 36 36 <BreadcrumbSeparator className="hidden md:block" /> ··· 48 48 <div className="flex-1 flex flex-col items-center p-4"> 49 49 <div className="flex flex-col gap-4 max-w-2xl w-full items-center"> 50 50 <QueryPlaceholder query={query} cards cardsCount={12}> 51 - {query.data?.recipes.map((recipe, idx) => ( 51 + {data => data.recipes.map(recipe => ( 52 52 <RecipeCard 53 53 recipe={recipe} 54 - key={idx} 54 + key={`${recipe.author.did}-${recipe.rkey}`} 55 55 /> 56 56 ))} 57 57 </QueryPlaceholder>
+20 -20
apps/web/src/routes/_.(app)/recipes/$author/$rkey/index.lazy.tsx
··· 12 12 import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card' 13 13 import { recipeQueryOptions } from '@/queries/recipe' 14 14 import { useSuspenseQuery } from '@tanstack/react-query' 15 - import { useXrpc } from '@/hooks/use-xrpc' 16 15 import { Badge } from '@/components/ui/badge' 17 16 import { Clock, Users } from 'lucide-react' 18 - import { useAuth } from '@/state/auth' 17 + import { useClient, useSession } from '@/state/auth' 19 18 import { Button } from '@/components/ui/button' 19 + import { ActorIdentifier } from '@atcute/lexicons' 20 20 21 21 export const Route = createLazyFileRoute('/_/(app)/recipes/$author/$rkey/')({ 22 22 component: RouteComponent, 23 23 }) 24 24 25 25 function RouteComponent() { 26 - const rpc = useXrpc(); 26 + const rpc = useClient(); 27 27 const { author, rkey } = Route.useParams() 28 28 const { 29 - data: { recipe }, 29 + data: { recipes }, 30 30 error, 31 - } = useSuspenseQuery(recipeQueryOptions(rpc, author, rkey)) 32 - const { isLoggedIn, agent } = useAuth(); 31 + } = useSuspenseQuery(recipeQueryOptions(rpc, author as ActorIdentifier, rkey)) 32 + const { isLoggedIn, agent } = useSession(); 33 33 34 - if (error) return <p>Error</p> 34 + if (error || !recipes[0]) return <p>Error</p> 35 35 36 36 return ( 37 37 <> ··· 55 55 <BreadcrumbSeparator className="hidden md:block" /> 56 56 <BreadcrumbItem className="hidden md:block"> 57 57 <BreadcrumbLink asChild> 58 - <Link to="/recipes/$author" params={{ author: recipe.author.handle }}> 59 - {recipe.author.displayName} 58 + <Link to="/recipes/$author" params={{ author: recipes[0].author.handle }}> 59 + {recipes[0].author.displayName} 60 60 </Link> 61 61 </BreadcrumbLink> 62 62 </BreadcrumbItem> 63 63 <BreadcrumbSeparator className="hidden md:block" /> 64 64 <BreadcrumbItem> 65 - <BreadcrumbPage>{recipe.title}</BreadcrumbPage> 65 + <BreadcrumbPage>{recipes[0].record.title}</BreadcrumbPage> 66 66 </BreadcrumbItem> 67 67 </BreadcrumbList> 68 68 </Breadcrumb> ··· 72 72 <Card className="w-full"> 73 73 74 74 <CardHeader> 75 - <CardTitle className="text-3xl font-bold">{recipe.title}</CardTitle> 76 - <CardDescription>{recipe.description}</CardDescription> 75 + <CardTitle className="text-3xl font-bold">{recipes[0].record.title}</CardTitle> 76 + <CardDescription>{recipes[0].record.description}</CardDescription> 77 77 </CardHeader> 78 78 79 79 <CardContent className="space-y-6"> 80 80 { 81 - recipe.imageUrl && 81 + recipes[0].record.image && 82 82 <img 83 - src={recipe.imageUrl} 84 - alt={recipe.title} 83 + src={recipes[0].record.image.ref.$link} 84 + alt={recipes[0].record.title} 85 85 className="h-64 w-full object-cover rounded-md" 86 86 /> 87 87 } 88 88 <div className="flex flex-wrap gap-4"> 89 89 <Badge variant="secondary" className="flex items-center gap-2"> 90 90 <Clock className="size-4" /> 91 - <span>{recipe.time} mins</span> 91 + <span>{recipes[0].record.time} mins</span> 92 92 </Badge> 93 93 <Badge variant="secondary" className="flex items-center gap-2"> 94 94 <Users className="size-4" /> 95 - <span>Serves {recipe.serves ?? '1'}</span> 95 + <span>Serves {recipes[0].record.serves ?? '1'}</span> 96 96 </Badge> 97 97 </div> 98 98 99 99 <div> 100 100 <h3 className="text-xl font-semibold mb-2">Ingredients</h3> 101 101 <ul className="list-disc list-inside space-y-1"> 102 - {recipe.ingredients.map((ing, idx) => ( 102 + {recipes[0].record.ingredients.map((ing, idx) => ( 103 103 <li key={idx}> 104 104 <b>{ing.amount}</b> {ing.name} 105 105 </li> ··· 110 110 <div> 111 111 <h3 className="text-xl font-semibold mb-2">Steps</h3> 112 112 <ol className="list-decimal list-outside space-y-1 ml-4"> 113 - {recipe.steps.map((ing, idx) => ( 113 + {recipes[0].record.steps.map((ing, idx) => ( 114 114 <li key={idx}>{ing.text}</li> 115 115 ))} 116 116 </ol> 117 117 </div> 118 118 </CardContent> 119 119 <CardFooter className="flex justify-between"> 120 - {(isLoggedIn && agent?.sub == recipe.author.did) && ( 120 + {(isLoggedIn && agent?.sub == recipes[0].author.did) && ( 121 121 <div className="flex items-center gap-x-4"> 122 122 <Button variant="outline">Edit</Button> 123 123 <Button variant="destructive">Delete</Button>
+1 -32
apps/web/src/routes/_.(app)/recipes/new.tsx
··· 9 9 } from "@/components/ui/breadcrumb"; 10 10 import { Separator } from "@/components/ui/separator"; 11 11 import { SidebarTrigger } from "@/components/ui/sidebar"; 12 - import { useFieldArray, useForm } from "react-hook-form"; 13 - import { z } from "zod"; 14 - import { zodResolver } from "@hookform/resolvers/zod"; 15 - import { 16 - Form, 17 - FormControl, 18 - FormDescription, 19 - FormField, 20 - FormItem, 21 - FormLabel, 22 - FormMessage, 23 - } from "@/components/ui/form"; 24 - import { Button } from "@/components/ui/button"; 25 - import { Input } from "@/components/ui/input"; 26 - import { Textarea } from "@/components/ui/textarea"; 27 - import { 28 - Card, 29 - CardContent, 30 - CardDescription, 31 - CardHeader, 32 - CardTitle, 33 - } from "@/components/ui/card"; 34 - import { 35 - Sortable, 36 - SortableDragHandle, 37 - SortableItem, 38 - } from "@/components/ui/sortable"; 39 - import { DragHandleDots2Icon } from "@radix-ui/react-icons"; 40 - import { Label } from "@/components/ui/label"; 41 - import { TrashIcon } from "lucide-react"; 42 - import { useNewRecipeMutation } from "@/queries/recipe"; 43 12 44 13 export const Route = createFileRoute("/_/(app)/recipes/new")({ 45 14 beforeLoad: async ({ context }) => { 46 - if (!context.auth.isLoggedIn) { 15 + if (!context.session.isLoggedIn) { 47 16 throw redirect({ 48 17 to: '/login', 49 18 });
+8 -8
apps/web/src/screens/Recipes/RecipeCard.tsx
··· 1 1 import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar"; 2 2 import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; 3 - import { BlueRecipesFeedGetRecipes } from "@atcute/client/lexicons"; 4 3 import { Link } from "@tanstack/react-router"; 5 4 import { Clock, CookingPot, ListIcon } from "lucide-react"; 5 + import { BlueRecipesFeedGetRecipes } from "@cookware/lexicons"; 6 6 7 7 type RecipeCardProps = { 8 - recipe: BlueRecipesFeedGetRecipes.Result; 8 + recipe: BlueRecipesFeedGetRecipes.$output['recipes'][0]; 9 9 }; 10 10 11 11 export const RecipeCard = ({ recipe }: RecipeCardProps) => { ··· 13 13 <Link to="/recipes/$author/$rkey" params={{ author: recipe.author.handle, rkey: recipe.rkey }} className="w-full"> 14 14 <Card className="w-full"> 15 15 <CardHeader> 16 + <CardTitle>{recipe.record.title}</CardTitle> 16 17 <CardDescription className="flex items-center space-x-2"> 17 18 <Avatar className="h-6 w-6 rounded-lg"> 18 - <AvatarImage src={recipe.author.avatarUrl} alt={recipe.author.displayName} /> 19 + <AvatarImage src={recipe.author.avatar} alt={recipe.author.displayName} /> 19 20 <AvatarFallback className="rounded-lg">{recipe.author.displayName}</AvatarFallback> 20 21 </Avatar> 21 22 22 23 <span>{recipe.author.displayName}</span> 23 24 </CardDescription> 24 - <CardTitle>{recipe.title}</CardTitle> 25 25 </CardHeader> 26 26 <CardContent> 27 - <p>{recipe.description}</p> 27 + <p>{recipe.record.description}</p> 28 28 </CardContent> 29 29 <CardFooter className="flex gap-6 text-sm text-muted-foreground"> 30 30 <span className="flex items-center gap-2"> 31 - <ListIcon className="size-4" /> <span>{recipe.steps}</span> 31 + <ListIcon className="size-4" /> <span>{recipe.record.steps.length}</span> 32 32 </span> 33 33 34 34 <span className="flex items-center gap-2"> 35 - <CookingPot className="size-4" /> <span>{recipe.ingredients}</span> 35 + <CookingPot className="size-4" /> <span>{recipe.record.ingredients.length}</span> 36 36 </span> 37 37 38 38 <span className="flex items-center gap-2"> 39 - <Clock className="size-4" /> <span>{recipe.time} mins</span> 39 + <Clock className="size-4" /> <span>{recipe.record.time} mins</span> 40 40 </span> 41 41 </CardFooter> 42 42 </Card>
+2 -2
apps/web/tailwind.config.js
··· 1 1 import animate from 'tailwindcss-animate'; 2 2 /** @type {import('tailwindcss').Config} */ 3 3 export default { 4 - darkMode: ["class"], 5 - content: ["./index.html", "./src/**/*.{ts,tsx,js,jsx}"], 4 + darkMode: ["class"], 5 + content: ["./index.html", "./src/**/*.{ts,tsx,js,jsx}"], 6 6 theme: { 7 7 extend: { 8 8 borderRadius: {
+2
libs/lexicons/lexicons/feed/defs.tsp
··· 6 6 model RecipeView { 7 7 @required uri: atUri; 8 8 @required cid: cid; 9 + @required rkey: string; 10 + imageUrl?: url; 9 11 @required author: blue.recipes.actor.defs.ProfileViewBasic; 10 12 @required record: blue.recipes.feed.recipe.Main; 11 13 @required indexedAt: datetime;
+2
libs/lexicons/lib/types/blue/recipes/feed/defs.ts
··· 18 18 return BlueRecipesActorDefs.profileViewBasicSchema; 19 19 }, 20 20 cid: /*#__PURE__*/ v.cidString(), 21 + imageUrl: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 21 22 indexedAt: /*#__PURE__*/ v.datetimeString(), 22 23 get record() { 23 24 return BlueRecipesFeedRecipe.mainSchema; 24 25 }, 26 + rkey: /*#__PURE__*/ v.string(), 25 27 uri: /*#__PURE__*/ v.resourceUriString(), 26 28 }); 27 29