The recipes.blue monorepo recipes.blue
recipes appview atproto

feat: secondary nav with new recipe link

+105 -69
+1 -50
apps/api/src/recipes/index.ts
··· 1 1 import { Hono } from "hono"; 2 - import { getDidDoc, getPdsUrl, RecipeCollection, RecipeRecord } from "@cookware/lexicons"; 3 - import { TID } from "@atproto/common"; 4 - import { verifyJwt } from "../util/jwt.js"; 5 - import { XRPCError } from "../util/xrpc.js"; 6 2 7 3 export const recipeApp = new Hono(); 8 4 9 - recipeApp.post('/', async ctx => { 10 - const authz = ctx.req.header('Authorization'); 11 - if (!authz || !authz.startsWith('Bearer ')) { 12 - throw new XRPCError('this endpoint requires authentication', 'authz_required', 401); 13 - } 14 - 15 - const serviceJwt = await verifyJwt( 16 - authz.split(' ')[1]!, 17 - 'did:web:recipes.blue#api', 18 - null, 19 - async (iss, forceRefresh) => { 20 - console.log(iss); 21 - return ''; 22 - }, 23 - ); 24 - 25 - //const agent = await getSessionAgent(ctx); 26 - //if (!agent) { 27 - // ctx.status(401); 28 - // return ctx.json({ 29 - // error: 'unauthenticated', 30 - // message: 'You must be authenticated to access this resource.', 31 - // }); 32 - //} 33 - // 34 - //const body = await ctx.req.json(); 35 - //const { data: record, success, error } = RecipeRecord.safeParse(body); 36 - //if (!success) { 37 - // ctx.status(400); 38 - // return ctx.json({ 39 - // error: 'invalid_recipe', 40 - // message: error.message, 41 - // fields: error.formErrors, 42 - // }); 43 - //} 44 - // 45 - //const res = await agent.com.atproto.repo.putRecord({ 46 - // repo: agent.assertDid, 47 - // collection: RecipeCollection, 48 - // record: record, 49 - // rkey: TID.nextStr(), 50 - // validate: false, 51 - //}); 52 - // 53 - //return ctx.json(res.data); 54 - }); 5 + recipeApp.post('/', async ctx => {});
+2
apps/web/package.json
··· 12 12 "dependencies": { 13 13 "@atcute/client": "^2.0.6", 14 14 "@atcute/oauth-browser-client": "^1.0.7", 15 + "@atproto/common": "^0.4.5", 16 + "@atproto/common-web": "^0.3.1", 15 17 "@dnd-kit/core": "^6.3.1", 16 18 "@dnd-kit/modifiers": "^9.0.0", 17 19 "@dnd-kit/sortable": "^10.0.0",
+2
apps/web/src/components/app-sidebar.tsx
··· 17 17 SidebarMenuItem, 18 18 SidebarRail, 19 19 } from "@/components/ui/sidebar" 20 + import { NavUserOpts } from "./nav-user-opts" 20 21 21 22 const data = { 22 23 navMain: [ ··· 76 77 </SidebarHeader> 77 78 <SidebarContent> 78 79 <NavMain items={data.navMain} /> 80 + <NavUserOpts /> 79 81 </SidebarContent> 80 82 <SidebarFooter> 81 83 <NavUser />
+67
apps/web/src/components/nav-user-opts.tsx
··· 1 + "use client" 2 + 3 + import { 4 + SidebarGroup, 5 + SidebarGroupContent, 6 + SidebarMenu, 7 + SidebarMenuButton, 8 + SidebarMenuItem, 9 + } from "@/components/ui/sidebar" 10 + import { useAuth } from "@/state/auth" 11 + import { Link } from "@tanstack/react-router"; 12 + import { LifeBuoy, Pencil, Send } from "lucide-react"; 13 + 14 + export function NavUserOpts() { 15 + const { isLoggedIn } = useAuth(); 16 + 17 + if (!isLoggedIn) { 18 + return ( 19 + <SidebarGroup className="mt-auto"> 20 + <SidebarGroupContent> 21 + <SidebarMenu> 22 + <AlwaysItems /> 23 + </SidebarMenu> 24 + </SidebarGroupContent> 25 + </SidebarGroup> 26 + ); 27 + } 28 + 29 + return ( 30 + <SidebarGroup className="mt-auto"> 31 + <SidebarGroupContent> 32 + <SidebarMenu> 33 + <AlwaysItems /> 34 + <SidebarMenuItem> 35 + <SidebarMenuButton asChild size="sm"> 36 + <Link to="/recipes/new"> 37 + <Pencil /> 38 + <span>New recipe</span> 39 + </Link> 40 + </SidebarMenuButton> 41 + </SidebarMenuItem> 42 + </SidebarMenu> 43 + </SidebarGroupContent> 44 + </SidebarGroup> 45 + ) 46 + } 47 + 48 + const AlwaysItems = () => ( 49 + <> 50 + <SidebarMenuItem> 51 + <SidebarMenuButton asChild size="sm"> 52 + <a href="https://github.com/recipes-blue/recipes.blue/issues/new"> 53 + <LifeBuoy /> 54 + <span>Support</span> 55 + </a> 56 + </SidebarMenuButton> 57 + </SidebarMenuItem> 58 + <SidebarMenuItem> 59 + <SidebarMenuButton asChild size="sm"> 60 + <a href="https://github.com/recipes-blue/recipes.blue/discussions"> 61 + <Send /> 62 + <span>Feedback</span> 63 + </a> 64 + </SidebarMenuButton> 65 + </SidebarMenuItem> 66 + </> 67 + );
+1 -8
apps/web/src/components/nav-user.tsx
··· 100 100 </div> 101 101 </DropdownMenuLabel> 102 102 <DropdownMenuSeparator /> 103 - <DropdownMenuGroup> 104 - <DropdownMenuItem> 105 - <BadgeCheck /> 106 - Account 107 - </DropdownMenuItem> 108 - </DropdownMenuGroup> 109 - <DropdownMenuSeparator /> 110 - <DropdownMenuItem> 103 + <DropdownMenuItem className="cursor-pointer" onClick={() => agent.signOut()}> 111 104 <LogOut /> 112 105 Log out 113 106 </DropdownMenuItem>
+22 -7
apps/web/src/queries/recipe.ts
··· 1 1 import { useXrpc } from "@/hooks/use-xrpc"; 2 - import { SERVER_URL } from "@/lib/utils"; 2 + import { useAuth } from "@/state/auth"; 3 3 import { XRPC, XRPCError } from "@atcute/client"; 4 - import { Recipe } from "@cookware/lexicons"; 4 + import { Recipe, RecipeCollection } from "@cookware/lexicons"; 5 5 import { queryOptions, useMutation, useQuery } from "@tanstack/react-query"; 6 - import { notFound } from "@tanstack/react-router"; 7 - import axios from "axios"; 6 + import { notFound, useLocation, useRouter } from "@tanstack/react-router"; 8 7 import { UseFormReturn } from "react-hook-form"; 8 + import { TID } from '@atproto/common-web'; 9 9 10 10 const RQKEY_ROOT = 'posts'; 11 11 export const RQKEY = (cursor: string, did: string, rkey: string) => [RQKEY_ROOT, cursor, did, rkey]; ··· 48 48 }; 49 49 50 50 export const useNewRecipeMutation = (form: UseFormReturn<Recipe>) => { 51 + const { agent } = useAuth(); 52 + const rpc = useXrpc(); 51 53 return useMutation({ 52 54 mutationKey: ['recipes.new'], 53 55 mutationFn: async ({ recipe }: { recipe: Recipe }) => { 54 - const res = await axios.post(`https://${SERVER_URL}/api/recipes`, recipe); 55 - return res.data; 56 + const rkey = TID.nextStr(); 57 + const res = await rpc.call(`com.atproto.repo.createRecord`, { 58 + data: { 59 + repo: agent?.session.info.sub as `did:${string}`, 60 + record: recipe, 61 + collection: RecipeCollection, 62 + rkey: rkey, 63 + }, 64 + }); 65 + return { 66 + rkey: rkey, 67 + resp: res.data 68 + }; 56 69 }, 57 70 onError: (error) => { 58 - console.error(error); 59 71 form.setError('title', error); 72 + }, 73 + onSuccess: ({ rkey }) => { 74 + window.location.assign(`/recipes/${agent?.sub}/${rkey}`); 60 75 }, 61 76 }); 62 77 };
+10 -4
pnpm-lock.yaml
··· 52 52 version: 4.0.8 53 53 drizzle-orm: 54 54 specifier: ^0.37.0 55 - version: 0.37.0(@libsql/client@0.14.0)(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(@types/react@19.0.1)(react@19.0.0) 55 + version: 0.37.0(@libsql/client@0.14.0(bufferutil@4.0.8))(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(@types/react@19.0.1)(react@19.0.0) 56 56 hono: 57 57 specifier: ^4.6.12 58 58 version: 4.6.12 ··· 131 131 version: 4.0.8 132 132 drizzle-orm: 133 133 specifier: ^0.37.0 134 - version: 0.37.0(@libsql/client@0.14.0)(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(@types/react@19.0.1)(react@19.0.0) 134 + version: 0.37.0(@libsql/client@0.14.0(bufferutil@4.0.8))(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(@types/react@19.0.1)(react@19.0.0) 135 135 pino: 136 136 specifier: ^9.5.0 137 137 version: 9.5.0 ··· 181 181 '@atcute/oauth-browser-client': 182 182 specifier: ^1.0.7 183 183 version: 1.0.7 184 + '@atproto/common': 185 + specifier: ^0.4.5 186 + version: 0.4.5 187 + '@atproto/common-web': 188 + specifier: ^0.3.1 189 + version: 0.3.1 184 190 '@dnd-kit/core': 185 191 specifier: ^6.3.1 186 192 version: 6.3.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0) ··· 334 340 version: 0.14.0(bufferutil@4.0.8) 335 341 drizzle-orm: 336 342 specifier: ^0.37.0 337 - version: 0.37.0(@libsql/client@0.14.0)(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(@types/react@19.0.1)(react@19.0.0) 343 + version: 0.37.0(@libsql/client@0.14.0(bufferutil@4.0.8))(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(@types/react@19.0.1)(react@19.0.0) 338 344 zod: 339 345 specifier: ^3.23.8 340 346 version: 3.23.8 ··· 6665 6671 transitivePeerDependencies: 6666 6672 - supports-color 6667 6673 6668 - drizzle-orm@0.37.0(@libsql/client@0.14.0)(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(@types/react@19.0.1)(react@19.0.0): 6674 + drizzle-orm@0.37.0(@libsql/client@0.14.0(bufferutil@4.0.8))(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(@types/react@19.0.1)(react@19.0.0): 6669 6675 optionalDependencies: 6670 6676 '@libsql/client': 0.14.0(bufferutil@4.0.8) 6671 6677 '@opentelemetry/api': 1.9.0