+10
-5
apps/api/src/xrpc/index.ts
+10
-5
apps/api/src/xrpc/index.ts
···
35
handler: simpleFetchHandler({
36
service: 'https://public.api.bsky.app',
37
}),
38
-
})
39
40
let authorInfo: BlueRecipesFeedDefs.AuthorInfo | null = null;
41
if (did) {
···
94
});
95
}
96
97
-
const author = await getDidDoc(recipe.authorDid);
98
99
return ctx.json({
100
recipe: {
101
-
author: {
102
-
handle: author.alsoKnownAs[0]?.substring(5),
103
-
},
104
title: recipe.title,
105
description: recipe.description,
106
ingredients: recipe.ingredients,
107
steps: recipe.steps,
···
35
handler: simpleFetchHandler({
36
service: 'https://public.api.bsky.app',
37
}),
38
+
});
39
40
let authorInfo: BlueRecipesFeedDefs.AuthorInfo | null = null;
41
if (did) {
···
94
});
95
}
96
97
+
const rpc = new XRPC({
98
+
handler: simpleFetchHandler({
99
+
service: 'https://public.api.bsky.app',
100
+
}),
101
+
});
102
+
103
+
const authorInfo = await getAuthorInfo(recipe.authorDid, rpc);
104
105
return ctx.json({
106
recipe: {
107
+
author: authorInfo,
108
title: recipe.title,
109
+
time: 5,
110
description: recipe.description,
111
ingredients: recipe.ingredients,
112
steps: recipe.steps,
+28
-32
apps/web/src/routes/_.(app)/recipes/new.tsx
+28
-32
apps/web/src/routes/_.(app)/recipes/new.tsx
···
56
component: RouteComponent,
57
});
58
59
-
const schema = RecipeRecord.extend({
60
-
ingredients: z.array(
61
-
IngredientObject.extend({
62
-
amount: z.coerce.number().nullable(),
63
-
}),
64
-
),
65
-
});
66
67
function RouteComponent() {
68
const form = useForm<z.infer<typeof schema>>({
···
136
{...field}
137
/>
138
</FormControl>
139
<FormMessage />
140
</FormItem>
141
)}
···
151
<div className="flex w-full flex-col gap-2">
152
{ingredients.fields.map((field, index) => (
153
<SortableItem key={field.id} value={field.id} asChild>
154
-
<div className="grid grid-cols-[2rem_1fr_0.2fr_0.2fr_2rem] items-center gap-2">
155
<SortableDragHandle
156
type="button"
157
variant="outline"
···
166
167
<FormField
168
control={form.control}
169
-
name={`ingredients.${index}.name`}
170
-
render={({ field }) => (
171
-
<FormItem>
172
-
<FormControl>
173
-
<Input
174
-
placeholder="Ingredient"
175
-
className="h-8"
176
-
{...field}
177
-
/>
178
-
</FormControl>
179
-
<FormMessage />
180
-
</FormItem>
181
-
)}
182
-
/>
183
-
184
-
<FormField
185
-
control={form.control}
186
name={`ingredients.${index}.amount`}
187
render={({ field: { value, ...field } }) => (
188
<FormItem>
189
<FormControl>
190
<Input
191
-
type="number"
192
-
placeholder="#"
193
-
value={value || "0"}
194
className="h-8"
195
{...field}
196
/>
···
202
203
<FormField
204
control={form.control}
205
-
name={`ingredients.${index}.unit`}
206
-
render={({ field: { value, ...field } }) => (
207
<FormItem>
208
<FormControl>
209
<Input
210
-
placeholder="Unit"
211
className="h-8"
212
-
value={value || ""}
213
{...field}
214
/>
215
</FormControl>
···
56
component: RouteComponent,
57
});
58
59
+
const schema = RecipeRecord;
60
61
function RouteComponent() {
62
const form = useForm<z.infer<typeof schema>>({
···
130
{...field}
131
/>
132
</FormControl>
133
+
<FormDescription>Describe your recipe, maybe tell the world how tasty it is? (Optional)</FormDescription>
134
+
<FormMessage />
135
+
</FormItem>
136
+
)}
137
+
/>
138
+
139
+
<FormField
140
+
name="time"
141
+
control={form.control}
142
+
render={({ field: { value, ...field } }) => (
143
+
<FormItem>
144
+
<FormLabel>Time</FormLabel>
145
+
<FormControl>
146
+
<Input
147
+
type="number"
148
+
className="resize-none"
149
+
value={value || ""}
150
+
{...field}
151
+
/>
152
+
</FormControl>
153
+
<FormDescription>How long (in minutes) does your recipe take to complete?</FormDescription>
154
<FormMessage />
155
</FormItem>
156
)}
···
166
<div className="flex w-full flex-col gap-2">
167
{ingredients.fields.map((field, index) => (
168
<SortableItem key={field.id} value={field.id} asChild>
169
+
<div className="grid grid-cols-[2rem_0.3fr_1fr_2rem] items-center gap-2">
170
<SortableDragHandle
171
type="button"
172
variant="outline"
···
181
182
<FormField
183
control={form.control}
184
name={`ingredients.${index}.amount`}
185
render={({ field: { value, ...field } }) => (
186
<FormItem>
187
<FormControl>
188
<Input
189
+
placeholder="Amount"
190
+
value={value || ""}
191
className="h-8"
192
{...field}
193
/>
···
199
200
<FormField
201
control={form.control}
202
+
name={`ingredients.${index}.name`}
203
+
render={({ field }) => (
204
<FormItem>
205
<FormControl>
206
<Input
207
+
placeholder="Ingredient"
208
className="h-8"
209
{...field}
210
/>
211
</FormControl>
+1
-2
libs/lexicons/src/defs.ts
+1
-2
libs/lexicons/src/defs.ts
···
1
import { z } from 'zod';
2
3
export const IngredientObject = z.object({
4
name: z.string().max(3000, 'Ingredient names must be under 3000 characters.'),
5
-
amount: z.number().nullable(),
6
-
unit: z.string().max(3000, 'Ingredient units must be under 3000 characters.').nullable(),
7
});
8
9
export type Ingredient = z.infer<typeof IngredientObject>;
+1
libs/lexicons/src/recipe.ts
+1
libs/lexicons/src/recipe.ts
···
6
export const RecipeRecord = z.object({
7
title: z.string().max(3000, 'Recipe titles must be under 3000 characters.'),
8
description: z.string().max(3000, 'Recipe descriptions must be under 3000 characters.').nullable(),
9
ingredients: z.array(IngredientObject),
10
steps: z.array(StepObject),
11
});
···
6
export const RecipeRecord = z.object({
7
title: z.string().max(3000, 'Recipe titles must be under 3000 characters.'),
8
description: z.string().max(3000, 'Recipe descriptions must be under 3000 characters.').nullable(),
9
+
time: z.number({ message: 'Time must be a number.' }),
10
ingredients: z.array(IngredientObject),
11
steps: z.array(StepObject),
12
});