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: start using query options for pages

+86 -49
+3 -2
apps/web/src/components/nav-main.tsx
··· 17 17 SidebarMenuSubButton, 18 18 SidebarMenuSubItem, 19 19 } from "@/components/ui/sidebar" 20 + import { Link } from "@tanstack/react-router" 20 21 21 22 export function NavMain({ 22 23 items, ··· 57 58 {item.items?.map((subItem) => ( 58 59 <SidebarMenuSubItem key={subItem.title}> 59 60 <SidebarMenuSubButton asChild isActive={subItem.isActive}> 60 - <a href={subItem.url}> 61 + <Link to={subItem.url}> 61 62 <span>{subItem.title}</span> 62 - </a> 63 + </Link> 63 64 </SidebarMenuSubButton> 64 65 </SidebarMenuSubItem> 65 66 ))}
+1 -1
apps/web/src/components/nav-user.tsx
··· 62 62 className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground" 63 63 > 64 64 <Button asChild> 65 - <Link href="/login" className="w-full">Log in</Link> 65 + <Link to="/login" className="w-full">Log in</Link> 66 66 </Button> 67 67 </SidebarMenuButton> 68 68 </SidebarMenuItem>
+3
apps/web/src/hooks/use-xrpc.tsx
··· 1 + import { SERVER_URL } from "@/lib/utils"; 1 2 import { CredentialManager, XRPC } from "@atcute/client" 2 3 import { createContext, useContext } from "react"; 3 4 5 + export const creds = new CredentialManager({ service: `https://${SERVER_URL}` }); 6 + export const rpc = new XRPC({ handler: creds }); 4 7 export const XrpcContext = createContext<{ rpc: XRPC; creds: CredentialManager; } | null>(null); 5 8 6 9 export function useXrpc() {
+11
apps/web/src/lib/react-query.ts
··· 1 + import { QueryClient } from "@tanstack/react-query"; 2 + 3 + export const queryClient = new QueryClient({ 4 + defaultOptions: { 5 + queries: { 6 + refetchOnWindowFocus: false, 7 + structuralSharing: false, 8 + retry: false, 9 + } 10 + } 11 + });
+1 -1
apps/web/src/lib/utils.ts
··· 5 5 return twMerge(clsx(inputs)) 6 6 } 7 7 8 - export const SERVER_URL = 'cookware.fly.dev'; 8 + export const SERVER_URL = 'cookware.dev.hayden.moe' as const;
+1 -1
apps/web/src/main.tsx
··· 17 17 } 18 18 } 19 19 20 - const creds = new CredentialManager({ service: `https://${SERVER_URL}/api/` }); 20 + const creds = new CredentialManager({ service: `https://${SERVER_URL}` }); 21 21 const rpc = new XRPC({ handler: creds }); 22 22 const queryClient = new QueryClient({ 23 23 defaultOptions: {
+9 -4
apps/web/src/queries/recipe.ts
··· 1 1 import { useXrpc } from "@/hooks/use-xrpc"; 2 - import { useQuery } from "@tanstack/react-query"; 2 + import { XRPC } from "@atcute/client"; 3 + import { queryOptions, useQuery } from "@tanstack/react-query"; 3 4 4 5 const RQKEY_ROOT = 'posts'; 5 6 export const RQKEY = (cursor: string, did: string, rkey: string) => [RQKEY_ROOT, cursor, did, rkey]; ··· 17 18 }); 18 19 }; 19 20 20 - export const useRecipeQuery = (did: string, rkey: string) => { 21 - const { rpc } = useXrpc(); 22 - return useQuery({ 21 + export const recipeQueryOptions = (rpc: XRPC, did: string, rkey: string) => { 22 + return queryOptions({ 23 23 queryKey: RQKEY('', did, rkey), 24 24 queryFn: async () => { 25 25 const res = await rpc.get('moe.hayden.cookware.getRecipe', { ··· 29 29 }, 30 30 }); 31 31 }; 32 + 33 + export const useRecipeQuery = (did: string, rkey: string) => { 34 + const { rpc } = useXrpc(); 35 + return useQuery(recipeQueryOptions(rpc, did, rkey)); 36 + };
+11 -3
apps/web/src/queries/self.ts
··· 1 1 import { AppBskyActorGetProfile } from "@atcute/client/lexicons"; 2 2 import { useQuery } from "@tanstack/react-query"; 3 - import axios from "axios"; 3 + import axios, { AxiosError } from "axios"; 4 4 5 5 export const useUserQuery = () => { 6 6 return useQuery({ 7 7 queryKey: ['self'], 8 8 queryFn: async () => { 9 - const res = await axios.get<AppBskyActorGetProfile.Output>('/oauth/me'); 10 - return res.data; 9 + try { 10 + const res = await axios.get<AppBskyActorGetProfile.Output>('/oauth/me'); 11 + return res.data; 12 + } catch(err) { 13 + if (err instanceof AxiosError && err.status == 401) { 14 + // If we get a 401, we're just unauthenticated. 15 + return null; 16 + } 17 + throw err; 18 + } 11 19 }, 12 20 }); 13 21 }
+43 -37
apps/web/src/routes/(app)/recipes.$did.$rkey.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 QueryPlaceholder from '@/components/query-placeholder' 13 12 import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' 14 - import { useRecipeQuery } from '@/queries/recipe' 13 + import { recipeQueryOptions } from '@/queries/recipe' 14 + import { queryClient } from '@/lib/react-query' 15 + import { useSuspenseQuery } from '@tanstack/react-query' 16 + import { rpc } from '@/hooks/use-xrpc' 15 17 16 18 export const Route = createFileRoute('/(app)/recipes/$did/$rkey')({ 19 + loader: ({ params: { did, rkey }, }) => { 20 + queryClient.ensureQueryData(recipeQueryOptions(rpc, did, rkey)); 21 + }, 22 + 17 23 component: RouteComponent, 18 24 }) 19 25 20 26 function RouteComponent() { 21 27 const { did, rkey } = Route.useParams() 22 - const query = useRecipeQuery(did, rkey); 28 + const { 29 + data: { recipe }, 30 + } = useSuspenseQuery(recipeQueryOptions(rpc, did, rkey)); 23 31 24 32 return ( 25 33 <> ··· 38 46 </BreadcrumbItem> 39 47 <BreadcrumbSeparator className="hidden md:block" /> 40 48 <BreadcrumbItem className="hidden md:block"> 41 - <BreadcrumbLink asChild><Link href={`/profiles/${did}`}>{query.data ? query.data.recipe.author.handle : did}</Link></BreadcrumbLink> 49 + <BreadcrumbLink asChild><Link href={`/profiles/${did}`}>{recipe.author.handle}</Link></BreadcrumbLink> 42 50 </BreadcrumbItem> 43 51 <BreadcrumbSeparator className="hidden md:block" /> 44 52 <BreadcrumbItem> 45 - <BreadcrumbPage>{query.data ? query.data.recipe.title : rkey}</BreadcrumbPage> 53 + <BreadcrumbPage>{recipe.title}</BreadcrumbPage> 46 54 </BreadcrumbItem> 47 55 </BreadcrumbList> 48 56 </Breadcrumb> 49 57 </div> 50 58 </header> 51 59 <div className="flex flex-1 flex-col gap-4 p-4 pt-0"> 52 - <QueryPlaceholder query={query}> 53 - <div className="max-w-6xl"> 54 - <h1 className="scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl">{query.data?.recipe.title}</h1> 55 - <p className="leading-7 [&:not(:first-child)]:mt-6">{query.data?.recipe.description}</p> 56 - </div> 60 + <div className="max-w-6xl"> 61 + <h1 className="scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl">{recipe.title}</h1> 62 + <p className="leading-7 [&:not(:first-child)]:mt-6">{recipe.description}</p> 63 + </div> 57 64 58 - <div className="grid lg:grid-cols-3 gap-4"> 59 - <Card className="lg:col-start-3"> 60 - <CardHeader> 61 - <CardTitle>Ingredients</CardTitle> 62 - </CardHeader> 63 - <CardContent> 64 - <ul> 65 - {query.data?.recipe.ingredients.map((ing, idx) => ( 66 - <li key={idx}>{ing.name} ({ing.amount} {ing.unit})</li> 67 - ))} 68 - </ul> 69 - </CardContent> 70 - </Card> 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 + {recipe.ingredients.map((ing, idx) => ( 73 + <li key={idx}>{ing.name} ({ing.amount} {ing.unit})</li> 74 + ))} 75 + </ul> 76 + </CardContent> 77 + </Card> 71 78 72 - <Card className="lg:col-start-1 lg:row-start-1 lg:col-span-2"> 73 - <CardHeader> 74 - <CardTitle>Steps</CardTitle> 75 - </CardHeader> 76 - <CardContent> 77 - <ol> 78 - {query.data?.recipe.steps.map((ing, idx) => ( 79 - <li key={idx}>{ing.text}</li> 80 - ))} 81 - </ol> 82 - </CardContent> 83 - </Card> 84 - </div> 85 - </QueryPlaceholder> 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 + {recipe.steps.map((ing, idx) => ( 86 + <li key={idx}>{ing.text}</li> 87 + ))} 88 + </ol> 89 + </CardContent> 90 + </Card> 91 + </div> 86 92 </div> 87 93 </> 88 94 )
+3
apps/web/src/routes/__root.tsx
··· 4 4 SidebarProvider, 5 5 } from '@/components/ui/sidebar' 6 6 import { Outlet, createRootRoute } from '@tanstack/react-router' 7 + import { Suspense } from 'react' 7 8 8 9 export const Route = createRootRoute({ 9 10 component: RootComponent, ··· 14 15 <SidebarProvider> 15 16 <AppSidebar /> 16 17 <SidebarInset> 18 + <Suspense fallback={<p>Loading...</p>}> 17 19 <Outlet /> 20 + </Suspense> 18 21 </SidebarInset> 19 22 </SidebarProvider> 20 23 )