tangled
alpha
login
or
join now
roost.moe
/
recipes.blue
2
fork
atom
The recipes.blue monorepo
recipes.blue
recipes
appview
atproto
2
fork
atom
overview
issues
1
pulls
pipelines
feat: lexicon & ui updates
hayden.moe
3 months ago
45308949
50c5f49b
verified
This commit was signed with the committer's
known signature
.
hayden.moe
SSH Key Fingerprint:
SHA256:egi2RxHATuWGOtHoLWJQb68bxJ+Jg/4m40QL5UFBWEI=
+82
-107
15 changed files
expand all
collapse all
unified
split
apps
api
src
xrpc
blue.recipes.feed.getRecipe.ts
blue.recipes.feed.getRecipes.ts
web
src
components
nav-user-opts.tsx
nav-user.tsx
query-placeholder.tsx
recipe-card.tsx
queries
recipe.ts
self.ts
routes
_.(app)
index.lazy.tsx
recipes
$author
$rkey
index.lazy.tsx
new.tsx
screens
Recipes
RecipeCard.tsx
tailwind.config.js
libs
lexicons
lexicons
feed
defs.tsp
lib
types
blue
recipes
feed
defs.ts
+5
-2
apps/api/src/xrpc/blue.recipes.feed.getRecipe.ts
···
1
1
import { json, XRPCRouter, XRPCError } from '@atcute/xrpc-server';
2
2
-
import { BlueRecipesFeedGetRecipe, BlueRecipesFeedRecipe } from '@cookware/lexicons';
2
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
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
59
+
rkey: recipe.rkey,
60
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
70
-
}))
73
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
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
10
-
import { useAuth } from "@/state/auth"
10
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
15
-
const { isLoggedIn } = useAuth();
15
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
23
-
import { useAuth } from "@/state/auth"
23
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
30
-
const { isLoggedIn, agent, logOut } = useAuth();
30
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
77
-
<AvatarImage src={`https://cdn.bsky.app/img/avatar_thumbnail/plain/${agent.sub}/${userQuery.data.avatar?.ref.$link}@jpeg`} alt={userQuery.data.displayName} />
77
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
95
-
<AvatarImage src={`https://cdn.bsky.app/img/avatar_thumbnail/plain/${agent.sub}/${userQuery.data.avatar?.ref.$link}@jpeg`} alt={userQuery.data.displayName} />
95
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
104
-
<DropdownMenuItem className="cursor-pointer" onClick={() => logOut()}>
104
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
2
-
import { PropsWithChildren, ReactNode } from 'react';
2
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
8
-
type QueryPlaceholderProps<TData, TError> = PropsWithChildren<{
8
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
-
}>;
13
13
+
children: ReactNode | ReactNode[] | ((data: TData) => ReactNode | ReactNode[]);
14
14
+
};
14
15
15
16
const QueryPlaceholder = <TData = {}, TError = Error>(
16
17
{
···
50
51
</Alert>
51
52
)
52
53
} else if (query.data) {
54
54
+
if (typeof children === 'function') {
55
55
+
return children(query.data);
56
56
+
}
53
57
return children;
54
58
}
55
59
return noData;
+11
-11
apps/web/src/components/recipe-card.tsx
···
1
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
5
+
import { BlueRecipesFeedGetRecipes } from "@cookware/lexicons";
6
6
7
7
type RecipeCardProps = {
8
8
-
recipe: BlueRecipesFeedGetRecipes.Result;
8
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
21
-
{ recipe.imageUrl &&
21
21
+
{ recipe.record.image &&
22
22
<div className="relative h-48 w-full">
23
23
<img
24
24
src={recipe.imageUrl}
25
25
-
alt={recipe.title}
25
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
32
-
<h3 className="text-lg font-semibold mb-2">{recipe.title}</h3>
32
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
34
-
{truncateDescription(recipe.description || '')}
34
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
41
-
<AvatarImage src={recipe.author.avatarUrl} alt={recipe.author.displayName} />
41
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
49
-
<span>{recipe.ingredients}</span>
49
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
54
-
<span>{recipe.steps}</span>
54
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
59
-
<span>{recipe.serves}</span>
59
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
64
-
<span>{recipe.time} min</span>
64
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
1
-
import { useXrpc } from "@/hooks/use-xrpc";
2
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
24
+
if (!res.ok) throw res.data;
26
25
return res.data;
27
26
},
28
27
});
29
28
};
30
29
31
31
-
export const recipeQueryOptions = (rpc: Client, did: Did, rkey: string) => {
30
30
+
export const recipeQueryOptions = (rpc: Client, actor: ActorIdentifier, rkey: string) => {
32
31
return queryOptions({
33
33
-
queryKey: RQKEY('', did, rkey),
32
32
+
queryKey: RQKEY('', actor, rkey),
34
33
queryFn: async () => {
35
34
const { ok, data } = await rpc.get('blue.recipes.feed.getRecipe', {
36
36
-
params: { did, rkey },
35
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
54
-
const rpc = useXrpc();
53
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
59
-
const { agent } = useAuth();
60
60
-
const rpc = useXrpc();
58
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
1
-
import { useXrpc } from "@/hooks/use-xrpc";
2
2
-
import { useAuth } from "@/state/auth";
3
3
-
import { AppBskyActorProfile } from "@atcute/client/lexicons";
4
4
-
import { At } from "@atcute/client/lexicons";
1
1
+
import { useClient, useSession } from "@/state/auth";
2
2
+
import { BlueRecipesActorDefs } from "@cookware/lexicons";
5
3
import { useQuery } from "@tanstack/react-query";
6
4
7
5
export const useUserQuery = () => {
8
8
-
const { isLoggedIn, agent } = useAuth();
9
9
-
const rpc = useXrpc();
6
6
+
const { isLoggedIn, agent } = useSession();
7
7
+
const rpc = useClient();
10
8
11
9
return useQuery({
12
10
queryKey: ['self'],
13
11
queryFn: async () => {
14
14
-
const res = await rpc.get('com.atproto.repo.getRecord', {
12
12
+
const res = await rpc.get('blue.recipes.actor.getProfile', {
15
13
params: {
16
16
-
repo: agent?.sub as At.DID,
17
17
-
collection: 'app.bsky.actor.profile',
18
18
-
rkey: 'self',
14
14
+
actor: agent?.sub!
19
15
},
20
16
});
21
17
22
22
-
return res.data.value as AppBskyActorProfile.Record;
18
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
33
-
<Link href="/">Community</Link>
33
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
51
-
{query.data?.recipes.map((recipe, idx) => (
51
51
+
{data => data.recipes.map(recipe => (
52
52
<RecipeCard
53
53
recipe={recipe}
54
54
-
key={idx}
54
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
15
-
import { useXrpc } from '@/hooks/use-xrpc'
16
15
import { Badge } from '@/components/ui/badge'
17
16
import { Clock, Users } from 'lucide-react'
18
18
-
import { useAuth } from '@/state/auth'
17
17
+
import { useClient, useSession } from '@/state/auth'
19
18
import { Button } from '@/components/ui/button'
19
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
26
-
const rpc = useXrpc();
26
26
+
const rpc = useClient();
27
27
const { author, rkey } = Route.useParams()
28
28
const {
29
29
-
data: { recipe },
29
29
+
data: { recipes },
30
30
error,
31
31
-
} = useSuspenseQuery(recipeQueryOptions(rpc, author, rkey))
32
32
-
const { isLoggedIn, agent } = useAuth();
31
31
+
} = useSuspenseQuery(recipeQueryOptions(rpc, author as ActorIdentifier, rkey))
32
32
+
const { isLoggedIn, agent } = useSession();
33
33
34
34
-
if (error) return <p>Error</p>
34
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
58
-
<Link to="/recipes/$author" params={{ author: recipe.author.handle }}>
59
59
-
{recipe.author.displayName}
58
58
+
<Link to="/recipes/$author" params={{ author: recipes[0].author.handle }}>
59
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
65
-
<BreadcrumbPage>{recipe.title}</BreadcrumbPage>
65
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
75
-
<CardTitle className="text-3xl font-bold">{recipe.title}</CardTitle>
76
76
-
<CardDescription>{recipe.description}</CardDescription>
75
75
+
<CardTitle className="text-3xl font-bold">{recipes[0].record.title}</CardTitle>
76
76
+
<CardDescription>{recipes[0].record.description}</CardDescription>
77
77
</CardHeader>
78
78
79
79
<CardContent className="space-y-6">
80
80
{
81
81
-
recipe.imageUrl &&
81
81
+
recipes[0].record.image &&
82
82
<img
83
83
-
src={recipe.imageUrl}
84
84
-
alt={recipe.title}
83
83
+
src={recipes[0].record.image.ref.$link}
84
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
91
-
<span>{recipe.time} mins</span>
91
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
95
-
<span>Serves {recipe.serves ?? '1'}</span>
95
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
102
-
{recipe.ingredients.map((ing, idx) => (
102
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
113
-
{recipe.steps.map((ing, idx) => (
113
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
120
-
{(isLoggedIn && agent?.sub == recipe.author.did) && (
120
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
12
-
import { useFieldArray, useForm } from "react-hook-form";
13
13
-
import { z } from "zod";
14
14
-
import { zodResolver } from "@hookform/resolvers/zod";
15
15
-
import {
16
16
-
Form,
17
17
-
FormControl,
18
18
-
FormDescription,
19
19
-
FormField,
20
20
-
FormItem,
21
21
-
FormLabel,
22
22
-
FormMessage,
23
23
-
} from "@/components/ui/form";
24
24
-
import { Button } from "@/components/ui/button";
25
25
-
import { Input } from "@/components/ui/input";
26
26
-
import { Textarea } from "@/components/ui/textarea";
27
27
-
import {
28
28
-
Card,
29
29
-
CardContent,
30
30
-
CardDescription,
31
31
-
CardHeader,
32
32
-
CardTitle,
33
33
-
} from "@/components/ui/card";
34
34
-
import {
35
35
-
Sortable,
36
36
-
SortableDragHandle,
37
37
-
SortableItem,
38
38
-
} from "@/components/ui/sortable";
39
39
-
import { DragHandleDots2Icon } from "@radix-ui/react-icons";
40
40
-
import { Label } from "@/components/ui/label";
41
41
-
import { TrashIcon } from "lucide-react";
42
42
-
import { useNewRecipeMutation } from "@/queries/recipe";
43
12
44
13
export const Route = createFileRoute("/_/(app)/recipes/new")({
45
14
beforeLoad: async ({ context }) => {
46
46
-
if (!context.auth.isLoggedIn) {
15
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
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
5
+
import { BlueRecipesFeedGetRecipes } from "@cookware/lexicons";
6
6
7
7
type RecipeCardProps = {
8
8
-
recipe: BlueRecipesFeedGetRecipes.Result;
8
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
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
18
-
<AvatarImage src={recipe.author.avatarUrl} alt={recipe.author.displayName} />
19
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
24
-
<CardTitle>{recipe.title}</CardTitle>
25
25
</CardHeader>
26
26
<CardContent>
27
27
-
<p>{recipe.description}</p>
27
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
31
-
<ListIcon className="size-4" /> <span>{recipe.steps}</span>
31
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
35
-
<CookingPot className="size-4" /> <span>{recipe.ingredients}</span>
35
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
39
-
<Clock className="size-4" /> <span>{recipe.time} mins</span>
39
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
4
-
darkMode: ["class"],
5
5
-
content: ["./index.html", "./src/**/*.{ts,tsx,js,jsx}"],
4
4
+
darkMode: ["class"],
5
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
9
+
@required rkey: string;
10
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
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
26
+
rkey: /*#__PURE__*/ v.string(),
25
27
uri: /*#__PURE__*/ v.resourceUriString(),
26
28
});
27
29