The recipes.blue monorepo recipes.blue
recipes appview atproto

Compare changes

Choose any two refs to compare.

+1 -2
.env.example
··· 1 1 # Database 2 - DATABASE_URL=http://localhost:4001 3 - DATABASE_AUTH_TOKEN= 2 + DATABASE_URL=postgres://postgres:postgres@localhost:5432/postgres 4 3 5 4 # API 6 5 PORT=3000
+3 -1
apps/api/package.json
··· 4 4 "private": true, 5 5 "scripts": { 6 6 "build": "bun --bun run check-types && bun --bun run compile", 7 - "dev": "bun run --hot src/index.ts", 7 + "dev": "bun run --hot src/index.ts | pino-pretty", 8 8 "check-types": "tsc --noEmit", 9 9 "compile": "bun build src/index.ts --compile --minify --sourcemap --outfile=dist/api --target=bun", 10 10 "clean": "rimraf dist" ··· 20 20 "@cookware/lexicons": "workspace:*", 21 21 "@libsql/client": "^0.14.0", 22 22 "drizzle-orm": "catalog:", 23 + "hono": "^4.10.7", 23 24 "pino": "^9.5.0" 24 25 }, 25 26 "devDependencies": { ··· 27 28 "@cookware/tsconfig": "workspace:*", 28 29 "@types/bun": "catalog:", 29 30 "drizzle-kit": "^0.29.0", 31 + "pino-pretty": "^13.1.2", 30 32 "rimraf": "^6.0.1" 31 33 } 32 34 }
+19 -5
apps/api/src/index.ts
··· 5 5 import { logMiddleware } from './logger.js'; 6 6 import pino from 'pino'; 7 7 import { RedisClient } from 'bun'; 8 + import { registerGetProfile } from './xrpc/blue.recipes.actor.getProfile.js'; 9 + import { Hono } from 'hono'; 10 + import { mountXrpcRouter } from './util/hono.js'; 8 11 9 12 const logger = pino(); 10 13 const redis = new RedisClient(Bun.env.REDIS_URL ?? "redis://127.0.0.1:6379/0"); 11 14 12 - const router = new XRPCRouter({ 15 + const xrpcRouter = new XRPCRouter({ 13 16 handleException: (err, _req) => { 14 17 if (err instanceof XRPCError) { 15 18 return err.toResponse(); ··· 32 35 ], 33 36 }); 34 37 35 - registerGetRecipes(router, logger, redis); 36 - registerGetRecipe(router, logger, redis); 38 + // actor 39 + registerGetProfile(xrpcRouter, logger, redis); 40 + 41 + // feed 42 + registerGetRecipes(xrpcRouter, logger, redis); 43 + registerGetRecipe(xrpcRouter, logger, redis); 44 + 45 + const app = new Hono(); 46 + 47 + // mount xrpc router at /xrpc 48 + const xrpcApp = new Hono(); 49 + mountXrpcRouter(xrpcApp, xrpcRouter); 50 + app.route('/xrpc', xrpcApp); 37 51 38 52 const server = Bun.serve({ 39 53 port: process.env.PORT || 3000, 40 - ...router 54 + fetch: app.fetch, 41 55 }); 42 56 43 - console.log(`Server running on http://localhost:${server.port}`); 57 + logger.info({ url: server.url.toString() }, `Recipes.blue API started up`);
+18
apps/api/src/util/api.ts
··· 4 4 import { ActorIdentifier, AtprotoDid, Handle, isHandle } from '@atcute/lexicons/syntax'; 5 5 import { isAtprotoDid } from '@atcute/identity'; 6 6 import { RedisClient } from 'bun'; 7 + import { ProfileViewBasic } from '../../../../libs/lexicons/dist/types/blue/recipes/actor/defs.js'; 8 + import { Blob, LegacyBlob } from '@atcute/lexicons'; 9 + import { buildCdnUrl } from './cdn.js'; 7 10 8 11 const handleResolver = new CompositeHandleResolver({ 9 12 strategy: 'race', ··· 43 46 44 47 return handle; 45 48 } 49 + 50 + export const buildProfileViewBasic = async (author: { 51 + did: AtprotoDid; 52 + displayName: string; 53 + pronouns: string | null; 54 + avatarRef: Blob | LegacyBlob | null; 55 + createdAt: Date; 56 + }, redis: RedisClient): Promise<ProfileViewBasic> => ({ 57 + did: author.did, 58 + handle: await getHandle(author.did, redis), 59 + displayName: author.displayName, 60 + pronouns: author.pronouns ?? undefined, 61 + avatar: author.avatarRef ? buildCdnUrl('avatar', author.did, author.avatarRef) : undefined, 62 + createdAt: author.createdAt.toISOString(), 63 + });
+21
apps/api/src/util/cdn.ts
··· 1 + import { type Blob, type LegacyBlob } from "@atcute/lexicons"; 2 + import { isBlob, isLegacyBlob } from "@atcute/lexicons/interfaces"; 3 + 4 + const CDN_ROOT = "https://cdn.bsky.app/img/"; 5 + 6 + export const buildCdnUrl = ( 7 + type: 'feed_thumbnail' | 'post_image' | 'avatar', 8 + did: string, 9 + blob: Blob | LegacyBlob, 10 + ): string => { 11 + let ref: string; 12 + if (isLegacyBlob(blob)) { 13 + ref = blob.cid; 14 + } else if (isBlob(blob)) { 15 + ref = blob.ref.$link; 16 + } else { 17 + throw new Error("Invalid blob type"); 18 + } 19 + 20 + return `${CDN_ROOT}${type}/plain/${did}/${ref}`; 21 + }
+40
apps/api/src/util/hono.ts
··· 1 + import { XRPCRouter } from '@atcute/xrpc-server'; 2 + import type { Context, Hono } from 'hono'; 3 + 4 + export type ApiContext = {}; 5 + 6 + /** 7 + * mounts an @atcute/xrpc-server router into hono as a nested route 8 + * 9 + * basically just bridges the two request handlers since both are 10 + * web standard Request/Response. you can optionally pass hono context 11 + * properties to xrpc handlers via the request object 12 + */ 13 + export const mountXrpcRouter = ( 14 + app: Hono, 15 + router: XRPCRouter, 16 + injectContext?: (c: Context) => ApiContext, 17 + ) => { 18 + app.all('*', async (c) => { 19 + let request = c.req.raw; 20 + 21 + // if context injector provided, attach properties to request 22 + if (injectContext) { 23 + const contextData = injectContext(c); 24 + request = Object.assign(request, contextData); 25 + } 26 + 27 + const response = await router.fetch(request); 28 + return response; 29 + }); 30 + }; 31 + 32 + /** 33 + * helper to extract injected context from xrpc request 34 + * use this in your xrpc handlers to access hono context data 35 + */ 36 + export const getInjectedContext = ( 37 + request: Request 38 + ): ApiContext => { 39 + return request as any as ApiContext; 40 + };
+44
apps/api/src/xrpc/blue.recipes.actor.getProfile.ts
··· 1 + import { json, XRPCRouter, XRPCError } from '@atcute/xrpc-server'; 2 + import { BlueRecipesActorGetProfile } from '@cookware/lexicons'; 3 + import { db, eq } from '@cookware/database'; 4 + import { getHandle, parseDid } from '../util/api.js'; 5 + import { Logger } from 'pino'; 6 + import { profilesTable, recipeTable } from '@cookware/database/schema'; 7 + import { RedisClient } from 'bun'; 8 + import { buildCdnUrl } from '../util/cdn.js'; 9 + 10 + export const registerGetProfile = (router: XRPCRouter, _logger: Logger, redis: RedisClient) => { 11 + router.addQuery(BlueRecipesActorGetProfile.mainSchema, { 12 + async handler({ params: { actor } }) { 13 + const where = eq(profilesTable.did, await parseDid(actor)); 14 + const profile = await db.query.profilesTable.findFirst({ 15 + where, 16 + orderBy: profilesTable.createdAt, 17 + extras: { 18 + recipesCount: db.$count(recipeTable, where).as('recipesCount'), 19 + } 20 + }); 21 + 22 + if (!profile) { 23 + throw new XRPCError({ 24 + status: 404, 25 + error: 'ProfileNotFound', 26 + description: `Profile for actor ${actor} not found.`, 27 + }); 28 + } 29 + 30 + return json({ 31 + did: profile.did, 32 + handle: await getHandle(profile.did, redis), 33 + displayName: profile.displayName ?? undefined, 34 + description: profile.description ?? undefined, 35 + pronouns: profile.pronouns ?? undefined, 36 + website: profile.website ?? undefined, 37 + avatar: profile.avatarRef ? buildCdnUrl('avatar', profile.did, profile.avatarRef) : undefined, 38 + banner: profile.bannerRef ? buildCdnUrl('feed_thumbnail', profile.did, profile.bannerRef) : undefined, 39 + recipesCount: profile.recipesCount, 40 + createdAt: profile.createdAt.toISOString(), 41 + }); 42 + }, 43 + }); 44 + };
+36 -25
apps/api/src/xrpc/blue.recipes.feed.getRecipe.ts
··· 1 1 import { json, XRPCRouter, XRPCError } from '@atcute/xrpc-server'; 2 - import { BlueRecipesFeedGetRecipe, BlueRecipesFeedRecipe, BlueRecipesActorDefs } from '@cookware/lexicons'; 2 + import { BlueRecipesFeedDefs, BlueRecipesFeedGetRecipe, BlueRecipesFeedRecipe } from '@cookware/lexicons'; 3 3 import { db, and, or, eq } from '@cookware/database'; 4 - import { parseDid } from '../util/api.js'; 4 + import { buildProfileViewBasic, parseDid } from '../util/api.js'; 5 5 import { Logger } from 'pino'; 6 6 import { parseResourceUri, ResourceUri } from '@atcute/lexicons'; 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, ··· 14 15 description: `The provided URI is invalid: ${uri}`, 15 16 }); 16 17 17 - export const registerGetRecipe = (router: XRPCRouter, _logger: Logger, _redis: RedisClient) => { 18 + export const registerGetRecipe = (router: XRPCRouter, _logger: Logger, redis: RedisClient) => { 18 19 router.addQuery(BlueRecipesFeedGetRecipe.mainSchema, { 19 20 async handler({ params: { uris } }) { 20 21 const whereClauses = []; ··· 36 37 const recipes = await db.query.recipeTable.findMany({ 37 38 orderBy: recipeTable.createdAt, 38 39 where: or(...whereClauses), 40 + with: { 41 + author: { 42 + columns: { 43 + did: true, 44 + displayName: true, 45 + pronouns: true, 46 + avatarRef: true, 47 + createdAt: true, 48 + }, 49 + }, 50 + }, 39 51 }); 40 52 41 53 return json({ 42 - recipes: recipes.map((recipe) => ({ 43 - author: { 44 - $type: BlueRecipesActorDefs.profileViewBasicSchema.shape.$type.wrapped.expected, 45 - did: recipe.did, 46 - handle: 'hayden.moe', 47 - createdAt: new Date().toISOString(), 48 - }, 49 - cid: '', 50 - indexedAt: new Date().toISOString(), 51 - record: { 52 - $type: BlueRecipesFeedRecipe.mainSchema.object.shape.$type.expected, 53 - title: recipe.title, 54 - description: recipe.description ?? undefined, 55 - time: recipe.time ?? undefined, 56 - serves: recipe.serves ?? undefined, 57 - ingredients: recipe.ingredients as BlueRecipesFeedRecipe.Ingredient[], 58 - steps: recipe.steps as BlueRecipesFeedRecipe.Step[], 59 - image: isLegacyBlob(recipe.imageRef) ? undefined : recipe.imageRef ?? undefined, 60 - createdAt: recipe.createdAt.toISOString(), 61 - }, 62 - uri: recipe.uri as ResourceUri, 63 - })), 54 + recipes: await Promise.all( 55 + recipes.map(async ({ author, ...recipe }) => ({ 56 + uri: recipe.uri as ResourceUri, 57 + author: await buildProfileViewBasic(author, redis), 58 + cid: recipe.cid, 59 + rkey: recipe.rkey, 60 + imageUrl: recipe.imageRef ? buildCdnUrl('post_image', recipe.did, recipe.imageRef) : undefined, 61 + indexedAt: recipe.ingestedAt.toISOString(), 62 + record: { 63 + $type: BlueRecipesFeedRecipe.mainSchema.object.shape.$type.expected, 64 + title: recipe.title, 65 + description: recipe.description ?? undefined, 66 + time: recipe.time ?? undefined, 67 + serves: recipe.serves ?? undefined, 68 + ingredients: recipe.ingredients as BlueRecipesFeedRecipe.Ingredient[], 69 + steps: recipe.steps as BlueRecipesFeedRecipe.Step[], 70 + image: isLegacyBlob(recipe.imageRef) ? undefined : recipe.imageRef ?? undefined, 71 + createdAt: recipe.createdAt.toISOString(), 72 + }, 73 + })), 74 + ), 64 75 }); 65 76 }, 66 77 });
+13 -3
apps/api/src/xrpc/blue.recipes.feed.getRecipes.ts
··· 7 7 import { Logger } from 'pino'; 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 export const registerGetRecipes = (router: XRPCRouter, _logger: Logger, redis: RedisClient) => { 12 13 router.addQuery(BlueRecipesFeedGetRecipes.mainSchema, { ··· 27 28 limit: limit, 28 29 where: whereClauses ? and(...whereClauses) : undefined, 29 30 with: { 30 - author: true, 31 + author: { 32 + columns: { 33 + did: true, 34 + displayName: true, 35 + pronouns: true, 36 + avatarRef: true, 37 + createdAt: true, 38 + }, 39 + }, 31 40 }, 32 41 }); 33 42 ··· 44 53 did: recipe.author.did, 45 54 handle: await getHandle(recipe.author.did, redis), 46 55 displayName: recipe.author.displayName, 47 - avatar: isLegacyBlob(recipe.author.avatarRef) ? undefined : recipe.author.avatarRef ?? undefined, 56 + avatar: recipe.author.avatarRef ? buildCdnUrl('avatar', recipe.author.did, recipe.author.avatarRef) : undefined, 48 57 pronouns: recipe.author.pronouns ?? undefined, 49 58 createdAt: recipe.author.createdAt.toISOString(), 50 59 }, 51 - cid: '', 60 + cid: recipe.cid, 61 + rkey: recipe.rkey, 52 62 indexedAt: recipe.ingestedAt.toISOString(), 53 63 record: { 54 64 $type: BlueRecipesFeedRecipe.mainSchema.object.shape.$type.expected,
+8 -5
apps/web/package.json
··· 12 12 "dependencies": { 13 13 "@atcute/atproto": "^3.1.9", 14 14 "@atcute/client": "catalog:", 15 + "@atcute/identity-resolver": "^1.1.4", 15 16 "@atcute/lexicons": "catalog:", 16 - "@atcute/oauth-browser-client": "^1.0.7", 17 + "@atcute/oauth-browser-client": "^2.0.1", 17 18 "@atproto/common": "^0.4.5", 18 19 "@atproto/common-web": "^0.3.1", 19 20 "@dnd-kit/core": "^6.3.1", ··· 26 27 "@radix-ui/react-dialog": "^1.1.4", 27 28 "@radix-ui/react-dropdown-menu": "^2.1.4", 28 29 "@radix-ui/react-icons": "^1.3.2", 29 - "@radix-ui/react-label": "^2.1.0", 30 - "@radix-ui/react-separator": "^1.1.0", 31 - "@radix-ui/react-slot": "^1.1.0", 30 + "@radix-ui/react-label": "^2.1.8", 31 + "@radix-ui/react-separator": "^1.1.8", 32 + "@radix-ui/react-slot": "^1.2.4", 32 33 "@radix-ui/react-tooltip": "^1.1.4", 33 34 "@tanstack/react-query": "^5.62.2", 34 35 "@tanstack/react-query-devtools": "^5.62.2", ··· 53 54 "@types/node": "^22.10.1", 54 55 "@types/react": "^19.0.0", 55 56 "@types/react-dom": "^19.0.0", 57 + "@vitejs/plugin-react": "^5.1.1", 56 58 "@vitejs/plugin-react-swc": "^3.5.0", 57 59 "autoprefixer": "^10.4.20", 60 + "babel-plugin-react-compiler": "^1.0.0", 58 61 "cssnano": "^7.0.6", 59 62 "eslint": "^9.15.0", 60 63 "eslint-plugin-react-hooks": "^5.0.0", ··· 65 68 "tailwindcss": "^3.4.16", 66 69 "typescript": "~5.6.2", 67 70 "typescript-eslint": "^8.15.0", 68 - "vite": "^6.0.1" 71 + "vite": "^7.2.4" 69 72 } 70 73 }
-12
apps/web/public/client-metadata.json
··· 1 - { 2 - "client_id": "https://recipes.blue/client-metadata.json", 3 - "client_name": "Recipes", 4 - "client_uri": "https://recipes.blue", 5 - "redirect_uris": ["https://recipes.blue/"], 6 - "scope": "atproto transition:generic", 7 - "grant_types": ["authorization_code", "refresh_token"], 8 - "response_types": ["code"], 9 - "token_endpoint_auth_method": "none", 10 - "application_type": "web", 11 - "dpop_bound_access_tokens": true 12 - }
+9
apps/web/public/oauth-client-metadata.json
··· 1 + { 2 + "client_id": "https://recipes.blue/oauth-client-metadata.json", 3 + "client_name": "Recipes.blue", 4 + "redirect_uris": ["https://recipes.blue/"], 5 + "scope": "atproto transition:generic", 6 + "token_endpoint_auth_method": "none", 7 + "application_type": "web", 8 + "dpop_bound_access_tokens": true 9 + }
+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>
+10 -6
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 - import { XRPCError } from '@atcute/client'; 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 { ··· 32 33 } else if (query.isError) { 33 34 const { error } = query; 34 35 let errMsg = 'Unknown'; 35 - if (error instanceof XRPCError) { 36 - errMsg = error.kind ?? `HTTP_${error.status}`; 36 + if (isXRPCErrorPayload(error)) { 37 + errMsg = error.message ?? `XRPC_${error.error}`; 37 38 } if (error instanceof Error) { 38 39 errMsg = `${error.message} (${error.name})`; 39 40 } ··· 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>
+242
apps/web/src/components/ui/field.tsx
··· 1 + import { useMemo } from "react" 2 + import { cva, type VariantProps } from "class-variance-authority" 3 + 4 + import { cn } from "@/lib/utils" 5 + import { Label } from "@/components/ui/label" 6 + import { Separator } from "@/components/ui/separator" 7 + 8 + function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) { 9 + return ( 10 + <fieldset 11 + data-slot="field-set" 12 + className={cn( 13 + "flex flex-col gap-6", 14 + "has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3", 15 + className 16 + )} 17 + {...props} 18 + /> 19 + ) 20 + } 21 + 22 + function FieldLegend({ 23 + className, 24 + variant = "legend", 25 + ...props 26 + }: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) { 27 + return ( 28 + <legend 29 + data-slot="field-legend" 30 + data-variant={variant} 31 + className={cn( 32 + "mb-3 font-medium", 33 + "data-[variant=legend]:text-base", 34 + "data-[variant=label]:text-sm", 35 + className 36 + )} 37 + {...props} 38 + /> 39 + ) 40 + } 41 + 42 + function FieldGroup({ className, ...props }: React.ComponentProps<"div">) { 43 + return ( 44 + <div 45 + data-slot="field-group" 46 + className={cn( 47 + "group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4", 48 + className 49 + )} 50 + {...props} 51 + /> 52 + ) 53 + } 54 + 55 + const fieldVariants = cva( 56 + "group/field data-[invalid=true]:text-destructive flex w-full gap-3", 57 + { 58 + variants: { 59 + orientation: { 60 + vertical: ["flex-col [&>*]:w-full [&>.sr-only]:w-auto"], 61 + horizontal: [ 62 + "flex-row items-center", 63 + "[&>[data-slot=field-label]]:flex-auto", 64 + "has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px has-[>[data-slot=field-content]]:items-start", 65 + ], 66 + responsive: [ 67 + "@md/field-group:flex-row @md/field-group:items-center @md/field-group:[&>*]:w-auto flex-col [&>*]:w-full [&>.sr-only]:w-auto", 68 + "@md/field-group:[&>[data-slot=field-label]]:flex-auto", 69 + "@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px", 70 + ], 71 + }, 72 + }, 73 + defaultVariants: { 74 + orientation: "vertical", 75 + }, 76 + } 77 + ) 78 + 79 + function Field({ 80 + className, 81 + orientation = "vertical", 82 + ...props 83 + }: React.ComponentProps<"div"> & VariantProps<typeof fieldVariants>) { 84 + return ( 85 + <div 86 + role="group" 87 + data-slot="field" 88 + data-orientation={orientation} 89 + className={cn(fieldVariants({ orientation }), className)} 90 + {...props} 91 + /> 92 + ) 93 + } 94 + 95 + function FieldContent({ className, ...props }: React.ComponentProps<"div">) { 96 + return ( 97 + <div 98 + data-slot="field-content" 99 + className={cn( 100 + "group/field-content flex flex-1 flex-col gap-1.5 leading-snug", 101 + className 102 + )} 103 + {...props} 104 + /> 105 + ) 106 + } 107 + 108 + function FieldLabel({ 109 + className, 110 + ...props 111 + }: React.ComponentProps<typeof Label>) { 112 + return ( 113 + <Label 114 + data-slot="field-label" 115 + className={cn( 116 + "group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50", 117 + "has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>[data-slot=field]]:p-4", 118 + "has-data-[state=checked]:bg-primary/5 has-data-[state=checked]:border-primary dark:has-data-[state=checked]:bg-primary/10", 119 + className 120 + )} 121 + {...props} 122 + /> 123 + ) 124 + } 125 + 126 + function FieldTitle({ className, ...props }: React.ComponentProps<"div">) { 127 + return ( 128 + <div 129 + data-slot="field-label" 130 + className={cn( 131 + "flex w-fit items-center gap-2 text-sm font-medium leading-snug group-data-[disabled=true]/field:opacity-50", 132 + className 133 + )} 134 + {...props} 135 + /> 136 + ) 137 + } 138 + 139 + function FieldDescription({ className, ...props }: React.ComponentProps<"p">) { 140 + return ( 141 + <p 142 + data-slot="field-description" 143 + className={cn( 144 + "text-muted-foreground text-sm font-normal leading-normal group-has-[[data-orientation=horizontal]]/field:text-balance", 145 + "nth-last-2:-mt-1 last:mt-0 [[data-variant=legend]+&]:-mt-1.5", 146 + "[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4", 147 + className 148 + )} 149 + {...props} 150 + /> 151 + ) 152 + } 153 + 154 + function FieldSeparator({ 155 + children, 156 + className, 157 + ...props 158 + }: React.ComponentProps<"div"> & { 159 + children?: React.ReactNode 160 + }) { 161 + return ( 162 + <div 163 + data-slot="field-separator" 164 + data-content={!!children} 165 + className={cn( 166 + "relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2", 167 + className 168 + )} 169 + {...props} 170 + > 171 + <Separator className="absolute inset-0 top-1/2" /> 172 + {children && ( 173 + <span 174 + className="bg-background text-muted-foreground relative mx-auto block w-fit px-2" 175 + data-slot="field-separator-content" 176 + > 177 + {children} 178 + </span> 179 + )} 180 + </div> 181 + ) 182 + } 183 + 184 + function FieldError({ 185 + className, 186 + children, 187 + errors, 188 + ...props 189 + }: React.ComponentProps<"div"> & { 190 + errors?: Array<{ message?: string } | undefined> 191 + }) { 192 + const content = useMemo(() => { 193 + if (children) { 194 + return children 195 + } 196 + 197 + if (!errors) { 198 + return null 199 + } 200 + 201 + if (errors?.length === 1 && errors[0]?.message) { 202 + return errors[0].message 203 + } 204 + 205 + return ( 206 + <ul className="ml-4 flex list-disc flex-col gap-1"> 207 + {errors.map( 208 + (error, index) => 209 + error?.message && <li key={index}>{error.message}</li> 210 + )} 211 + </ul> 212 + ) 213 + }, [children, errors]) 214 + 215 + if (!content) { 216 + return null 217 + } 218 + 219 + return ( 220 + <div 221 + role="alert" 222 + data-slot="field-error" 223 + className={cn("text-destructive text-sm font-normal", className)} 224 + {...props} 225 + > 226 + {content} 227 + </div> 228 + ) 229 + } 230 + 231 + export { 232 + Field, 233 + FieldLabel, 234 + FieldDescription, 235 + FieldError, 236 + FieldGroup, 237 + FieldLegend, 238 + FieldSeparator, 239 + FieldSet, 240 + FieldContent, 241 + FieldTitle, 242 + }
+2
apps/web/src/components/ui/separator.tsx
··· 1 + "use client" 2 + 1 3 import * as React from "react" 2 4 import * as SeparatorPrimitive from "@radix-ui/react-separator" 3 5
-9
apps/web/src/forms/recipe.ts
··· 1 - import { RecipeRecord } from "@cookware/lexicons"; 2 - import { z } from "zod"; 3 - 4 - export const recipeSchema = RecipeRecord.extend({ 5 - time: z.coerce.number(), 6 - image: z 7 - .instanceof(FileList) 8 - .or(z.null()), 9 - });
+4 -17
apps/web/src/hooks/use-xrpc.tsx
··· 1 - import { SERVER_URL } from "@/lib/utils"; 2 - import { useAuth } from "@/state/auth"; 3 - import { Client, simpleFetchHandler } from "@atcute/client"; 1 + import { useClient } from "@/state/auth"; 4 2 3 + /** @deprecated Use `useClient` from `state/auth/client` instead. */ 5 4 export function useXrpc() { 6 - const { agent } = useAuth(); 7 - 8 - if (agent) { 9 - return new Client({ 10 - handler: agent, 11 - proxy: { 12 - did: `did:web:${SERVER_URL}`, 13 - serviceId: '#recipes_blue', 14 - }, 15 - }); 16 - } 17 - 18 - const handler = simpleFetchHandler({ service: `https://${SERVER_URL}` }); 19 - return new Client({ handler }); 5 + const client = useClient(); 6 + return client; 20 7 }
+29 -14
apps/web/src/main.tsx
··· 4 4 import { createRouter, RouterProvider } from '@tanstack/react-router'; 5 5 import { QueryClientProvider, QueryClient } from '@tanstack/react-query' 6 6 import { ReactQueryDevtools } from '@tanstack/react-query-devtools' 7 - import { configureOAuth } from '@atcute/oauth-browser-client'; 7 + import { configureOAuth, defaultIdentityResolver } from '@atcute/oauth-browser-client'; 8 8 import './index.css' 9 - import { AuthProvider, useAuth } from './state/auth'; 10 9 import { ThemeProvider } from './components/theme-provider'; 10 + import { CompositeDidDocumentResolver, PlcDidDocumentResolver, WebDidDocumentResolver, XrpcHandleResolver } from '@atcute/identity-resolver'; 11 + import { SessionProvider, useSession } from './state/auth/session'; 12 + import { ClientProvider, useClient } from './state/auth'; 11 13 12 14 const router = createRouter({ 13 15 routeTree, 14 16 context: { 15 - auth: undefined!, 17 + session: undefined!, 18 + client: undefined!, 16 19 }, 17 20 }); 18 21 ··· 25 28 configureOAuth({ 26 29 metadata: { 27 30 client_id: import.meta.env.VITE_OAUTH_CLIENT_ID, 28 - redirect_uri: import.meta.env.VITE_OAUTH_REDIRECT_URL, 31 + redirect_uri: import.meta.env.VITE_OAUTH_REDIRECT_URI, 29 32 }, 33 + identityResolver: defaultIdentityResolver({ 34 + handleResolver: new XrpcHandleResolver({ serviceUrl: 'https://slingshot.microcosm.blue' }), 35 + didDocumentResolver: new CompositeDidDocumentResolver({ 36 + methods: { 37 + plc: new PlcDidDocumentResolver(), 38 + web: new WebDidDocumentResolver(), 39 + }, 40 + }), 41 + }), 30 42 }); 31 43 32 44 const queryClient = new QueryClient({ ··· 40 52 }); 41 53 42 54 const InnerApp = () => { 43 - const auth = useAuth(); 44 - return <RouterProvider router={router} context={{ auth }} /> 55 + const session = useSession(); 56 + const client = useClient(); 57 + return <RouterProvider router={router} context={{ session, client }} /> 45 58 }; 46 59 47 60 createRoot(document.getElementById('root')!).render( 48 61 <StrictMode> 49 - <ThemeProvider defaultTheme="dark" storageKey="recipes-theme"> 50 - <AuthProvider> 51 - <QueryClientProvider client={queryClient}> 52 - <InnerApp /> 53 - <ReactQueryDevtools initialIsOpen={false} /> 54 - </QueryClientProvider> 55 - </AuthProvider> 56 - </ThemeProvider> 62 + <SessionProvider> 63 + <ClientProvider> 64 + <ThemeProvider defaultTheme="dark" storageKey="recipes-theme"> 65 + <QueryClientProvider client={queryClient}> 66 + <InnerApp /> 67 + <ReactQueryDevtools initialIsOpen={false} /> 68 + </QueryClientProvider> 69 + </ThemeProvider> 70 + </ClientProvider> 71 + </SessionProvider> 57 72 </StrictMode>, 58 73 )
+9 -11
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"; 6 4 import { UseFormReturn } from "react-hook-form"; 7 5 import { TID } from '@atproto/common-web'; 8 - import { recipeSchema } from "@/forms/recipe"; 9 6 import { z } from "zod"; 10 7 import { ActorIdentifier, Did } from "@atcute/lexicons"; 11 8 12 9 import type {} from '@atcute/atproto'; 13 10 import type {} from '@cookware/lexicons'; 11 + import { useClient } from "../state/auth/client"; 14 12 15 13 const RQKEY_ROOT = 'posts'; 16 14 export const RQKEY = (cursor: string, did: string, rkey: string) => [RQKEY_ROOT, cursor, did, rkey]; 17 15 18 16 export const useRecipesQuery = (cursor: string, did?: Did) => { 19 - const rpc = useXrpc(); 17 + const client = useClient(); 20 18 return useQuery({ 21 19 queryKey: RQKEY(cursor, did ?? '', ''), 22 20 queryFn: async () => { 23 - const res = await rpc.get('blue.recipes.feed.getRecipes', { 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>
+289 -320
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 - import { recipeSchema } from "@/forms/recipe"; 44 12 45 13 export const Route = createFileRoute("/_/(app)/recipes/new")({ 46 14 beforeLoad: async ({ context }) => { 47 - if (!context.auth.isLoggedIn) { 15 + if (!context.session.isLoggedIn) { 48 16 throw redirect({ 49 17 to: '/login', 50 18 }); ··· 54 22 }); 55 23 56 24 function RouteComponent() { 57 - const form = useForm<z.infer<typeof recipeSchema>>({ 58 - resolver: zodResolver(recipeSchema), 59 - defaultValues: { 60 - title: "", 61 - time: 0, 62 - image: null, 63 - description: "", 64 - ingredients: [{ name: "" }], 65 - steps: [{ text: "" }], 66 - }, 67 - }); 68 - 69 - const { mutate, isPending } = useNewRecipeMutation(form); 70 - 71 - const onSubmit = (values: z.infer<typeof recipeSchema>) => { 72 - mutate({ recipe: values }); 73 - }; 74 - 75 - const imageRef = form.register("image"); 76 - 77 - const ingredients = useFieldArray({ 78 - control: form.control, 79 - name: "ingredients", 80 - }); 81 - 82 - const steps = useFieldArray({ 83 - control: form.control, 84 - name: "steps", 85 - }); 86 - 87 - return ( 88 - <> 89 - <Breadcrumbs /> 90 - <div className="flex-1 flex-col p-4 pt-0 max-w-xl w-full mx-auto"> 91 - <Card> 92 - <CardHeader> 93 - <CardTitle>New recipe</CardTitle> 94 - <CardDescription>Share your recipe with the world!</CardDescription> 95 - </CardHeader> 96 - <CardContent> 97 - <Form {...form}> 98 - <form 99 - onSubmit={form.handleSubmit(onSubmit)} 100 - className="space-y-8" 101 - > 102 - <FormField 103 - name="title" 104 - control={form.control} 105 - render={({ field }) => ( 106 - <FormItem> 107 - <FormLabel>Title</FormLabel> 108 - <FormControl> 109 - <Input placeholder="My awesome recipe!" {...field} /> 110 - </FormControl> 111 - <FormDescription> 112 - This is your recipe's name. 113 - </FormDescription> 114 - <FormMessage /> 115 - </FormItem> 116 - )} 117 - /> 118 - 119 - <FormField 120 - name="description" 121 - control={form.control} 122 - render={({ field: { value, ...field } }) => ( 123 - <FormItem> 124 - <FormLabel>Description</FormLabel> 125 - <FormControl> 126 - <Textarea 127 - className="resize-none" 128 - value={value || ""} 129 - {...field} 130 - /> 131 - </FormControl> 132 - <FormDescription>Describe your recipe, maybe tell the world how tasty it is? (Optional)</FormDescription> 133 - <FormMessage /> 134 - </FormItem> 135 - )} 136 - /> 137 - 138 - <FormField 139 - name="image" 140 - control={form.control} 141 - render={(_props) => ( 142 - <FormItem> 143 - <FormLabel>Image</FormLabel> 144 - <FormControl> 145 - <Input 146 - type="file" 147 - className="resize-none" 148 - {...imageRef} 149 - /> 150 - </FormControl> 151 - <FormMessage /> 152 - </FormItem> 153 - )} 154 - /> 155 - 156 - <FormField 157 - name="time" 158 - control={form.control} 159 - render={({ field: { value, ...field } }) => ( 160 - <FormItem> 161 - <FormLabel>Time</FormLabel> 162 - <FormControl> 163 - <Input 164 - type="number" 165 - className="resize-none" 166 - value={value || ""} 167 - {...field} 168 - /> 169 - </FormControl> 170 - <FormDescription>How long (in minutes) does your recipe take to complete?</FormDescription> 171 - <FormMessage /> 172 - </FormItem> 173 - )} 174 - /> 175 - 176 - <div className="grid gap-2"> 177 - <Label>Ingredients</Label> 178 - <Sortable 179 - value={ingredients.fields} 180 - onMove={({ activeIndex, overIndex }) => 181 - ingredients.move(activeIndex, overIndex)} 182 - > 183 - <div className="flex w-full flex-col gap-2"> 184 - {ingredients.fields.map((field, index) => ( 185 - <SortableItem key={field.id} value={field.id} asChild> 186 - <div className="grid grid-cols-[2rem_0.3fr_1fr_2rem] items-center gap-2"> 187 - <SortableDragHandle 188 - type="button" 189 - variant="outline" 190 - size="icon" 191 - className="size-8 shrink-0" 192 - > 193 - <DragHandleDots2Icon 194 - className="size-4" 195 - aria-hidden="true" 196 - /> 197 - </SortableDragHandle> 198 - 199 - <FormField 200 - control={form.control} 201 - name={`ingredients.${index}.amount`} 202 - render={({ field: { value, ...field } }) => ( 203 - <FormItem> 204 - <FormControl> 205 - <Input 206 - placeholder="Amount" 207 - value={value || ""} 208 - className="h-8" 209 - {...field} 210 - /> 211 - </FormControl> 212 - <FormMessage /> 213 - </FormItem> 214 - )} 215 - /> 216 - 217 - <FormField 218 - control={form.control} 219 - name={`ingredients.${index}.name`} 220 - render={({ field }) => ( 221 - <FormItem> 222 - <FormControl> 223 - <Input 224 - placeholder="Ingredient" 225 - className="h-8" 226 - {...field} 227 - /> 228 - </FormControl> 229 - <FormMessage /> 230 - </FormItem> 231 - )} 232 - /> 233 - 234 - <Button 235 - type="button" 236 - variant="destructive" 237 - className="size-8" 238 - onClick={(e) => { 239 - e.preventDefault(); 240 - ingredients.remove(index); 241 - }} 242 - > 243 - <TrashIcon /> 244 - </Button> 245 - </div> 246 - </SortableItem> 247 - ))} 248 - </div> 249 - </Sortable> 250 - <Button 251 - type="button" 252 - variant="secondary" 253 - onClick={(e) => { 254 - e.preventDefault(); 255 - ingredients.append({ 256 - name: "", 257 - amount: "", 258 - }); 259 - }} 260 - > 261 - Add 262 - </Button> 263 - </div> 264 - 265 - <div className="grid gap-2"> 266 - <Label>Steps</Label> 267 - <Sortable 268 - value={steps.fields} 269 - onMove={({ activeIndex, overIndex }) => 270 - steps.move(activeIndex, overIndex)} 271 - > 272 - <div className="flex w-full flex-col gap-2"> 273 - {steps.fields.map((field, index) => ( 274 - <SortableItem key={field.id} value={field.id} asChild> 275 - <div className="grid grid-cols-[2rem_auto_2rem] items-center gap-2"> 276 - <SortableDragHandle 277 - type="button" 278 - variant="outline" 279 - size="icon" 280 - className="size-8 shrink-0" 281 - > 282 - <DragHandleDots2Icon 283 - className="size-4" 284 - aria-hidden="true" 285 - /> 286 - </SortableDragHandle> 287 - <FormField 288 - control={form.control} 289 - name={`steps.${index}.text`} 290 - render={({ field }) => ( 291 - <FormItem> 292 - <FormControl> 293 - <Input className="h-8" {...field} /> 294 - </FormControl> 295 - <FormMessage /> 296 - </FormItem> 297 - )} 298 - /> 299 - 300 - <Button 301 - type="button" 302 - variant="destructive" 303 - className="size-8" 304 - onClick={(e) => { 305 - e.preventDefault(); 306 - steps.remove(index); 307 - }} 308 - > 309 - <TrashIcon /> 310 - </Button> 311 - </div> 312 - </SortableItem> 313 - ))} 314 - </div> 315 - </Sortable> 316 - <Button 317 - type="button" 318 - variant="secondary" 319 - onClick={(e) => { 320 - e.preventDefault(); 321 - steps.append({ text: "" }); 322 - }} 323 - > 324 - Add 325 - </Button> 326 - </div> 327 - 328 - <div className="grid justify-end"> 329 - <Button 330 - type="submit" 331 - className="ml-auto" 332 - disabled={isPending} 333 - > 334 - Submit 335 - </Button> 336 - </div> 337 - </form> 338 - </Form> 339 - </CardContent> 340 - </Card> 341 - </div> 342 - </> 343 - ); 25 + return (<></>); 26 + // const form = useForm<z.infer<typeof recipeSchema>>({ 27 + // resolver: zodResolver(recipeSchema), 28 + // defaultValues: { 29 + // title: "", 30 + // time: 0, 31 + // image: null, 32 + // description: "", 33 + // ingredients: [{ name: "" }], 34 + // steps: [{ text: "" }], 35 + // }, 36 + // }); 37 + // 38 + // const { mutate, isPending } = useNewRecipeMutation(form); 39 + // 40 + // const onSubmit = (values: z.infer<typeof recipeSchema>) => { 41 + // mutate({ recipe: values }); 42 + // }; 43 + // 44 + // const imageRef = form.register("image"); 45 + // 46 + // const ingredients = useFieldArray({ 47 + // control: form.control, 48 + // name: "ingredients", 49 + // }); 50 + // 51 + // const steps = useFieldArray({ 52 + // control: form.control, 53 + // name: "steps", 54 + // }); 55 + // 56 + // return ( 57 + // <> 58 + // <Breadcrumbs /> 59 + // <div className="flex-1 flex-col p-4 pt-0 max-w-xl w-full mx-auto"> 60 + // <Card> 61 + // <CardHeader> 62 + // <CardTitle>New recipe</CardTitle> 63 + // <CardDescription>Share your recipe with the world!</CardDescription> 64 + // </CardHeader> 65 + // <CardContent> 66 + // <Form {...form}> 67 + // <form 68 + // onSubmit={form.handleSubmit(onSubmit)} 69 + // className="space-y-8" 70 + // > 71 + // <FormField 72 + // name="title" 73 + // control={form.control} 74 + // render={({ field }) => ( 75 + // <FormItem> 76 + // <FormLabel>Title</FormLabel> 77 + // <FormControl> 78 + // <Input placeholder="My awesome recipe!" {...field} /> 79 + // </FormControl> 80 + // <FormDescription> 81 + // This is your recipe's name. 82 + // </FormDescription> 83 + // <FormMessage /> 84 + // </FormItem> 85 + // )} 86 + // /> 87 + // 88 + // <FormField 89 + // name="description" 90 + // control={form.control} 91 + // render={({ field: { value, ...field } }) => ( 92 + // <FormItem> 93 + // <FormLabel>Description</FormLabel> 94 + // <FormControl> 95 + // <Textarea 96 + // className="resize-none" 97 + // value={value || ""} 98 + // {...field} 99 + // /> 100 + // </FormControl> 101 + // <FormDescription>Describe your recipe, maybe tell the world how tasty it is? (Optional)</FormDescription> 102 + // <FormMessage /> 103 + // </FormItem> 104 + // )} 105 + // /> 106 + // 107 + // <FormField 108 + // name="image" 109 + // control={form.control} 110 + // render={(_props) => ( 111 + // <FormItem> 112 + // <FormLabel>Image</FormLabel> 113 + // <FormControl> 114 + // <Input 115 + // type="file" 116 + // className="resize-none" 117 + // {...imageRef} 118 + // /> 119 + // </FormControl> 120 + // <FormMessage /> 121 + // </FormItem> 122 + // )} 123 + // /> 124 + // 125 + // <FormField 126 + // name="time" 127 + // control={form.control} 128 + // render={({ field: { value, ...field } }) => ( 129 + // <FormItem> 130 + // <FormLabel>Time</FormLabel> 131 + // <FormControl> 132 + // <Input 133 + // type="number" 134 + // className="resize-none" 135 + // value={value || ""} 136 + // {...field} 137 + // /> 138 + // </FormControl> 139 + // <FormDescription>How long (in minutes) does your recipe take to complete?</FormDescription> 140 + // <FormMessage /> 141 + // </FormItem> 142 + // )} 143 + // /> 144 + // 145 + // <div className="grid gap-2"> 146 + // <Label>Ingredients</Label> 147 + // <Sortable 148 + // value={ingredients.fields} 149 + // onMove={({ activeIndex, overIndex }) => 150 + // ingredients.move(activeIndex, overIndex)} 151 + // > 152 + // <div className="flex w-full flex-col gap-2"> 153 + // {ingredients.fields.map((field, index) => ( 154 + // <SortableItem key={field.id} value={field.id} asChild> 155 + // <div className="grid grid-cols-[2rem_0.3fr_1fr_2rem] items-center gap-2"> 156 + // <SortableDragHandle 157 + // type="button" 158 + // variant="outline" 159 + // size="icon" 160 + // className="size-8 shrink-0" 161 + // > 162 + // <DragHandleDots2Icon 163 + // className="size-4" 164 + // aria-hidden="true" 165 + // /> 166 + // </SortableDragHandle> 167 + // 168 + // <FormField 169 + // control={form.control} 170 + // name={`ingredients.${index}.amount`} 171 + // render={({ field: { value, ...field } }) => ( 172 + // <FormItem> 173 + // <FormControl> 174 + // <Input 175 + // placeholder="Amount" 176 + // value={value || ""} 177 + // className="h-8" 178 + // {...field} 179 + // /> 180 + // </FormControl> 181 + // <FormMessage /> 182 + // </FormItem> 183 + // )} 184 + // /> 185 + // 186 + // <FormField 187 + // control={form.control} 188 + // name={`ingredients.${index}.name`} 189 + // render={({ field }) => ( 190 + // <FormItem> 191 + // <FormControl> 192 + // <Input 193 + // placeholder="Ingredient" 194 + // className="h-8" 195 + // {...field} 196 + // /> 197 + // </FormControl> 198 + // <FormMessage /> 199 + // </FormItem> 200 + // )} 201 + // /> 202 + // 203 + // <Button 204 + // type="button" 205 + // variant="destructive" 206 + // className="size-8" 207 + // onClick={(e) => { 208 + // e.preventDefault(); 209 + // ingredients.remove(index); 210 + // }} 211 + // > 212 + // <TrashIcon /> 213 + // </Button> 214 + // </div> 215 + // </SortableItem> 216 + // ))} 217 + // </div> 218 + // </Sortable> 219 + // <Button 220 + // type="button" 221 + // variant="secondary" 222 + // onClick={(e) => { 223 + // e.preventDefault(); 224 + // ingredients.append({ 225 + // name: "", 226 + // amount: "", 227 + // }); 228 + // }} 229 + // > 230 + // Add 231 + // </Button> 232 + // </div> 233 + // 234 + // <div className="grid gap-2"> 235 + // <Label>Steps</Label> 236 + // <Sortable 237 + // value={steps.fields} 238 + // onMove={({ activeIndex, overIndex }) => 239 + // steps.move(activeIndex, overIndex)} 240 + // > 241 + // <div className="flex w-full flex-col gap-2"> 242 + // {steps.fields.map((field, index) => ( 243 + // <SortableItem key={field.id} value={field.id} asChild> 244 + // <div className="grid grid-cols-[2rem_auto_2rem] items-center gap-2"> 245 + // <SortableDragHandle 246 + // type="button" 247 + // variant="outline" 248 + // size="icon" 249 + // className="size-8 shrink-0" 250 + // > 251 + // <DragHandleDots2Icon 252 + // className="size-4" 253 + // aria-hidden="true" 254 + // /> 255 + // </SortableDragHandle> 256 + // <FormField 257 + // control={form.control} 258 + // name={`steps.${index}.text`} 259 + // render={({ field }) => ( 260 + // <FormItem> 261 + // <FormControl> 262 + // <Input className="h-8" {...field} /> 263 + // </FormControl> 264 + // <FormMessage /> 265 + // </FormItem> 266 + // )} 267 + // /> 268 + // 269 + // <Button 270 + // type="button" 271 + // variant="destructive" 272 + // className="size-8" 273 + // onClick={(e) => { 274 + // e.preventDefault(); 275 + // steps.remove(index); 276 + // }} 277 + // > 278 + // <TrashIcon /> 279 + // </Button> 280 + // </div> 281 + // </SortableItem> 282 + // ))} 283 + // </div> 284 + // </Sortable> 285 + // <Button 286 + // type="button" 287 + // variant="secondary" 288 + // onClick={(e) => { 289 + // e.preventDefault(); 290 + // steps.append({ text: "" }); 291 + // }} 292 + // > 293 + // Add 294 + // </Button> 295 + // </div> 296 + // 297 + // <div className="grid justify-end"> 298 + // <Button 299 + // type="submit" 300 + // className="ml-auto" 301 + // disabled={isPending} 302 + // > 303 + // Submit 304 + // </Button> 305 + // </div> 306 + // </form> 307 + // </Form> 308 + // </CardContent> 309 + // </Card> 310 + // </div> 311 + // </> 312 + // ); 344 313 } 345 314 346 315 const Breadcrumbs = () => (
+52 -70
apps/web/src/routes/_.(auth)/login.tsx
··· 13 13 CardHeader, 14 14 CardTitle, 15 15 } from '@/components/ui/card' 16 + import { Field, FieldError, FieldGroup, FieldLabel } from '@/components/ui/field' 16 17 import { Input } from '@/components/ui/input' 17 18 import { Label } from '@/components/ui/label' 18 19 import { Separator } from '@/components/ui/separator' 19 20 import { SidebarTrigger } from '@/components/ui/sidebar' 20 - import { sleep } from '@/lib/utils' 21 - import { 22 - createAuthorizationUrl, 23 - resolveFromIdentity, 24 - } from '@atcute/oauth-browser-client' 21 + import { useSession } from '@/state/auth/session' 25 22 import { useMutation } from '@tanstack/react-query' 26 23 import { createFileRoute } from '@tanstack/react-router' 27 24 import { useState } from 'react' ··· 31 28 }) 32 29 33 30 function RouteComponent() { 31 + const { signIn } = useSession(); 34 32 const [handle, setHandle] = useState('') 35 33 36 34 const { mutate, isPending, error } = useMutation({ 37 35 mutationKey: ['login'], 38 36 mutationFn: async () => { 39 - const { identity, metadata } = await resolveFromIdentity(handle) 40 - 41 - const authUrl = await createAuthorizationUrl({ 42 - metadata: metadata, 43 - identity: identity, 44 - scope: 'atproto transition:generic', 45 - }) 46 - 47 - await sleep(200) 48 - 49 - return authUrl 50 - }, 51 - onSuccess: async (authUrl: URL) => { 52 - window.location.assign(authUrl) 53 - 54 - await new Promise((_resolve, reject) => { 55 - const listener = () => { 56 - reject(new Error(`user aborted the login request`)) 57 - } 58 - 59 - window.addEventListener('pageshow', listener, { once: true }) 60 - }) 37 + await signIn(handle); 38 + return; 61 39 }, 62 40 }) 63 41 ··· 77 55 </div> 78 56 </header> 79 57 <div className="flex flex-1 flex-col items-center justify-center gap-4 p-4 pt-0"> 80 - <Card className="max-w-sm w-full"> 81 - <CardHeader> 82 - <CardTitle>Log in</CardTitle> 83 - <CardDescription> 84 - Enter your handle below to sign in to your account. 85 - </CardDescription> 86 - </CardHeader> 87 - <CardContent> 88 - <div className="flex flex-col gap-2"> 89 - <Label htmlFor="handle">Handle</Label> 90 - <Input 91 - className={`${error ? 'border-destructive text-destructive' : ''}`} 92 - type="text" 93 - id="handle" 94 - name="handle" 95 - placeholder="johndoe.bsky.social" 96 - required 97 - value={handle} 98 - onChange={(e) => setHandle(e.currentTarget.value)} 99 - /> 100 - {error && ( 101 - <p className="text-sm font-medium text-destructive"> 102 - {error.message} 103 - </p> 104 - )} 105 - </div> 106 - </CardContent> 107 - <CardFooter className="grid gap-2"> 108 - <Button onClick={() => mutate()} disabled={isPending}> 109 - Log in 110 - </Button> 111 - <p className="text-sm text-muted-foreground text-center"> 112 - Don't have an account?{' '} 113 - <a 114 - className="font-bold text-primary" 115 - href="https://bsky.app/" 116 - target="_blank" 117 - > 118 - Sign up on Bluesky! 119 - </a> 120 - </p> 121 - </CardFooter> 122 - </Card> 58 + <form onSubmit={e => { 59 + e.preventDefault(); 60 + mutate(); 61 + }}> 62 + <Card className="max-w-sm w-full"> 63 + <CardHeader> 64 + <CardTitle>Log in</CardTitle> 65 + <CardDescription> 66 + Enter your Atmosphere handle below to sign in to your account. 67 + </CardDescription> 68 + </CardHeader> 69 + <CardContent> 70 + <FieldGroup> 71 + <Field data-invalid={error ? true : false}> 72 + <FieldLabel htmlFor="handle">Handle</FieldLabel> 73 + <Input 74 + id="handle" 75 + placeholder="johndoe.bsky.social" 76 + required 77 + autoComplete="username" 78 + aria-invalid={error ? 'true' : 'false'} 79 + tabIndex={0} 80 + autoFocus 81 + value={handle} 82 + onChange={(e) => setHandle(e.currentTarget.value)} 83 + /> 84 + {error && <FieldError>{error.message}</FieldError>} 85 + </Field> 86 + </FieldGroup> 87 + </CardContent> 88 + <CardFooter className="grid gap-2"> 89 + <Button type="submit" disabled={isPending}> 90 + Log in 91 + </Button> 92 + <p className="text-sm text-muted-foreground text-center"> 93 + Don't have an account?{' '} 94 + <a 95 + className="font-bold text-primary" 96 + href="https://bsky.app/" 97 + target="_blank" 98 + > 99 + Sign up on Bluesky! 100 + </a> 101 + </p> 102 + </CardFooter> 103 + </Card> 104 + </form> 123 105 </div> 124 106 </> 125 107 )
+3 -2
apps/web/src/routes/__root.tsx
··· 3 3 SidebarInset, 4 4 SidebarProvider, 5 5 } from '@/components/ui/sidebar' 6 - import { AuthContextType } from '@/state/auth'; 6 + import { ClientContext, SessionContext } from '@/state/auth'; 7 7 import { Outlet, createRootRouteWithContext } from '@tanstack/react-router' 8 8 9 9 type RootContext = { 10 - auth: AuthContextType; 10 + session: SessionContext; 11 + client: ClientContext['client']; 11 12 }; 12 13 13 14 export const Route = createRootRouteWithContext<RootContext>()({
+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>
+38
apps/web/src/state/auth/client.tsx
··· 1 + import { Client, simpleFetchHandler } from "@atcute/client"; 2 + import { createContext, PropsWithChildren, useContext, useEffect, useState } from "react"; 3 + import { useSession } from "./session"; 4 + 5 + export type ClientContext = { 6 + client: Client; 7 + }; 8 + 9 + const clientContext = createContext<ClientContext>({ 10 + client: new Client({ handler: simpleFetchHandler({ service: import.meta.env.VITE_API_SERVICE }) }), 11 + }); 12 + 13 + export const ClientProvider = ({ children }: PropsWithChildren) => { 14 + const { agent } = useSession(); 15 + const [client, setClient] = useState<Client>( 16 + () => new Client({ handler: simpleFetchHandler({ service: import.meta.env.VITE_API_SERVICE }) }) 17 + ); 18 + 19 + useEffect(() => { 20 + setClient(new Client({ 21 + handler: agent ?? simpleFetchHandler({ service: import.meta.env.VITE_API_SERVICE }), 22 + proxy: { 23 + did: 'did:web:localhost', 24 + serviceId: '#api_service' 25 + }, 26 + })); 27 + }, [agent]); 28 + 29 + return ( 30 + <clientContext.Provider value={{ client: client }}> 31 + {children} 32 + </clientContext.Provider> 33 + ); 34 + } 35 + 36 + export const useClient = () => { 37 + return useContext(clientContext).client; 38 + }
+2
apps/web/src/state/auth/index.ts
··· 1 + export * from './client'; 2 + export * from './session';
+121
apps/web/src/state/auth/session.tsx
··· 1 + import { isDid, isActorIdentifier } from "@atcute/lexicons/syntax"; 2 + import { createAuthorizationUrl, deleteStoredSession, finalizeAuthorization, getSession, OAuthUserAgent, Session } from "@atcute/oauth-browser-client"; 3 + import { createContext, PropsWithChildren, useContext, useEffect, useState } from "react"; 4 + 5 + export type SessionContext = { 6 + session: null | Session; 7 + agent: null | OAuthUserAgent; 8 + isLoading: boolean; 9 + isLoggedIn: boolean; 10 + signIn: (handle: string) => Promise<void>; 11 + signOut: () => Promise<void>; 12 + }; 13 + 14 + const sessionContext = createContext<SessionContext>({ 15 + session: null, 16 + agent: null, 17 + isLoading: false, 18 + isLoggedIn: false, 19 + signIn: async () => { 20 + throw new Error("AuthContext not initialized"); 21 + }, 22 + signOut: async () => { 23 + throw new Error("AuthContext not initialized"); 24 + }, 25 + }); 26 + 27 + const LS_LAST_SIGNED_IN = "recipes:last-signed-in"; 28 + 29 + export const SessionProvider = ({ children }: PropsWithChildren<{}>) => { 30 + const [initialized, setInitialized] = useState(false); 31 + const [loading, setLoading] = useState(true); 32 + const [session, setSession] = useState<null | Session>(null); 33 + const [agent, setAgent] = useState<null | OAuthUserAgent>(null); 34 + 35 + useEffect(() => { 36 + setInitialized(false); 37 + setSession(null); 38 + setAgent(null); 39 + 40 + const params = new URLSearchParams(location.hash.slice(1)); 41 + if (params.has("state") && params.has("iss") && params.has("code")) { 42 + // If there is an active auth attempt: 43 + history.replaceState(null, "", location.pathname + location.search); 44 + console.log("finalizing authorization..."); 45 + finalizeAuthorization(params) 46 + .then(val => { 47 + setSession(val.session); 48 + setAgent(new OAuthUserAgent(val.session)); 49 + }) 50 + .catch(err => { 51 + console.error("Failed to initialize session:", err); 52 + }) 53 + .finally(() => { 54 + setLoading(false); 55 + setInitialized(true); 56 + }); 57 + } else { 58 + const lastSignedIn = localStorage.getItem(LS_LAST_SIGNED_IN); 59 + if (lastSignedIn && isDid(lastSignedIn)) { 60 + getSession(lastSignedIn, { allowStale: true }) 61 + .then((session) => { 62 + setSession(session); 63 + setAgent(new OAuthUserAgent(session)); 64 + }) 65 + .catch(err => { 66 + console.error("Failed to initialize session:", err); 67 + }) 68 + } 69 + 70 + setLoading(false); 71 + setInitialized(true); 72 + } 73 + }, []); 74 + 75 + const signIn = async (handle: string) => { 76 + if (!isActorIdentifier(handle)) throw new Error("Invalid handle or DID!"); 77 + const authUrl = await createAuthorizationUrl({ 78 + target: { type: 'account', identifier: handle }, 79 + scope: 'atproto transition:generic', 80 + }); 81 + window.location.assign(authUrl); 82 + }; 83 + 84 + const signOut = async () => { 85 + if (!agent || !session) return; 86 + 87 + const did = session.info.sub; 88 + try { 89 + const session = await getSession(did, { allowStale: true }); 90 + const agent = new OAuthUserAgent(session); 91 + 92 + await agent.signOut(); 93 + setSession(null); 94 + } catch(err) { 95 + deleteStoredSession(did); 96 + } 97 + }; 98 + 99 + if (!initialized) return ( 100 + <p>Loading...</p> 101 + ); 102 + 103 + return ( 104 + <sessionContext.Provider value={{ 105 + isLoading: loading, 106 + isLoggedIn: session !== null, 107 + session, 108 + agent, 109 + signIn, 110 + signOut, 111 + }}> 112 + {children} 113 + </sessionContext.Provider> 114 + ); 115 + }; 116 + 117 + export const useSession = () => { 118 + const ctx = useContext(sessionContext); 119 + if (!ctx) throw new Error("useSession() must be called inside a <SessionProvider />!"); 120 + return ctx; 121 + };
-78
apps/web/src/state/auth.tsx
··· 1 - import { Did } from "@atcute/lexicons"; 2 - import { finalizeAuthorization, getSession, OAuthUserAgent } from "@atcute/oauth-browser-client"; 3 - import { createContext, PropsWithChildren, useContext, useEffect, useState } from "react"; 4 - 5 - export type AuthContextType = { 6 - isLoggedIn: boolean; 7 - agent?: OAuthUserAgent; 8 - logOut: () => Promise<void>; 9 - }; 10 - 11 - const AuthContext = createContext<AuthContextType>({ 12 - isLoggedIn: false, 13 - logOut: async () => {}, 14 - }); 15 - 16 - export const AuthProvider = ({ children }: PropsWithChildren) => { 17 - const [isReady, setIsReady] = useState(false); 18 - const [isLoggedIn, setIsLoggedIn] = useState(false); 19 - const [agent, setAgent] = useState<OAuthUserAgent | undefined>(undefined); 20 - 21 - useEffect(() => { 22 - const init = async () => { 23 - const params = new URLSearchParams(location.hash.slice(1)); 24 - 25 - if (params.has("state") && (params.has("code") || params.has("error"))) { 26 - history.replaceState(null, "", location.pathname + location.search); 27 - 28 - const session = await finalizeAuthorization(params); 29 - const did = session.info.sub; 30 - 31 - localStorage.setItem("lastSignedIn", did); 32 - return session; 33 - 34 - } else { 35 - const lastSignedIn = localStorage.getItem("lastSignedIn"); 36 - 37 - if (lastSignedIn) { 38 - try { 39 - return await getSession(lastSignedIn as Did); 40 - } catch (err) { 41 - localStorage.removeItem("lastSignedIn"); 42 - throw err; 43 - } 44 - } 45 - } 46 - }; 47 - 48 - init() 49 - .then(session => { 50 - if (session) { 51 - setAgent(new OAuthUserAgent(session)); 52 - setIsLoggedIn(true); 53 - } 54 - 55 - setIsReady(true) 56 - }) 57 - .catch(() => {}); 58 - }, []); 59 - 60 - if (!isReady) return null; 61 - 62 - return ( 63 - <AuthContext.Provider value={{ 64 - isLoggedIn, 65 - agent, 66 - logOut: async () => { 67 - setIsLoggedIn(false); 68 - await agent?.signOut(); 69 - }, 70 - }}> 71 - {children} 72 - </AuthContext.Provider> 73 - ); 74 - }; 75 - 76 - export const useAuth = () => { 77 - return useContext(AuthContext); 78 - };
+1 -3
apps/web/src/vite-env.d.ts
··· 1 1 /// <reference types="vite/client" /> 2 - /// <reference types="@cookware/lexicons" /> 3 - /// <reference types="@atcute/bluesky/lexicons" /> 4 2 5 3 interface ImportMetaEnv { 6 4 readonly VITE_API_SERVICE: string; 7 5 readonly VITE_DEV_SERVER_PORT?: string; 8 6 readonly VITE_CLIENT_URI: string; 9 7 readonly VITE_OAUTH_CLIENT_ID: string; 10 - readonly VITE_OAUTH_REDIRECT_URL: string; 8 + readonly VITE_OAUTH_REDIRECT_URI: string; 11 9 readonly VITE_OAUTH_SCOPE: string; 12 10 } 13 11
+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: {
+27 -28
apps/web/vite.config.ts
··· 1 1 import { defineConfig } from 'vite' 2 - import react from '@vitejs/plugin-react-swc' 2 + import react from '@vitejs/plugin-react' 3 3 import { tanstackRouter } from '@tanstack/router-plugin/vite' 4 4 import path from 'path' 5 - import metadata from "./public/client-metadata.json"; 5 + import metadata from "./public/oauth-client-metadata.json" with { type: 'json' }; 6 6 7 7 const SERVER_HOST = "127.0.0.1"; 8 8 const SERVER_PORT = 5173; ··· 11 11 export default defineConfig({ 12 12 plugins: [ 13 13 tanstackRouter(), 14 - react(), 14 + react({ babel: { plugins: ['babel-plugin-react-compiler'] } }), 15 + { 16 + name: '_config', 17 + config(_conf, { command }) { 18 + if (command === 'build') { 19 + process.env.VITE_OAUTH_CLIENT_ID = metadata.client_id; 20 + process.env.VITE_OAUTH_REDIRECT_URI = metadata.redirect_uris[0]; 21 + } else { 22 + const redirectUri = (() => { 23 + const url = new URL(metadata.redirect_uris[0]); 24 + return `http://${SERVER_HOST}:${SERVER_PORT}${url.pathname}`; 25 + })(); 15 26 16 - { 17 - name: "oauth", 18 - config(_conf, { command }) { 19 - if (command === "build") { 20 - process.env.VITE_OAUTH_CLIENT_ID = metadata.client_id; 21 - process.env.VITE_OAUTH_REDIRECT_URL = metadata.redirect_uris[0]; 22 - } else { 23 - const redirectUri = ((): string => { 24 - const url = new URL(metadata.redirect_uris[0]); 25 - return `https://local.recipes.blue${url.pathname}`; 26 - })(); 27 + const clientId = 28 + `http://localhost` + 29 + `?redirect_uri=${encodeURIComponent(redirectUri)}` + 30 + `&scope=${encodeURIComponent(metadata.scope)}`; 27 31 28 - const clientId = 29 - `https://local.recipes.blue` + 30 - `?redirect_uri=${encodeURIComponent(redirectUri)}` + 31 - `&scope=${encodeURIComponent(metadata.scope)}`; 32 + process.env.VITE_API_SERVICE = 'http://localhost:3000'; 33 + process.env.VITE_DEV_SERVER_PORT = '' + SERVER_PORT; 34 + process.env.VITE_OAUTH_CLIENT_ID = clientId; 35 + process.env.VITE_OAUTH_REDIRECT_URI = redirectUri; 36 + } 32 37 33 - process.env.VITE_DEV_SERVER_PORT = "" + SERVER_PORT; 34 - process.env.VITE_OAUTH_CLIENT_ID = clientId; 35 - process.env.VITE_OAUTH_REDIRECT_URL = redirectUri; 36 - } 37 - 38 - process.env.VITE_CLIENT_URI = metadata.client_uri; 39 - process.env.VITE_OAUTH_SCOPE = metadata.scope; 40 - }, 41 - }, 38 + process.env.VITE_CLIENT_URI = metadata.client_uri; 39 + process.env.VITE_OAUTH_SCOPE = metadata.scope; 40 + }, 41 + }, 42 42 ], 43 43 server: { 44 - allowedHosts: ["local.recipes.blue"], 45 44 host: SERVER_HOST, 46 45 port: SERVER_PORT, 47 46 },
+54 -181
bun.lock
··· 22 22 "@cookware/lexicons": "workspace:*", 23 23 "@libsql/client": "^0.14.0", 24 24 "drizzle-orm": "catalog:", 25 + "hono": "^4.10.7", 25 26 "pino": "^9.5.0", 26 27 }, 27 28 "devDependencies": { ··· 29 30 "@cookware/tsconfig": "workspace:*", 30 31 "@types/bun": "catalog:", 31 32 "drizzle-kit": "^0.29.0", 33 + "pino-pretty": "^13.1.2", 32 34 "rimraf": "^6.0.1", 33 35 }, 34 36 }, ··· 61 63 "dependencies": { 62 64 "@atcute/atproto": "^3.1.9", 63 65 "@atcute/client": "catalog:", 66 + "@atcute/identity-resolver": "^1.1.4", 64 67 "@atcute/lexicons": "catalog:", 65 - "@atcute/oauth-browser-client": "^1.0.7", 68 + "@atcute/oauth-browser-client": "^2.0.1", 66 69 "@atproto/common": "^0.4.5", 67 70 "@atproto/common-web": "^0.3.1", 68 71 "@dnd-kit/core": "^6.3.1", ··· 75 78 "@radix-ui/react-dialog": "^1.1.4", 76 79 "@radix-ui/react-dropdown-menu": "^2.1.4", 77 80 "@radix-ui/react-icons": "^1.3.2", 78 - "@radix-ui/react-label": "^2.1.0", 79 - "@radix-ui/react-separator": "^1.1.0", 80 - "@radix-ui/react-slot": "^1.1.0", 81 + "@radix-ui/react-label": "^2.1.8", 82 + "@radix-ui/react-separator": "^1.1.8", 83 + "@radix-ui/react-slot": "^1.2.4", 81 84 "@radix-ui/react-tooltip": "^1.1.4", 82 85 "@tanstack/react-query": "^5.62.2", 83 86 "@tanstack/react-query-devtools": "^5.62.2", ··· 102 105 "@types/node": "^22.10.1", 103 106 "@types/react": "^19.0.0", 104 107 "@types/react-dom": "^19.0.0", 108 + "@vitejs/plugin-react": "^5.1.1", 105 109 "@vitejs/plugin-react-swc": "^3.5.0", 106 110 "autoprefixer": "^10.4.20", 111 + "babel-plugin-react-compiler": "^1.0.0", 107 112 "cssnano": "^7.0.6", 108 113 "eslint": "^9.15.0", 109 114 "eslint-plugin-react-hooks": "^5.0.0", ··· 114 119 "tailwindcss": "^3.4.16", 115 120 "typescript": "~5.6.2", 116 121 "typescript-eslint": "^8.15.0", 117 - "vite": "^6.0.1", 122 + "vite": "^7.2.4", 118 123 }, 119 124 }, 120 125 "libs/database": { ··· 123 128 "dependencies": { 124 129 "@libsql/client": "^0.15.15", 125 130 "drizzle-orm": "catalog:", 131 + "pg": "^8.16.3", 126 132 "zod": "^3.23.8", 127 133 }, 128 134 "devDependencies": { ··· 132 138 "@cookware/tsconfig": "workspace:*", 133 139 "@types/bun": "catalog:", 134 140 "@types/node": "^22.10.1", 141 + "@types/pg": "^8.15.6", 135 142 "drizzle-kit": "^0.29.0", 136 143 "typescript": "^5.2.2", 137 144 }, ··· 165 172 "@atcute/atproto": "^3.1.9", 166 173 "@atcute/bluesky": "^3.2.11", 167 174 "@atcute/client": "^4.0.5", 168 - "@atcute/lexicons": "^1.2.4", 175 + "@atcute/lexicons": "^1.2.5", 169 176 "@types/bun": "^1.3.3", 170 177 "drizzle-orm": "^0.44.7", 171 178 }, ··· 174 181 175 182 "@atcute/atproto": ["@atcute/atproto@3.1.9", "", { "dependencies": { "@atcute/lexicons": "^1.2.2" } }, "sha512-DyWwHCTdR4hY2BPNbLXgVmm7lI+fceOwWbE4LXbGvbvVtSn+ejSVFaAv01Ra3kWDha0whsOmbJL8JP0QPpf1+w=="], 176 183 177 - "@atcute/bluesky": ["@atcute/bluesky@3.2.10", "", { "dependencies": { "@atcute/atproto": "^3.1.9", "@atcute/lexicons": "^1.2.2" } }, "sha512-qwQWTzRf3umnh2u41gdU+xWYkbzGlKDupc3zeOB+YjmuP1N9wEaUhwS8H7vgrqr0xC9SGNDjeUVcjC4m5BPLBg=="], 184 + "@atcute/bluesky": ["@atcute/bluesky@3.2.11", "", { "dependencies": { "@atcute/atproto": "^3.1.9", "@atcute/lexicons": "^1.2.5" } }, "sha512-AboS6y4t+zaxIq7E4noue10csSpIuk/Uwo30/l6GgGBDPXrd7STw8Yb5nGZQP+TdG/uC8/c2mm7UnY65SDOh6A=="], 178 185 179 186 "@atcute/cbor": ["@atcute/cbor@2.2.8", "", { "dependencies": { "@atcute/cid": "^2.2.6", "@atcute/multibase": "^1.1.6", "@atcute/uint8array": "^1.0.5" } }, "sha512-UzOAN9BuN6JCXgn0ryV8qZuRJUDrNqrbLd6EFM8jc6RYssjRyGRxNy6RZ1NU/07Hd8Tq/0pz8+nQiMu5Zai5uw=="], 180 187 ··· 192 199 193 200 "@atcute/lex-cli": ["@atcute/lex-cli@2.3.3", "", { "dependencies": { "@atcute/lexicon-doc": "^2.0.0", "@badrap/valita": "^0.4.6", "@optique/core": "^0.6.2", "@optique/run": "^0.6.2", "picocolors": "^1.1.1", "prettier": "^3.6.2" }, "bin": { "lex-cli": "cli.mjs" } }, "sha512-L+fxfJWVBFiuS53yK2aDg7F3R0Ak7UOj9BOM2khofR6yBSKHrkCuk3b6GrkbcMKo+9O0ynaLsuXxUNlHvw/m3w=="], 194 201 195 - "@atcute/lexicon-doc": ["@atcute/lexicon-doc@2.0.1", "", { "dependencies": { "@atcute/identity": "^1.1.3", "@atcute/lexicons": "1.2.4", "@badrap/valita": "^0.4.6" } }, "sha512-yWgcBYkvifczVODZSgdVkIljzIfdh50t+QXjkDL/FSu2RP43NGBEZ5xfZqJcT68/UoyE+doSg0dhvOEIlVGU/A=="], 202 + "@atcute/lexicon-doc": ["@atcute/lexicon-doc@2.0.4", "", { "dependencies": { "@atcute/identity": "^1.1.3", "@atcute/lexicons": "^1.2.5", "@badrap/valita": "^0.4.6" } }, "sha512-YfwlYFoYiBvRIYG0I1zsINCTFugFtS8l67uT3nQ04zdKVflzdg8uUj8cNZYRNY1V7okoOPdikhR4kPFhYGyemw=="], 196 203 197 204 "@atcute/lexicons": ["@atcute/lexicons@1.2.5", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "esm-env": "^1.2.2" } }, "sha512-9yO9WdgxW8jZ7SbzUycH710z+JmsQ9W9n5S6i6eghYju32kkluFmgBeS47r8e8p2+Dv4DemS7o/3SUGsX9FR5Q=="], 198 205 199 206 "@atcute/multibase": ["@atcute/multibase@1.1.6", "", { "dependencies": { "@atcute/uint8array": "^1.0.5" } }, "sha512-HBxuCgYLKPPxETV0Rot4VP9e24vKl8JdzGCZOVsDaOXJgbRZoRIF67Lp0H/OgnJeH/Xpva8Z5ReoTNJE5dn3kg=="], 200 207 201 - "@atcute/oauth-browser-client": ["@atcute/oauth-browser-client@1.0.27", "", { "dependencies": { "@atcute/client": "^4.0.4", "@atcute/identity": "^1.1.1", "@atcute/lexicons": "^1.2.2", "@atcute/multibase": "^1.1.6", "@atcute/uint8array": "^1.0.5", "nanoid": "^5.1.5" } }, "sha512-Ng1tCOTMLgFHHoIHXTtCZR1/ND62an1qxPX2kBoUzkxxd7iCP7IBYYqOiKyJYT5n1R4zS+s29hFS4t9mxXa5kQ=="], 208 + "@atcute/oauth-browser-client": ["@atcute/oauth-browser-client@2.0.1", "", { "dependencies": { "@atcute/client": "^4.0.5", "@atcute/identity": "^1.1.1", "@atcute/identity-resolver": "^1.1.4", "@atcute/lexicons": "^1.2.2", "@atcute/multibase": "^1.1.6", "@atcute/uint8array": "^1.0.5", "nanoid": "^5.1.5" } }, "sha512-lG021GkeORG06zfFf4bH85egObjBEKHNgAWHvbtY/E2dX4wxo88hf370pJDx8acdnuUJLJ2VKPikJtZwo4Heeg=="], 202 209 203 210 "@atcute/uint8array": ["@atcute/uint8array@1.0.5", "", {}, "sha512-XLWWxoR2HNl2qU+FCr0rp1APwJXci7HnzbOQLxK55OaMNBXZ19+xNC5ii4QCsThsDxa4JS/JTzuiQLziITWf2Q=="], 204 211 ··· 293 300 "@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ=="], 294 301 295 302 "@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.27.1", "", { "dependencies": { "@babel/helper-module-transforms": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw=="], 303 + 304 + "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="], 305 + 306 + "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="], 296 307 297 308 "@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-create-class-features-plugin": "^7.28.5", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-x2Qa+v/CuEoX7Dr31iAfr0IhInrVOWZU/2vJMJ00FOR/2nM0BcBEclpaf9sWCDc+v5e9dMrhSH8/atq/kX7+bA=="], 298 309 ··· 672 683 673 684 "@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="], 674 685 675 - "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="], 686 + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.47", "", {}, "sha512-8QagwMH3kNCuzD8EWL8R2YPW5e4OrHNSAHRFDdmFqEwEaD/KcNKjVoumo+gP2vW5eKB2UPbM6vTYiGZX0ixLnw=="], 676 687 677 688 "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.53.3", "", { "os": "android", "cpu": "arm" }, "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w=="], 678 689 ··· 790 801 791 802 "@typelex/emitter": ["@typelex/emitter@0.4.0", "", { "dependencies": { "@typespec/compiler": "^1.4.0" } }, "sha512-BaKny+8TA0yX5jZibkAodHHKLJ6l6xVe5ut7KeoUyTD63lSSuB9OXe8tWXrs2DbeR/hialCimHFZQ3xANleMow=="], 792 803 804 + "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], 805 + 806 + "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], 807 + 808 + "@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="], 809 + 810 + "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], 811 + 793 812 "@types/bun": ["@types/bun@1.3.3", "", { "dependencies": { "bun-types": "1.3.3" } }, "sha512-ogrKbJ2X5N0kWLLFKeytG0eHDleBYtngtlbu9cyBKFtNL3cnpDZkNdQj8flVf6WTZUX5ulI9AY1oa7ljhSrp+g=="], 794 813 795 814 "@types/connect": ["@types/connect@3.4.36", "", { "dependencies": { "@types/node": "*" } }, "sha512-P63Zd/JUGq+PdrM1lv0Wv5SBYeA2+CORvbrXbngriYY0jzLUWfQMQQxOhjONEz/wlHOAxOdY7CY65rgQdTjq2w=="], ··· 802 821 803 822 "@types/node": ["@types/node@22.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ=="], 804 823 805 - "@types/pg": ["@types/pg@8.6.1", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w=="], 824 + "@types/pg": ["@types/pg@8.15.6", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ=="], 806 825 807 826 "@types/pg-pool": ["@types/pg-pool@2.0.6", "", { "dependencies": { "@types/pg": "*" } }, "sha512-TaAUE5rq2VQYxab5Ts7WZhKNmuN78Q6PiFonTDdpbx8a1H0M1vhy3rhiMjl+e2iHmogyMw7jZF4FrE6eJUy5HQ=="], 808 827 ··· 838 857 839 858 "@typespec/compiler": ["@typespec/compiler@1.6.0", "", { "dependencies": { "@babel/code-frame": "~7.27.1", "@inquirer/prompts": "^7.4.0", "ajv": "~8.17.1", "change-case": "~5.4.4", "env-paths": "^3.0.0", "globby": "~15.0.0", "is-unicode-supported": "^2.1.0", "mustache": "~4.2.0", "picocolors": "~1.1.1", "prettier": "~3.6.2", "semver": "^7.7.1", "tar": "^7.5.2", "temporal-polyfill": "^0.3.0", "vscode-languageserver": "~9.0.1", "vscode-languageserver-textdocument": "~1.0.12", "yaml": "~2.8.0", "yargs": "~18.0.0" }, "bin": { "tsp": "cmd/tsp.js", "tsp-server": "cmd/tsp-server.js" } }, "sha512-yxyV+ch8tnqiuU2gClv/mQEESoFwpkjo6177UkYfV0nVA9PzTg4zVVc7+WIMZk04wiLRRT3H1uc11FB1cwLY3g=="], 840 859 860 + "@vitejs/plugin-react": ["@vitejs/plugin-react@5.1.1", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.47", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-WQfkSw0QbQ5aJ2CHYw23ZGkqnRwqKHD/KYsMeTkZzPT4Jcf0DcBxBtwMJxnu6E7oxw5+JC6ZAiePgh28uJ1HBA=="], 861 + 841 862 "@vitejs/plugin-react-swc": ["@vitejs/plugin-react-swc@3.11.0", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-beta.27", "@swc/core": "^1.12.11" }, "peerDependencies": { "vite": "^4 || ^5 || ^6 || ^7" } }, "sha512-YTJCGFdNMHCMfjODYtxRNVAYmTWQ1Lb8PulP/2/f/oEEtglw8oKxKIZmmRkyXrVrHfsKOaVkAc3NT9/dMutO5w=="], 842 863 843 864 "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="], ··· 877 898 "axios": ["axios@1.13.2", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA=="], 878 899 879 900 "babel-dead-code-elimination": ["babel-dead-code-elimination@1.0.10", "", { "dependencies": { "@babel/core": "^7.23.7", "@babel/parser": "^7.23.6", "@babel/traverse": "^7.23.7", "@babel/types": "^7.23.6" } }, "sha512-DV5bdJZTzZ0zn0DC24v3jD7Mnidh6xhKa4GfKCbq3sfW8kaWhDdZjP3i81geA8T33tdYqWKw4D3fVv0CwEgKVA=="], 901 + 902 + "babel-plugin-react-compiler": ["babel-plugin-react-compiler@1.0.0", "", { "dependencies": { "@babel/types": "^7.26.0" } }, "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw=="], 880 903 881 904 "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], 882 905 ··· 1152 1175 1153 1176 "help-me": ["help-me@5.0.0", "", {}, "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg=="], 1154 1177 1178 + "hono": ["hono@4.10.7", "", {}, "sha512-icXIITfw/07Q88nLSkB9aiUrd8rYzSweK681Kjo/TSggaGbOX4RRyxxm71v+3PC8C/j+4rlxGeoTRxQDkaJkUw=="], 1179 + 1155 1180 "iconv-lite": ["iconv-lite@0.7.0", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ=="], 1156 1181 1157 1182 "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], ··· 1314 1339 1315 1340 "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], 1316 1341 1342 + "pg": ["pg@8.16.3", "", { "dependencies": { "pg-connection-string": "^2.9.1", "pg-pool": "^3.10.1", "pg-protocol": "^1.10.3", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.2.7" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw=="], 1343 + 1344 + "pg-cloudflare": ["pg-cloudflare@1.2.7", "", {}, "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg=="], 1345 + 1346 + "pg-connection-string": ["pg-connection-string@2.9.1", "", {}, "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w=="], 1347 + 1317 1348 "pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="], 1349 + 1350 + "pg-pool": ["pg-pool@3.10.1", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg=="], 1318 1351 1319 1352 "pg-protocol": ["pg-protocol@1.10.3", "", {}, "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ=="], 1320 1353 1321 1354 "pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="], 1355 + 1356 + "pgpass": ["pgpass@1.0.5", "", { "dependencies": { "split2": "^4.1.0" } }, "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug=="], 1322 1357 1323 1358 "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], 1324 1359 ··· 1437 1472 "react-dom": ["react-dom@19.0.0", "", { "dependencies": { "scheduler": "^0.25.0" }, "peerDependencies": { "react": "^19.0.0" } }, "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ=="], 1438 1473 1439 1474 "react-hook-form": ["react-hook-form@7.66.1", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-2KnjpgG2Rhbi+CIiIBQQ9Df6sMGH5ExNyFl4Hw9qO7pIqMBR8Bvu9RQyjl3JM4vehzCh9soiNUM/xYMswb2EiA=="], 1475 + 1476 + "react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="], 1440 1477 1441 1478 "react-remove-scroll": ["react-remove-scroll@2.7.1", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA=="], 1442 1479 ··· 1608 1645 1609 1646 "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], 1610 1647 1611 - "vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="], 1648 + "vite": ["vite@7.2.4", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w=="], 1612 1649 1613 1650 "vscode-jsonrpc": ["vscode-jsonrpc@8.2.0", "", {}, "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA=="], 1614 1651 ··· 1652 1689 1653 1690 "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], 1654 1691 1655 - "@atcute/atproto/@atcute/lexicons": ["@atcute/lexicons@1.2.4", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "esm-env": "^1.2.2" } }, "sha512-s6fl/SVjQMv7jiitLCcZ434X+VrTsJt7Fl9iJg8WXHJIELRz/U0sNUoP++oWd7bvPy1Vcd2Wnm+YtTm/Zn7AIQ=="], 1656 - 1657 - "@atcute/bluesky/@atcute/lexicons": ["@atcute/lexicons@1.2.4", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "esm-env": "^1.2.2" } }, "sha512-s6fl/SVjQMv7jiitLCcZ434X+VrTsJt7Fl9iJg8WXHJIELRz/U0sNUoP++oWd7bvPy1Vcd2Wnm+YtTm/Zn7AIQ=="], 1658 - 1659 - "@atcute/client/@atcute/lexicons": ["@atcute/lexicons@1.2.4", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "esm-env": "^1.2.2" } }, "sha512-s6fl/SVjQMv7jiitLCcZ434X+VrTsJt7Fl9iJg8WXHJIELRz/U0sNUoP++oWd7bvPy1Vcd2Wnm+YtTm/Zn7AIQ=="], 1660 - 1661 - "@atcute/identity/@atcute/lexicons": ["@atcute/lexicons@1.2.4", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "esm-env": "^1.2.2" } }, "sha512-s6fl/SVjQMv7jiitLCcZ434X+VrTsJt7Fl9iJg8WXHJIELRz/U0sNUoP++oWd7bvPy1Vcd2Wnm+YtTm/Zn7AIQ=="], 1662 - 1663 - "@atcute/identity-resolver/@atcute/lexicons": ["@atcute/lexicons@1.2.4", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "esm-env": "^1.2.2" } }, "sha512-s6fl/SVjQMv7jiitLCcZ434X+VrTsJt7Fl9iJg8WXHJIELRz/U0sNUoP++oWd7bvPy1Vcd2Wnm+YtTm/Zn7AIQ=="], 1664 - 1665 - "@atcute/jetstream/@atcute/lexicons": ["@atcute/lexicons@1.2.4", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "esm-env": "^1.2.2" } }, "sha512-s6fl/SVjQMv7jiitLCcZ434X+VrTsJt7Fl9iJg8WXHJIELRz/U0sNUoP++oWd7bvPy1Vcd2Wnm+YtTm/Zn7AIQ=="], 1666 - 1667 - "@atcute/lexicon-doc/@atcute/lexicons": ["@atcute/lexicons@1.2.4", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "esm-env": "^1.2.2" } }, "sha512-s6fl/SVjQMv7jiitLCcZ434X+VrTsJt7Fl9iJg8WXHJIELRz/U0sNUoP++oWd7bvPy1Vcd2Wnm+YtTm/Zn7AIQ=="], 1668 - 1669 - "@atcute/oauth-browser-client/@atcute/lexicons": ["@atcute/lexicons@1.2.4", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "esm-env": "^1.2.2" } }, "sha512-s6fl/SVjQMv7jiitLCcZ434X+VrTsJt7Fl9iJg8WXHJIELRz/U0sNUoP++oWd7bvPy1Vcd2Wnm+YtTm/Zn7AIQ=="], 1670 - 1671 - "@atcute/xrpc-server/@atcute/lexicons": ["@atcute/lexicons@1.2.4", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "esm-env": "^1.2.2" } }, "sha512-s6fl/SVjQMv7jiitLCcZ434X+VrTsJt7Fl9iJg8WXHJIELRz/U0sNUoP++oWd7bvPy1Vcd2Wnm+YtTm/Zn7AIQ=="], 1672 - 1673 1692 "@atproto-labs/simple-store-memory/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], 1674 1693 1675 1694 "@atproto/common/@atproto/common-web": ["@atproto/common-web@0.4.3", "", { "dependencies": { "graphemer": "^1.4.0", "multiformats": "^9.9.0", "uint8arrays": "3.0.0", "zod": "^3.23.8" } }, "sha512-nRDINmSe4VycJzPo6fP/hEltBcULFxt9Kw7fQk6405FyAWZiTluYHlXOnU7GkQfeUK44OENG1qFTBcmCJ7e8pg=="], ··· 1687 1706 "@babel/helper-create-class-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], 1688 1707 1689 1708 "@cookware/database/@libsql/client": ["@libsql/client@0.15.15", "", { "dependencies": { "@libsql/core": "^0.15.14", "@libsql/hrana-client": "^0.7.0", "js-base64": "^3.7.5", "libsql": "^0.5.22", "promise-limit": "^2.7.0" } }, "sha512-twC0hQxPNHPKfeOv3sNT6u2pturQjLcI+CnpTM0SjRpocEGgfiZ7DWKXLNnsothjyJmDqEsBQJ5ztq9Wlu470w=="], 1690 - 1691 - "@cookware/lexicons/@atcute/bluesky": ["@atcute/bluesky@3.2.11", "", { "dependencies": { "@atcute/atproto": "^3.1.9", "@atcute/lexicons": "^1.2.5" } }, "sha512-AboS6y4t+zaxIq7E4noue10csSpIuk/Uwo30/l6GgGBDPXrd7STw8Yb5nGZQP+TdG/uC8/c2mm7UnY65SDOh6A=="], 1692 1709 1693 1710 "@cookware/web/@atcute/bluesky": ["@atcute/bluesky@1.0.15", "", { "peerDependencies": { "@atcute/client": "^1.0.0 || ^2.0.0" } }, "sha512-+EFiybmKQ97aBAgtaD+cKRJER5AMn3cZMkEwEg/pDdWyzxYJ9m1UgemmLdTgI8VrxPufKqdXS2nl7uO7TY6BPA=="], 1694 1711 ··· 1713 1730 "@opentelemetry/instrumentation-http/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="], 1714 1731 1715 1732 "@opentelemetry/instrumentation-pg/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.27.0", "", {}, "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg=="], 1733 + 1734 + "@opentelemetry/instrumentation-pg/@types/pg": ["@types/pg@8.6.1", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w=="], 1716 1735 1717 1736 "@opentelemetry/resources/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="], 1718 1737 ··· 1770 1789 1771 1790 "@radix-ui/react-visually-hidden/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], 1772 1791 1773 - "@tanstack/react-router-devtools/vite": ["vite@7.2.4", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w=="], 1774 - 1775 - "@tanstack/router-devtools/vite": ["vite@7.2.4", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w=="], 1776 - 1777 - "@tanstack/router-devtools-core/vite": ["vite@7.2.4", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w=="], 1792 + "@types/pg-pool/@types/pg": ["@types/pg@8.6.1", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w=="], 1778 1793 1779 1794 "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], 1780 1795 1781 1796 "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], 1782 1797 1798 + "@vitejs/plugin-react-swc/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="], 1799 + 1783 1800 "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], 1784 1801 1785 1802 "chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], ··· 1918 1935 1919 1936 "@radix-ui/react-visually-hidden/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], 1920 1937 1921 - "@tanstack/react-router-devtools/vite/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], 1922 - 1923 - "@tanstack/router-devtools-core/vite/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], 1924 - 1925 - "@tanstack/router-devtools/vite/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], 1926 - 1927 1938 "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], 1928 1939 1929 1940 "csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="], ··· 2039 2050 "@inquirer/core/wrap-ansi/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], 2040 2051 2041 2052 "@inquirer/core/wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], 2042 - 2043 - "@tanstack/react-router-devtools/vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], 2044 - 2045 - "@tanstack/react-router-devtools/vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], 2046 - 2047 - "@tanstack/react-router-devtools/vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], 2048 - 2049 - "@tanstack/react-router-devtools/vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], 2050 - 2051 - "@tanstack/react-router-devtools/vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], 2052 - 2053 - "@tanstack/react-router-devtools/vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], 2054 - 2055 - "@tanstack/react-router-devtools/vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], 2056 - 2057 - "@tanstack/react-router-devtools/vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], 2058 - 2059 - "@tanstack/react-router-devtools/vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], 2060 - 2061 - "@tanstack/react-router-devtools/vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], 2062 - 2063 - "@tanstack/react-router-devtools/vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], 2064 - 2065 - "@tanstack/react-router-devtools/vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], 2066 - 2067 - "@tanstack/react-router-devtools/vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], 2068 - 2069 - "@tanstack/react-router-devtools/vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], 2070 - 2071 - "@tanstack/react-router-devtools/vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], 2072 - 2073 - "@tanstack/react-router-devtools/vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], 2074 - 2075 - "@tanstack/react-router-devtools/vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], 2076 - 2077 - "@tanstack/react-router-devtools/vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], 2078 - 2079 - "@tanstack/react-router-devtools/vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], 2080 - 2081 - "@tanstack/react-router-devtools/vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], 2082 - 2083 - "@tanstack/react-router-devtools/vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], 2084 - 2085 - "@tanstack/react-router-devtools/vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], 2086 - 2087 - "@tanstack/react-router-devtools/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], 2088 - 2089 - "@tanstack/router-devtools-core/vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], 2090 - 2091 - "@tanstack/router-devtools-core/vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], 2092 - 2093 - "@tanstack/router-devtools-core/vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], 2094 - 2095 - "@tanstack/router-devtools-core/vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], 2096 - 2097 - "@tanstack/router-devtools-core/vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], 2098 - 2099 - "@tanstack/router-devtools-core/vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], 2100 - 2101 - "@tanstack/router-devtools-core/vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], 2102 - 2103 - "@tanstack/router-devtools-core/vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], 2104 - 2105 - "@tanstack/router-devtools-core/vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], 2106 - 2107 - "@tanstack/router-devtools-core/vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], 2108 - 2109 - "@tanstack/router-devtools-core/vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], 2110 - 2111 - "@tanstack/router-devtools-core/vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], 2112 - 2113 - "@tanstack/router-devtools-core/vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], 2114 - 2115 - "@tanstack/router-devtools-core/vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], 2116 - 2117 - "@tanstack/router-devtools-core/vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], 2118 - 2119 - "@tanstack/router-devtools-core/vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], 2120 - 2121 - "@tanstack/router-devtools-core/vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], 2122 - 2123 - "@tanstack/router-devtools-core/vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], 2124 - 2125 - "@tanstack/router-devtools-core/vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], 2126 - 2127 - "@tanstack/router-devtools-core/vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], 2128 - 2129 - "@tanstack/router-devtools-core/vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], 2130 - 2131 - "@tanstack/router-devtools-core/vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], 2132 - 2133 - "@tanstack/router-devtools-core/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], 2134 - 2135 - "@tanstack/router-devtools/vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], 2136 - 2137 - "@tanstack/router-devtools/vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], 2138 - 2139 - "@tanstack/router-devtools/vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], 2140 - 2141 - "@tanstack/router-devtools/vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], 2142 - 2143 - "@tanstack/router-devtools/vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], 2144 - 2145 - "@tanstack/router-devtools/vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], 2146 - 2147 - "@tanstack/router-devtools/vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], 2148 - 2149 - "@tanstack/router-devtools/vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], 2150 - 2151 - "@tanstack/router-devtools/vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], 2152 - 2153 - "@tanstack/router-devtools/vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], 2154 - 2155 - "@tanstack/router-devtools/vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], 2156 - 2157 - "@tanstack/router-devtools/vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], 2158 - 2159 - "@tanstack/router-devtools/vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], 2160 - 2161 - "@tanstack/router-devtools/vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], 2162 - 2163 - "@tanstack/router-devtools/vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], 2164 - 2165 - "@tanstack/router-devtools/vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], 2166 - 2167 - "@tanstack/router-devtools/vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], 2168 - 2169 - "@tanstack/router-devtools/vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], 2170 - 2171 - "@tanstack/router-devtools/vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], 2172 - 2173 - "@tanstack/router-devtools/vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], 2174 - 2175 - "@tanstack/router-devtools/vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], 2176 - 2177 - "@tanstack/router-devtools/vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], 2178 - 2179 - "@tanstack/router-devtools/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], 2180 2053 } 2181 2054 }
+13
config/dev/db/compose.yaml
··· 1 + --- 2 + volumes: 3 + postgres: {} 4 + 5 + services: 6 + postgres: 7 + image: postgres:18 8 + environment: 9 + POSTGRES_PASSWORD: postgres 10 + ports: 11 + - 5432:5432 12 + volumes: 13 + - postgres:/var/lib/postgresql/18/docker
-13
config/dev/libsql/compose.yaml
··· 1 - --- 2 - volumes: 3 - libsql: {} 4 - 5 - services: 6 - libsql: 7 - image: ghcr.io/tursodatabase/libsql-server:latest 8 - environment: 9 - SQLD_NODE: primary 10 - ports: 11 - - 4001:8080 12 - volumes: 13 - - libsql:/var/lib/sqld
+12
config/dev/redis/compose.yaml
··· 1 + --- 2 + volumes: 3 + redis: 4 + 5 + services: 6 + redis: 7 + image: redis:8 8 + command: redis-server --save 60 1 --loglevel warning 9 + ports: 10 + - 6379:6379 11 + volumes: 12 + - redis:/data
+2 -32
docker-compose.yaml
··· 1 1 --- 2 2 include: 3 - - path: config/dev/caddy/compose.yaml 4 - - path: config/dev/libsql/compose.yaml 3 + - path: config/dev/db/compose.yaml 4 + - path: config/dev/redis/compose.yaml 5 5 6 6 networks: 7 7 recipesblue: 8 - 9 - services: 10 - redis: 11 - image: redis:8 12 - ports: [6379:6379] 13 - 14 - api: 15 - build: 16 - context: . 17 - dockerfile: apps/api/Dockerfile 18 - restart: unless-stopped 19 - networks: [recipesblue] 20 - ports: 21 - - "3000:3000" 22 - environment: 23 - - DATABASE_URL=http://libsql:8080 24 - - PORT=3000 25 - depends_on: 26 - - libsql 27 - 28 - ingester: 29 - build: 30 - context: . 31 - dockerfile: apps/ingester/Dockerfile 32 - restart: unless-stopped 33 - networks: [recipesblue] 34 - environment: 35 - - DATABASE_URL=http://libsql:8080 36 - depends_on: 37 - - libsql
+2 -3
libs/database/drizzle.config.ts
··· 3 3 export default { 4 4 schema: "./lib/schema.ts", 5 5 out: "./migrations", 6 - dialect: "turso", 6 + dialect: "postgresql", 7 7 dbCredentials: { 8 - url: process.env.DATABASE_URL || "http://localhost:4001", 9 - authToken: process.env.DATABASE_AUTH_TOKEN 8 + url: process.env.DATABASE_URL || 'postgres://postgres:postgres@localhost:5432/postgres', 10 9 } 11 10 } satisfies Config;
+5 -6
libs/database/lib/index.ts
··· 1 - import { drizzle } from 'drizzle-orm/libsql'; 2 - import { createClient } from '@libsql/client'; 1 + import { drizzle } from 'drizzle-orm/node-postgres'; 2 + import { Pool } from 'pg'; 3 3 4 - const client = createClient({ 5 - url: process.env.TURSO_CONNECTION_URL || 'http://localhost:4001', 6 - authToken: process.env.TURSO_AUTH_TOKEN || '', 4 + const pool = new Pool({ 5 + connectionString: process.env.DATABASE_URL || 'postgres://postgres:postgres@localhost:5432/postgres', 7 6 }); 8 7 9 8 import * as schema from './schema.js'; 10 - export const db = drizzle(client, { schema }); 9 + export const db = drizzle(pool, { schema }); 11 10 12 11 // Re-export drizzle-orm functions to ensure single instance 13 12 export { eq, and, or, desc, asc, sql } from 'drizzle-orm';
+14 -13
libs/database/lib/schema.ts
··· 1 - import { customType, index, int, primaryKey, sqliteTable, text } from "drizzle-orm/sqlite-core"; 2 - import { BlueRecipesFeedRecipe, BlueRecipesActorProfile } from "@cookware/lexicons"; 1 + import { customType, index, integer, primaryKey, pgTable, text, jsonb, varchar } from "drizzle-orm/pg-core"; 2 + import { BlueRecipesFeedRecipe } from "@cookware/lexicons"; 3 3 import { Cid, isCid, ResourceUri, type AtprotoDid } from "@atcute/lexicons/syntax"; 4 4 import { Blob, LegacyBlob } from "@atcute/lexicons"; 5 5 import { relations, sql, type SQL } from "drizzle-orm"; ··· 53 53 }, 54 54 }); 55 55 56 - export const profilesTable = sqliteTable("profiles", { 56 + export const profilesTable = pgTable("profiles", { 57 57 uri: text('uri') 58 58 .generatedAlwaysAs((): SQL => sql`'at://' || ${profilesTable.did} || '/blue.recipes.actor.profile/self'`) 59 59 .$type<ResourceUri>(), 60 + 60 61 cid: text("cid").$type<Cid>().notNull(), 61 62 did: text("did").$type<AtprotoDid>().notNull().primaryKey(), 62 63 ingestedAt: dateIsoText("ingested_at").notNull().default(sql`CURRENT_TIMESTAMP`), 63 64 64 - displayName: text('display_name', { length: 640 }).notNull(), 65 - description: text('description', { length: 2500 }), 66 - pronouns: text('pronouns', { length: 200 }), 65 + displayName: varchar('display_name', { length: 640 }).notNull(), 66 + description: varchar('description', { length: 2500 }), 67 + pronouns: varchar('pronouns', { length: 200 }), 67 68 website: text('website'), 68 69 avatarRef: atBlob('avatar'), 69 70 bannerRef: atBlob('banner'), ··· 74 75 index('profiles_iat_idx').on(t.ingestedAt), 75 76 ])); 76 77 77 - export const recipeTable = sqliteTable("recipes", { 78 + export const recipeTable = pgTable("recipes", { 78 79 uri: text('uri') 79 80 .generatedAlwaysAs((): SQL => sql`'at://' || ${recipeTable.did} || '/blue.recipes.feed.recipe/' || ${recipeTable.rkey}`), 80 81 ··· 88 89 imageRef: atBlob('image'), 89 90 90 91 title: text('title').notNull(), 91 - time: int('time').notNull().default(0), 92 - serves: int('serves'), 92 + time: integer('time').notNull().default(0), 93 + serves: integer('serves'), 93 94 description: text('description'), 94 95 95 - ingredients: text('ingredients', { mode: 'json' }).$type<BlueRecipesFeedRecipe.Main['ingredients']>().notNull(), 96 - ingredientsCount: int('ingredients_count').generatedAlwaysAs((): SQL => sql`json_array_length(${recipeTable.ingredients})`), 96 + ingredients: jsonb('ingredients').$type<BlueRecipesFeedRecipe.Main['ingredients']>().notNull(), 97 + ingredientsCount: integer('ingredients_count').generatedAlwaysAs((): SQL => sql`jsonb_array_length(${recipeTable.ingredients})`), 97 98 98 - steps: text('steps', { mode: 'json' }).$type<BlueRecipesFeedRecipe.Main['steps']>().notNull(), 99 - stepsCount: int('steps_count').generatedAlwaysAs((): SQL => sql`json_array_length(${recipeTable.steps})`), 99 + steps: jsonb('steps').$type<BlueRecipesFeedRecipe.Main['steps']>().notNull(), 100 + stepsCount: integer('steps_count').generatedAlwaysAs((): SQL => sql`jsonb_array_length(${recipeTable.steps})`), 100 101 101 102 createdAt: dateIsoText("created_at").notNull(), 102 103 ingestedAt: dateIsoText("ingested_at").notNull().default(sql`CURRENT_TIMESTAMP`),
-16
libs/database/migrations/0000_kind_ultron.sql
··· 1 - CREATE TABLE `recipes` ( 2 - `uri` text GENERATED ALWAYS AS ("author_did" || '/' || "rkey") VIRTUAL, 3 - `author_did` text NOT NULL, 4 - `rkey` text NOT NULL, 5 - `image_ref` text, 6 - `title` text NOT NULL, 7 - `time` integer DEFAULT 0 NOT NULL, 8 - `serves` integer, 9 - `description` text, 10 - `ingredients` text NOT NULL, 11 - `ingredients_count` integer GENERATED ALWAYS AS (json_array_length("ingredients")) VIRTUAL, 12 - `steps` text NOT NULL, 13 - `steps_count` integer GENERATED ALWAYS AS (json_array_length("steps")) VIRTUAL, 14 - `created_at` text NOT NULL, 15 - PRIMARY KEY(`author_did`, `rkey`) 16 - );
+46
libs/database/migrations/0000_young_hellcat.sql
··· 1 + CREATE TABLE IF NOT EXISTS "profiles" ( 2 + "uri" text GENERATED ALWAYS AS ('at://' || "profiles"."did" || '/blue.recipes.actor.profile/self') STORED, 3 + "cid" text NOT NULL, 4 + "did" text PRIMARY KEY NOT NULL, 5 + "ingested_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL, 6 + "display_name" varchar(640) NOT NULL, 7 + "description" varchar(2500), 8 + "pronouns" varchar(200), 9 + "website" text, 10 + "avatar" text, 11 + "banner" text, 12 + "created_at" text NOT NULL 13 + ); 14 + --> statement-breakpoint 15 + CREATE TABLE IF NOT EXISTS "recipes" ( 16 + "uri" text GENERATED ALWAYS AS ('at://' || "recipes"."author_did" || '/blue.recipes.feed.recipe/' || "recipes"."rkey") STORED, 17 + "cid" text NOT NULL, 18 + "author_did" text NOT NULL, 19 + "rkey" text NOT NULL, 20 + "image" text, 21 + "title" text NOT NULL, 22 + "time" integer DEFAULT 0 NOT NULL, 23 + "serves" integer, 24 + "description" text, 25 + "ingredients" jsonb NOT NULL, 26 + "ingredients_count" integer GENERATED ALWAYS AS (jsonb_array_length("recipes"."ingredients")) STORED, 27 + "steps" jsonb NOT NULL, 28 + "steps_count" integer GENERATED ALWAYS AS (jsonb_array_length("recipes"."steps")) STORED, 29 + "created_at" text NOT NULL, 30 + "ingested_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL, 31 + CONSTRAINT "recipes_author_did_rkey_pk" PRIMARY KEY("author_did","rkey") 32 + ); 33 + --> statement-breakpoint 34 + DO $$ BEGIN 35 + ALTER TABLE "recipes" ADD CONSTRAINT "recipes_author_did_profiles_did_fk" FOREIGN KEY ("author_did") REFERENCES "public"."profiles"("did") ON DELETE cascade ON UPDATE no action; 36 + EXCEPTION 37 + WHEN duplicate_object THEN null; 38 + END $$; 39 + --> statement-breakpoint 40 + CREATE INDEX IF NOT EXISTS "profiles_cid_idx" ON "profiles" USING btree ("cid");--> statement-breakpoint 41 + CREATE INDEX IF NOT EXISTS "profiles_cat_idx" ON "profiles" USING btree ("created_at");--> statement-breakpoint 42 + CREATE INDEX IF NOT EXISTS "profiles_iat_idx" ON "profiles" USING btree ("ingested_at");--> statement-breakpoint 43 + CREATE INDEX IF NOT EXISTS "recipes_title_idx" ON "recipes" USING btree ("title");--> statement-breakpoint 44 + CREATE INDEX IF NOT EXISTS "recipes_cid_idx" ON "recipes" USING btree ("cid");--> statement-breakpoint 45 + CREATE INDEX IF NOT EXISTS "recipes_cat_idx" ON "recipes" USING btree ("created_at");--> statement-breakpoint 46 + CREATE INDEX IF NOT EXISTS "recipes_iat_idx" ON "recipes" USING btree ("ingested_at");
-23
libs/database/migrations/0001_past_umar.sql
··· 1 - ALTER TABLE `recipes` RENAME COLUMN "image_ref" TO "image";--> statement-breakpoint 2 - CREATE TABLE `profiles` ( 3 - `uri` text GENERATED ALWAYS AS ('at://' || "did" || '/?/self') VIRTUAL, 4 - `did` text PRIMARY KEY NOT NULL, 5 - `ingested_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, 6 - `display_name` text(640) NOT NULL, 7 - `description` text(2500), 8 - `pronouns` text(200), 9 - `website` text, 10 - `avatar` text, 11 - `banner` text, 12 - `created_at` text NOT NULL 13 - ); 14 - --> statement-breakpoint 15 - CREATE INDEX `profiles_cat_idx` ON `profiles` (`created_at`);--> statement-breakpoint 16 - CREATE INDEX `profiles_iat_idx` ON `profiles` (`ingested_at`);--> statement-breakpoint 17 - ALTER TABLE `recipes` DROP COLUMN `uri`;--> statement-breakpoint 18 - ALTER TABLE `recipes` ADD `uri` text GENERATED ALWAYS AS ('at://' || "author_did" || '/?/' || "rkey") VIRTUAL;--> statement-breakpoint 19 - ALTER TABLE `recipes` ADD `ingested_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL;--> statement-breakpoint 20 - CREATE INDEX `recipes_title_idx` ON `recipes` (`title`);--> statement-breakpoint 21 - CREATE INDEX `recipes_cat_idx` ON `recipes` (`created_at`);--> statement-breakpoint 22 - CREATE INDEX `recipes_iat_idx` ON `recipes` (`ingested_at`);--> statement-breakpoint 23 - ALTER TABLE `recipes` ALTER COLUMN "author_did" TO "author_did" text NOT NULL REFERENCES profiles(did) ON DELETE cascade ON UPDATE no action;
-4
libs/database/migrations/0002_cheerful_venom.sql
··· 1 - ALTER TABLE `profiles` DROP COLUMN `uri`;--> statement-breakpoint 2 - ALTER TABLE `profiles` ADD `uri` text GENERATED ALWAYS AS ('at://' || "did" || '/blue.recipes.actor.profile/self') VIRTUAL;--> statement-breakpoint 3 - ALTER TABLE `recipes` DROP COLUMN `uri`;--> statement-breakpoint 4 - ALTER TABLE `recipes` ADD `uri` text GENERATED ALWAYS AS ('at://' || "author_did" || '/blue.recipes.feed.recipe/' || "rkey") VIRTUAL;
-4
libs/database/migrations/0003_long_blue_marvel.sql
··· 1 - ALTER TABLE `profiles` ADD `cid` text NOT NULL;--> statement-breakpoint 2 - CREATE INDEX `profiles_cid_idx` ON `profiles` (`cid`);--> statement-breakpoint 3 - ALTER TABLE `recipes` ADD `cid` text NOT NULL;--> statement-breakpoint 4 - CREATE INDEX `recipes_cid_idx` ON `recipes` (`cid`);
+255 -46
libs/database/migrations/meta/0000_snapshot.json
··· 1 1 { 2 - "version": "6", 3 - "dialect": "sqlite", 4 - "id": "7b2675f9-5d97-4fac-983e-978efd250faf", 2 + "id": "5d896b70-087b-421e-8d94-c100476cd926", 5 3 "prevId": "00000000-0000-0000-0000-000000000000", 4 + "version": "7", 5 + "dialect": "postgresql", 6 6 "tables": { 7 - "recipes": { 7 + "public.profiles": { 8 + "name": "profiles", 9 + "schema": "", 10 + "columns": { 11 + "uri": { 12 + "name": "uri", 13 + "type": "text", 14 + "primaryKey": false, 15 + "notNull": false, 16 + "generated": { 17 + "as": "'at://' || \"profiles\".\"did\" || '/blue.recipes.actor.profile/self'", 18 + "type": "stored" 19 + } 20 + }, 21 + "cid": { 22 + "name": "cid", 23 + "type": "text", 24 + "primaryKey": false, 25 + "notNull": true 26 + }, 27 + "did": { 28 + "name": "did", 29 + "type": "text", 30 + "primaryKey": true, 31 + "notNull": true 32 + }, 33 + "ingested_at": { 34 + "name": "ingested_at", 35 + "type": "text", 36 + "primaryKey": false, 37 + "notNull": true, 38 + "default": "CURRENT_TIMESTAMP" 39 + }, 40 + "display_name": { 41 + "name": "display_name", 42 + "type": "varchar(640)", 43 + "primaryKey": false, 44 + "notNull": true 45 + }, 46 + "description": { 47 + "name": "description", 48 + "type": "varchar(2500)", 49 + "primaryKey": false, 50 + "notNull": false 51 + }, 52 + "pronouns": { 53 + "name": "pronouns", 54 + "type": "varchar(200)", 55 + "primaryKey": false, 56 + "notNull": false 57 + }, 58 + "website": { 59 + "name": "website", 60 + "type": "text", 61 + "primaryKey": false, 62 + "notNull": false 63 + }, 64 + "avatar": { 65 + "name": "avatar", 66 + "type": "text", 67 + "primaryKey": false, 68 + "notNull": false 69 + }, 70 + "banner": { 71 + "name": "banner", 72 + "type": "text", 73 + "primaryKey": false, 74 + "notNull": false 75 + }, 76 + "created_at": { 77 + "name": "created_at", 78 + "type": "text", 79 + "primaryKey": false, 80 + "notNull": true 81 + } 82 + }, 83 + "indexes": { 84 + "profiles_cid_idx": { 85 + "name": "profiles_cid_idx", 86 + "columns": [ 87 + { 88 + "expression": "cid", 89 + "isExpression": false, 90 + "asc": true, 91 + "nulls": "last" 92 + } 93 + ], 94 + "isUnique": false, 95 + "concurrently": false, 96 + "method": "btree", 97 + "with": {} 98 + }, 99 + "profiles_cat_idx": { 100 + "name": "profiles_cat_idx", 101 + "columns": [ 102 + { 103 + "expression": "created_at", 104 + "isExpression": false, 105 + "asc": true, 106 + "nulls": "last" 107 + } 108 + ], 109 + "isUnique": false, 110 + "concurrently": false, 111 + "method": "btree", 112 + "with": {} 113 + }, 114 + "profiles_iat_idx": { 115 + "name": "profiles_iat_idx", 116 + "columns": [ 117 + { 118 + "expression": "ingested_at", 119 + "isExpression": false, 120 + "asc": true, 121 + "nulls": "last" 122 + } 123 + ], 124 + "isUnique": false, 125 + "concurrently": false, 126 + "method": "btree", 127 + "with": {} 128 + } 129 + }, 130 + "foreignKeys": {}, 131 + "compositePrimaryKeys": {}, 132 + "uniqueConstraints": {}, 133 + "policies": {}, 134 + "checkConstraints": {}, 135 + "isRLSEnabled": false 136 + }, 137 + "public.recipes": { 8 138 "name": "recipes", 139 + "schema": "", 9 140 "columns": { 10 141 "uri": { 11 142 "name": "uri", 12 143 "type": "text", 13 144 "primaryKey": false, 14 145 "notNull": false, 15 - "autoincrement": false, 16 146 "generated": { 17 - "as": "(\"author_did\" || '/' || \"rkey\")", 18 - "type": "virtual" 147 + "as": "'at://' || \"recipes\".\"author_did\" || '/blue.recipes.feed.recipe/' || \"recipes\".\"rkey\"", 148 + "type": "stored" 19 149 } 20 150 }, 151 + "cid": { 152 + "name": "cid", 153 + "type": "text", 154 + "primaryKey": false, 155 + "notNull": true 156 + }, 21 157 "author_did": { 22 158 "name": "author_did", 23 159 "type": "text", 24 160 "primaryKey": false, 25 - "notNull": true, 26 - "autoincrement": false 161 + "notNull": true 27 162 }, 28 163 "rkey": { 29 164 "name": "rkey", 30 165 "type": "text", 31 166 "primaryKey": false, 32 - "notNull": true, 33 - "autoincrement": false 167 + "notNull": true 34 168 }, 35 - "image_ref": { 36 - "name": "image_ref", 169 + "image": { 170 + "name": "image", 37 171 "type": "text", 38 172 "primaryKey": false, 39 - "notNull": false, 40 - "autoincrement": false 173 + "notNull": false 41 174 }, 42 175 "title": { 43 176 "name": "title", 44 177 "type": "text", 45 178 "primaryKey": false, 46 - "notNull": true, 47 - "autoincrement": false 179 + "notNull": true 48 180 }, 49 181 "time": { 50 182 "name": "time", 51 183 "type": "integer", 52 184 "primaryKey": false, 53 185 "notNull": true, 54 - "autoincrement": false, 55 186 "default": 0 56 187 }, 57 188 "serves": { 58 189 "name": "serves", 59 190 "type": "integer", 60 191 "primaryKey": false, 61 - "notNull": false, 62 - "autoincrement": false 192 + "notNull": false 63 193 }, 64 194 "description": { 65 195 "name": "description", 66 196 "type": "text", 67 197 "primaryKey": false, 68 - "notNull": false, 69 - "autoincrement": false 198 + "notNull": false 70 199 }, 71 200 "ingredients": { 72 201 "name": "ingredients", 73 - "type": "text", 202 + "type": "jsonb", 74 203 "primaryKey": false, 75 - "notNull": true, 76 - "autoincrement": false 204 + "notNull": true 77 205 }, 78 206 "ingredients_count": { 79 207 "name": "ingredients_count", 80 208 "type": "integer", 81 209 "primaryKey": false, 82 210 "notNull": false, 83 - "autoincrement": false, 84 211 "generated": { 85 - "as": "(json_array_length(\"ingredients\"))", 86 - "type": "virtual" 212 + "as": "jsonb_array_length(\"recipes\".\"ingredients\")", 213 + "type": "stored" 87 214 } 88 215 }, 89 216 "steps": { 90 217 "name": "steps", 91 - "type": "text", 218 + "type": "jsonb", 92 219 "primaryKey": false, 93 - "notNull": true, 94 - "autoincrement": false 220 + "notNull": true 95 221 }, 96 222 "steps_count": { 97 223 "name": "steps_count", 98 224 "type": "integer", 99 225 "primaryKey": false, 100 226 "notNull": false, 101 - "autoincrement": false, 102 227 "generated": { 103 - "as": "(json_array_length(\"steps\"))", 104 - "type": "virtual" 228 + "as": "jsonb_array_length(\"recipes\".\"steps\")", 229 + "type": "stored" 105 230 } 106 231 }, 107 232 "created_at": { 108 233 "name": "created_at", 234 + "type": "text", 235 + "primaryKey": false, 236 + "notNull": true 237 + }, 238 + "ingested_at": { 239 + "name": "ingested_at", 109 240 "type": "text", 110 241 "primaryKey": false, 111 242 "notNull": true, 112 - "autoincrement": false 243 + "default": "CURRENT_TIMESTAMP" 113 244 } 114 245 }, 115 - "indexes": {}, 116 - "foreignKeys": {}, 246 + "indexes": { 247 + "recipes_title_idx": { 248 + "name": "recipes_title_idx", 249 + "columns": [ 250 + { 251 + "expression": "title", 252 + "isExpression": false, 253 + "asc": true, 254 + "nulls": "last" 255 + } 256 + ], 257 + "isUnique": false, 258 + "concurrently": false, 259 + "method": "btree", 260 + "with": {} 261 + }, 262 + "recipes_cid_idx": { 263 + "name": "recipes_cid_idx", 264 + "columns": [ 265 + { 266 + "expression": "cid", 267 + "isExpression": false, 268 + "asc": true, 269 + "nulls": "last" 270 + } 271 + ], 272 + "isUnique": false, 273 + "concurrently": false, 274 + "method": "btree", 275 + "with": {} 276 + }, 277 + "recipes_cat_idx": { 278 + "name": "recipes_cat_idx", 279 + "columns": [ 280 + { 281 + "expression": "created_at", 282 + "isExpression": false, 283 + "asc": true, 284 + "nulls": "last" 285 + } 286 + ], 287 + "isUnique": false, 288 + "concurrently": false, 289 + "method": "btree", 290 + "with": {} 291 + }, 292 + "recipes_iat_idx": { 293 + "name": "recipes_iat_idx", 294 + "columns": [ 295 + { 296 + "expression": "ingested_at", 297 + "isExpression": false, 298 + "asc": true, 299 + "nulls": "last" 300 + } 301 + ], 302 + "isUnique": false, 303 + "concurrently": false, 304 + "method": "btree", 305 + "with": {} 306 + } 307 + }, 308 + "foreignKeys": { 309 + "recipes_author_did_profiles_did_fk": { 310 + "name": "recipes_author_did_profiles_did_fk", 311 + "tableFrom": "recipes", 312 + "tableTo": "profiles", 313 + "columnsFrom": [ 314 + "author_did" 315 + ], 316 + "columnsTo": [ 317 + "did" 318 + ], 319 + "onDelete": "cascade", 320 + "onUpdate": "no action" 321 + } 322 + }, 117 323 "compositePrimaryKeys": { 118 324 "recipes_author_did_rkey_pk": { 325 + "name": "recipes_author_did_rkey_pk", 119 326 "columns": [ 120 327 "author_did", 121 328 "rkey" 122 - ], 123 - "name": "recipes_author_did_rkey_pk" 329 + ] 124 330 } 125 331 }, 126 332 "uniqueConstraints": {}, 127 - "checkConstraints": {} 333 + "policies": {}, 334 + "checkConstraints": {}, 335 + "isRLSEnabled": false 128 336 } 129 337 }, 130 - "views": {}, 131 338 "enums": {}, 339 + "schemas": {}, 340 + "sequences": {}, 341 + "roles": {}, 342 + "policies": {}, 343 + "views": {}, 132 344 "_meta": { 345 + "columns": {}, 133 346 "schemas": {}, 134 - "tables": {}, 135 - "columns": {} 136 - }, 137 - "internal": { 138 - "indexes": {} 347 + "tables": {} 139 348 } 140 349 }
-286
libs/database/migrations/meta/0001_snapshot.json
··· 1 - { 2 - "version": "6", 3 - "dialect": "sqlite", 4 - "id": "d6f06b7d-9822-43ee-b96c-3b980a5e4953", 5 - "prevId": "7b2675f9-5d97-4fac-983e-978efd250faf", 6 - "tables": { 7 - "profiles": { 8 - "name": "profiles", 9 - "columns": { 10 - "uri": { 11 - "name": "uri", 12 - "type": "text", 13 - "primaryKey": false, 14 - "notNull": false, 15 - "autoincrement": false, 16 - "generated": { 17 - "as": "('at://' || \"did\" || '/?/self')", 18 - "type": "virtual" 19 - } 20 - }, 21 - "did": { 22 - "name": "did", 23 - "type": "text", 24 - "primaryKey": true, 25 - "notNull": true, 26 - "autoincrement": false 27 - }, 28 - "ingested_at": { 29 - "name": "ingested_at", 30 - "type": "text", 31 - "primaryKey": false, 32 - "notNull": true, 33 - "autoincrement": false, 34 - "default": "CURRENT_TIMESTAMP" 35 - }, 36 - "display_name": { 37 - "name": "display_name", 38 - "type": "text(640)", 39 - "primaryKey": false, 40 - "notNull": true, 41 - "autoincrement": false 42 - }, 43 - "description": { 44 - "name": "description", 45 - "type": "text(2500)", 46 - "primaryKey": false, 47 - "notNull": false, 48 - "autoincrement": false 49 - }, 50 - "pronouns": { 51 - "name": "pronouns", 52 - "type": "text(200)", 53 - "primaryKey": false, 54 - "notNull": false, 55 - "autoincrement": false 56 - }, 57 - "website": { 58 - "name": "website", 59 - "type": "text", 60 - "primaryKey": false, 61 - "notNull": false, 62 - "autoincrement": false 63 - }, 64 - "avatar": { 65 - "name": "avatar", 66 - "type": "text", 67 - "primaryKey": false, 68 - "notNull": false, 69 - "autoincrement": false 70 - }, 71 - "banner": { 72 - "name": "banner", 73 - "type": "text", 74 - "primaryKey": false, 75 - "notNull": false, 76 - "autoincrement": false 77 - }, 78 - "created_at": { 79 - "name": "created_at", 80 - "type": "text", 81 - "primaryKey": false, 82 - "notNull": true, 83 - "autoincrement": false 84 - } 85 - }, 86 - "indexes": { 87 - "profiles_cat_idx": { 88 - "name": "profiles_cat_idx", 89 - "columns": [ 90 - "created_at" 91 - ], 92 - "isUnique": false 93 - }, 94 - "profiles_iat_idx": { 95 - "name": "profiles_iat_idx", 96 - "columns": [ 97 - "ingested_at" 98 - ], 99 - "isUnique": false 100 - } 101 - }, 102 - "foreignKeys": {}, 103 - "compositePrimaryKeys": {}, 104 - "uniqueConstraints": {}, 105 - "checkConstraints": {} 106 - }, 107 - "recipes": { 108 - "name": "recipes", 109 - "columns": { 110 - "uri": { 111 - "name": "uri", 112 - "type": "text", 113 - "primaryKey": false, 114 - "notNull": false, 115 - "autoincrement": false, 116 - "generated": { 117 - "as": "('at://' || \"author_did\" || '/?/' || \"rkey\")", 118 - "type": "virtual" 119 - } 120 - }, 121 - "author_did": { 122 - "name": "author_did", 123 - "type": "text", 124 - "primaryKey": false, 125 - "notNull": true, 126 - "autoincrement": false 127 - }, 128 - "rkey": { 129 - "name": "rkey", 130 - "type": "text", 131 - "primaryKey": false, 132 - "notNull": true, 133 - "autoincrement": false 134 - }, 135 - "image": { 136 - "name": "image", 137 - "type": "text", 138 - "primaryKey": false, 139 - "notNull": false, 140 - "autoincrement": false 141 - }, 142 - "title": { 143 - "name": "title", 144 - "type": "text", 145 - "primaryKey": false, 146 - "notNull": true, 147 - "autoincrement": false 148 - }, 149 - "time": { 150 - "name": "time", 151 - "type": "integer", 152 - "primaryKey": false, 153 - "notNull": true, 154 - "autoincrement": false, 155 - "default": 0 156 - }, 157 - "serves": { 158 - "name": "serves", 159 - "type": "integer", 160 - "primaryKey": false, 161 - "notNull": false, 162 - "autoincrement": false 163 - }, 164 - "description": { 165 - "name": "description", 166 - "type": "text", 167 - "primaryKey": false, 168 - "notNull": false, 169 - "autoincrement": false 170 - }, 171 - "ingredients": { 172 - "name": "ingredients", 173 - "type": "text", 174 - "primaryKey": false, 175 - "notNull": true, 176 - "autoincrement": false 177 - }, 178 - "ingredients_count": { 179 - "name": "ingredients_count", 180 - "type": "integer", 181 - "primaryKey": false, 182 - "notNull": false, 183 - "autoincrement": false, 184 - "generated": { 185 - "as": "(json_array_length(\"ingredients\"))", 186 - "type": "virtual" 187 - } 188 - }, 189 - "steps": { 190 - "name": "steps", 191 - "type": "text", 192 - "primaryKey": false, 193 - "notNull": true, 194 - "autoincrement": false 195 - }, 196 - "steps_count": { 197 - "name": "steps_count", 198 - "type": "integer", 199 - "primaryKey": false, 200 - "notNull": false, 201 - "autoincrement": false, 202 - "generated": { 203 - "as": "(json_array_length(\"steps\"))", 204 - "type": "virtual" 205 - } 206 - }, 207 - "created_at": { 208 - "name": "created_at", 209 - "type": "text", 210 - "primaryKey": false, 211 - "notNull": true, 212 - "autoincrement": false 213 - }, 214 - "ingested_at": { 215 - "name": "ingested_at", 216 - "type": "text", 217 - "primaryKey": false, 218 - "notNull": true, 219 - "autoincrement": false, 220 - "default": "CURRENT_TIMESTAMP" 221 - } 222 - }, 223 - "indexes": { 224 - "recipes_title_idx": { 225 - "name": "recipes_title_idx", 226 - "columns": [ 227 - "title" 228 - ], 229 - "isUnique": false 230 - }, 231 - "recipes_cat_idx": { 232 - "name": "recipes_cat_idx", 233 - "columns": [ 234 - "created_at" 235 - ], 236 - "isUnique": false 237 - }, 238 - "recipes_iat_idx": { 239 - "name": "recipes_iat_idx", 240 - "columns": [ 241 - "ingested_at" 242 - ], 243 - "isUnique": false 244 - } 245 - }, 246 - "foreignKeys": { 247 - "recipes_author_did_profiles_did_fk": { 248 - "name": "recipes_author_did_profiles_did_fk", 249 - "tableFrom": "recipes", 250 - "tableTo": "profiles", 251 - "columnsFrom": [ 252 - "author_did" 253 - ], 254 - "columnsTo": [ 255 - "did" 256 - ], 257 - "onDelete": "cascade", 258 - "onUpdate": "no action" 259 - } 260 - }, 261 - "compositePrimaryKeys": { 262 - "recipes_author_did_rkey_pk": { 263 - "columns": [ 264 - "author_did", 265 - "rkey" 266 - ], 267 - "name": "recipes_author_did_rkey_pk" 268 - } 269 - }, 270 - "uniqueConstraints": {}, 271 - "checkConstraints": {} 272 - } 273 - }, 274 - "views": {}, 275 - "enums": {}, 276 - "_meta": { 277 - "schemas": {}, 278 - "tables": {}, 279 - "columns": { 280 - "\"recipes\".\"image_ref\"": "\"recipes\".\"image\"" 281 - } 282 - }, 283 - "internal": { 284 - "indexes": {} 285 - } 286 - }
-284
libs/database/migrations/meta/0002_snapshot.json
··· 1 - { 2 - "version": "6", 3 - "dialect": "sqlite", 4 - "id": "25f6fc02-0357-4a4a-a43c-6fc138a21401", 5 - "prevId": "d6f06b7d-9822-43ee-b96c-3b980a5e4953", 6 - "tables": { 7 - "profiles": { 8 - "name": "profiles", 9 - "columns": { 10 - "uri": { 11 - "name": "uri", 12 - "type": "text", 13 - "primaryKey": false, 14 - "notNull": false, 15 - "autoincrement": false, 16 - "generated": { 17 - "as": "('at://' || \"did\" || '/blue.recipes.actor.profile/self')", 18 - "type": "virtual" 19 - } 20 - }, 21 - "did": { 22 - "name": "did", 23 - "type": "text", 24 - "primaryKey": true, 25 - "notNull": true, 26 - "autoincrement": false 27 - }, 28 - "ingested_at": { 29 - "name": "ingested_at", 30 - "type": "text", 31 - "primaryKey": false, 32 - "notNull": true, 33 - "autoincrement": false, 34 - "default": "CURRENT_TIMESTAMP" 35 - }, 36 - "display_name": { 37 - "name": "display_name", 38 - "type": "text(640)", 39 - "primaryKey": false, 40 - "notNull": true, 41 - "autoincrement": false 42 - }, 43 - "description": { 44 - "name": "description", 45 - "type": "text(2500)", 46 - "primaryKey": false, 47 - "notNull": false, 48 - "autoincrement": false 49 - }, 50 - "pronouns": { 51 - "name": "pronouns", 52 - "type": "text(200)", 53 - "primaryKey": false, 54 - "notNull": false, 55 - "autoincrement": false 56 - }, 57 - "website": { 58 - "name": "website", 59 - "type": "text", 60 - "primaryKey": false, 61 - "notNull": false, 62 - "autoincrement": false 63 - }, 64 - "avatar": { 65 - "name": "avatar", 66 - "type": "text", 67 - "primaryKey": false, 68 - "notNull": false, 69 - "autoincrement": false 70 - }, 71 - "banner": { 72 - "name": "banner", 73 - "type": "text", 74 - "primaryKey": false, 75 - "notNull": false, 76 - "autoincrement": false 77 - }, 78 - "created_at": { 79 - "name": "created_at", 80 - "type": "text", 81 - "primaryKey": false, 82 - "notNull": true, 83 - "autoincrement": false 84 - } 85 - }, 86 - "indexes": { 87 - "profiles_cat_idx": { 88 - "name": "profiles_cat_idx", 89 - "columns": [ 90 - "created_at" 91 - ], 92 - "isUnique": false 93 - }, 94 - "profiles_iat_idx": { 95 - "name": "profiles_iat_idx", 96 - "columns": [ 97 - "ingested_at" 98 - ], 99 - "isUnique": false 100 - } 101 - }, 102 - "foreignKeys": {}, 103 - "compositePrimaryKeys": {}, 104 - "uniqueConstraints": {}, 105 - "checkConstraints": {} 106 - }, 107 - "recipes": { 108 - "name": "recipes", 109 - "columns": { 110 - "uri": { 111 - "name": "uri", 112 - "type": "text", 113 - "primaryKey": false, 114 - "notNull": false, 115 - "autoincrement": false, 116 - "generated": { 117 - "as": "('at://' || \"author_did\" || '/blue.recipes.feed.recipe/' || \"rkey\")", 118 - "type": "virtual" 119 - } 120 - }, 121 - "author_did": { 122 - "name": "author_did", 123 - "type": "text", 124 - "primaryKey": false, 125 - "notNull": true, 126 - "autoincrement": false 127 - }, 128 - "rkey": { 129 - "name": "rkey", 130 - "type": "text", 131 - "primaryKey": false, 132 - "notNull": true, 133 - "autoincrement": false 134 - }, 135 - "image": { 136 - "name": "image", 137 - "type": "text", 138 - "primaryKey": false, 139 - "notNull": false, 140 - "autoincrement": false 141 - }, 142 - "title": { 143 - "name": "title", 144 - "type": "text", 145 - "primaryKey": false, 146 - "notNull": true, 147 - "autoincrement": false 148 - }, 149 - "time": { 150 - "name": "time", 151 - "type": "integer", 152 - "primaryKey": false, 153 - "notNull": true, 154 - "autoincrement": false, 155 - "default": 0 156 - }, 157 - "serves": { 158 - "name": "serves", 159 - "type": "integer", 160 - "primaryKey": false, 161 - "notNull": false, 162 - "autoincrement": false 163 - }, 164 - "description": { 165 - "name": "description", 166 - "type": "text", 167 - "primaryKey": false, 168 - "notNull": false, 169 - "autoincrement": false 170 - }, 171 - "ingredients": { 172 - "name": "ingredients", 173 - "type": "text", 174 - "primaryKey": false, 175 - "notNull": true, 176 - "autoincrement": false 177 - }, 178 - "ingredients_count": { 179 - "name": "ingredients_count", 180 - "type": "integer", 181 - "primaryKey": false, 182 - "notNull": false, 183 - "autoincrement": false, 184 - "generated": { 185 - "as": "(json_array_length(\"ingredients\"))", 186 - "type": "virtual" 187 - } 188 - }, 189 - "steps": { 190 - "name": "steps", 191 - "type": "text", 192 - "primaryKey": false, 193 - "notNull": true, 194 - "autoincrement": false 195 - }, 196 - "steps_count": { 197 - "name": "steps_count", 198 - "type": "integer", 199 - "primaryKey": false, 200 - "notNull": false, 201 - "autoincrement": false, 202 - "generated": { 203 - "as": "(json_array_length(\"steps\"))", 204 - "type": "virtual" 205 - } 206 - }, 207 - "created_at": { 208 - "name": "created_at", 209 - "type": "text", 210 - "primaryKey": false, 211 - "notNull": true, 212 - "autoincrement": false 213 - }, 214 - "ingested_at": { 215 - "name": "ingested_at", 216 - "type": "text", 217 - "primaryKey": false, 218 - "notNull": true, 219 - "autoincrement": false, 220 - "default": "CURRENT_TIMESTAMP" 221 - } 222 - }, 223 - "indexes": { 224 - "recipes_title_idx": { 225 - "name": "recipes_title_idx", 226 - "columns": [ 227 - "title" 228 - ], 229 - "isUnique": false 230 - }, 231 - "recipes_cat_idx": { 232 - "name": "recipes_cat_idx", 233 - "columns": [ 234 - "created_at" 235 - ], 236 - "isUnique": false 237 - }, 238 - "recipes_iat_idx": { 239 - "name": "recipes_iat_idx", 240 - "columns": [ 241 - "ingested_at" 242 - ], 243 - "isUnique": false 244 - } 245 - }, 246 - "foreignKeys": { 247 - "recipes_author_did_profiles_did_fk": { 248 - "name": "recipes_author_did_profiles_did_fk", 249 - "tableFrom": "recipes", 250 - "tableTo": "profiles", 251 - "columnsFrom": [ 252 - "author_did" 253 - ], 254 - "columnsTo": [ 255 - "did" 256 - ], 257 - "onDelete": "cascade", 258 - "onUpdate": "no action" 259 - } 260 - }, 261 - "compositePrimaryKeys": { 262 - "recipes_author_did_rkey_pk": { 263 - "columns": [ 264 - "author_did", 265 - "rkey" 266 - ], 267 - "name": "recipes_author_did_rkey_pk" 268 - } 269 - }, 270 - "uniqueConstraints": {}, 271 - "checkConstraints": {} 272 - } 273 - }, 274 - "views": {}, 275 - "enums": {}, 276 - "_meta": { 277 - "schemas": {}, 278 - "tables": {}, 279 - "columns": {} 280 - }, 281 - "internal": { 282 - "indexes": {} 283 - } 284 - }
-312
libs/database/migrations/meta/0003_snapshot.json
··· 1 - { 2 - "version": "6", 3 - "dialect": "sqlite", 4 - "id": "ca3337d9-69a0-468d-8364-0f05e91a0233", 5 - "prevId": "25f6fc02-0357-4a4a-a43c-6fc138a21401", 6 - "tables": { 7 - "profiles": { 8 - "name": "profiles", 9 - "columns": { 10 - "uri": { 11 - "name": "uri", 12 - "type": "text", 13 - "primaryKey": false, 14 - "notNull": false, 15 - "autoincrement": false, 16 - "generated": { 17 - "as": "('at://' || \"did\" || '/blue.recipes.actor.profile/self')", 18 - "type": "virtual" 19 - } 20 - }, 21 - "cid": { 22 - "name": "cid", 23 - "type": "text", 24 - "primaryKey": false, 25 - "notNull": true, 26 - "autoincrement": false 27 - }, 28 - "did": { 29 - "name": "did", 30 - "type": "text", 31 - "primaryKey": true, 32 - "notNull": true, 33 - "autoincrement": false 34 - }, 35 - "ingested_at": { 36 - "name": "ingested_at", 37 - "type": "text", 38 - "primaryKey": false, 39 - "notNull": true, 40 - "autoincrement": false, 41 - "default": "CURRENT_TIMESTAMP" 42 - }, 43 - "display_name": { 44 - "name": "display_name", 45 - "type": "text(640)", 46 - "primaryKey": false, 47 - "notNull": true, 48 - "autoincrement": false 49 - }, 50 - "description": { 51 - "name": "description", 52 - "type": "text(2500)", 53 - "primaryKey": false, 54 - "notNull": false, 55 - "autoincrement": false 56 - }, 57 - "pronouns": { 58 - "name": "pronouns", 59 - "type": "text(200)", 60 - "primaryKey": false, 61 - "notNull": false, 62 - "autoincrement": false 63 - }, 64 - "website": { 65 - "name": "website", 66 - "type": "text", 67 - "primaryKey": false, 68 - "notNull": false, 69 - "autoincrement": false 70 - }, 71 - "avatar": { 72 - "name": "avatar", 73 - "type": "text", 74 - "primaryKey": false, 75 - "notNull": false, 76 - "autoincrement": false 77 - }, 78 - "banner": { 79 - "name": "banner", 80 - "type": "text", 81 - "primaryKey": false, 82 - "notNull": false, 83 - "autoincrement": false 84 - }, 85 - "created_at": { 86 - "name": "created_at", 87 - "type": "text", 88 - "primaryKey": false, 89 - "notNull": true, 90 - "autoincrement": false 91 - } 92 - }, 93 - "indexes": { 94 - "profiles_cid_idx": { 95 - "name": "profiles_cid_idx", 96 - "columns": [ 97 - "cid" 98 - ], 99 - "isUnique": false 100 - }, 101 - "profiles_cat_idx": { 102 - "name": "profiles_cat_idx", 103 - "columns": [ 104 - "created_at" 105 - ], 106 - "isUnique": false 107 - }, 108 - "profiles_iat_idx": { 109 - "name": "profiles_iat_idx", 110 - "columns": [ 111 - "ingested_at" 112 - ], 113 - "isUnique": false 114 - } 115 - }, 116 - "foreignKeys": {}, 117 - "compositePrimaryKeys": {}, 118 - "uniqueConstraints": {}, 119 - "checkConstraints": {} 120 - }, 121 - "recipes": { 122 - "name": "recipes", 123 - "columns": { 124 - "uri": { 125 - "name": "uri", 126 - "type": "text", 127 - "primaryKey": false, 128 - "notNull": false, 129 - "autoincrement": false, 130 - "generated": { 131 - "as": "('at://' || \"author_did\" || '/blue.recipes.feed.recipe/' || \"rkey\")", 132 - "type": "virtual" 133 - } 134 - }, 135 - "cid": { 136 - "name": "cid", 137 - "type": "text", 138 - "primaryKey": false, 139 - "notNull": true, 140 - "autoincrement": false 141 - }, 142 - "author_did": { 143 - "name": "author_did", 144 - "type": "text", 145 - "primaryKey": false, 146 - "notNull": true, 147 - "autoincrement": false 148 - }, 149 - "rkey": { 150 - "name": "rkey", 151 - "type": "text", 152 - "primaryKey": false, 153 - "notNull": true, 154 - "autoincrement": false 155 - }, 156 - "image": { 157 - "name": "image", 158 - "type": "text", 159 - "primaryKey": false, 160 - "notNull": false, 161 - "autoincrement": false 162 - }, 163 - "title": { 164 - "name": "title", 165 - "type": "text", 166 - "primaryKey": false, 167 - "notNull": true, 168 - "autoincrement": false 169 - }, 170 - "time": { 171 - "name": "time", 172 - "type": "integer", 173 - "primaryKey": false, 174 - "notNull": true, 175 - "autoincrement": false, 176 - "default": 0 177 - }, 178 - "serves": { 179 - "name": "serves", 180 - "type": "integer", 181 - "primaryKey": false, 182 - "notNull": false, 183 - "autoincrement": false 184 - }, 185 - "description": { 186 - "name": "description", 187 - "type": "text", 188 - "primaryKey": false, 189 - "notNull": false, 190 - "autoincrement": false 191 - }, 192 - "ingredients": { 193 - "name": "ingredients", 194 - "type": "text", 195 - "primaryKey": false, 196 - "notNull": true, 197 - "autoincrement": false 198 - }, 199 - "ingredients_count": { 200 - "name": "ingredients_count", 201 - "type": "integer", 202 - "primaryKey": false, 203 - "notNull": false, 204 - "autoincrement": false, 205 - "generated": { 206 - "as": "(json_array_length(\"ingredients\"))", 207 - "type": "virtual" 208 - } 209 - }, 210 - "steps": { 211 - "name": "steps", 212 - "type": "text", 213 - "primaryKey": false, 214 - "notNull": true, 215 - "autoincrement": false 216 - }, 217 - "steps_count": { 218 - "name": "steps_count", 219 - "type": "integer", 220 - "primaryKey": false, 221 - "notNull": false, 222 - "autoincrement": false, 223 - "generated": { 224 - "as": "(json_array_length(\"steps\"))", 225 - "type": "virtual" 226 - } 227 - }, 228 - "created_at": { 229 - "name": "created_at", 230 - "type": "text", 231 - "primaryKey": false, 232 - "notNull": true, 233 - "autoincrement": false 234 - }, 235 - "ingested_at": { 236 - "name": "ingested_at", 237 - "type": "text", 238 - "primaryKey": false, 239 - "notNull": true, 240 - "autoincrement": false, 241 - "default": "CURRENT_TIMESTAMP" 242 - } 243 - }, 244 - "indexes": { 245 - "recipes_title_idx": { 246 - "name": "recipes_title_idx", 247 - "columns": [ 248 - "title" 249 - ], 250 - "isUnique": false 251 - }, 252 - "recipes_cid_idx": { 253 - "name": "recipes_cid_idx", 254 - "columns": [ 255 - "cid" 256 - ], 257 - "isUnique": false 258 - }, 259 - "recipes_cat_idx": { 260 - "name": "recipes_cat_idx", 261 - "columns": [ 262 - "created_at" 263 - ], 264 - "isUnique": false 265 - }, 266 - "recipes_iat_idx": { 267 - "name": "recipes_iat_idx", 268 - "columns": [ 269 - "ingested_at" 270 - ], 271 - "isUnique": false 272 - } 273 - }, 274 - "foreignKeys": { 275 - "recipes_author_did_profiles_did_fk": { 276 - "name": "recipes_author_did_profiles_did_fk", 277 - "tableFrom": "recipes", 278 - "tableTo": "profiles", 279 - "columnsFrom": [ 280 - "author_did" 281 - ], 282 - "columnsTo": [ 283 - "did" 284 - ], 285 - "onDelete": "cascade", 286 - "onUpdate": "no action" 287 - } 288 - }, 289 - "compositePrimaryKeys": { 290 - "recipes_author_did_rkey_pk": { 291 - "columns": [ 292 - "author_did", 293 - "rkey" 294 - ], 295 - "name": "recipes_author_did_rkey_pk" 296 - } 297 - }, 298 - "uniqueConstraints": {}, 299 - "checkConstraints": {} 300 - } 301 - }, 302 - "views": {}, 303 - "enums": {}, 304 - "_meta": { 305 - "schemas": {}, 306 - "tables": {}, 307 - "columns": {} 308 - }, 309 - "internal": { 310 - "indexes": {} 311 - } 312 - }
+4 -25
libs/database/migrations/meta/_journal.json
··· 1 1 { 2 2 "version": "7", 3 - "dialect": "sqlite", 3 + "dialect": "postgresql", 4 4 "entries": [ 5 5 { 6 6 "idx": 0, 7 - "version": "6", 8 - "when": 1764024817179, 9 - "tag": "0000_kind_ultron", 10 - "breakpoints": true 11 - }, 12 - { 13 - "idx": 1, 14 - "version": "6", 15 - "when": 1764102063385, 16 - "tag": "0001_past_umar", 17 - "breakpoints": true 18 - }, 19 - { 20 - "idx": 2, 21 - "version": "6", 22 - "when": 1764113357363, 23 - "tag": "0002_cheerful_venom", 24 - "breakpoints": true 25 - }, 26 - { 27 - "idx": 3, 28 - "version": "6", 29 - "when": 1764113735823, 30 - "tag": "0003_long_blue_marvel", 7 + "version": "7", 8 + "when": 1764420650497, 9 + "tag": "0000_young_hellcat", 31 10 "breakpoints": true 32 11 } 33 12 ]
+2
libs/database/package.json
··· 27 27 "@cookware/tsconfig": "workspace:*", 28 28 "@types/bun": "catalog:", 29 29 "@types/node": "^22.10.1", 30 + "@types/pg": "^8.15.6", 30 31 "drizzle-kit": "^0.29.0", 31 32 "typescript": "^5.2.2" 32 33 }, 33 34 "dependencies": { 34 35 "@libsql/client": "^0.15.15", 35 36 "drizzle-orm": "catalog:", 37 + "pg": "^8.16.3", 36 38 "zod": "^3.23.8" 37 39 } 38 40 }
+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;
+23 -2
libs/lexicons/lexicons/profiles/defs.tsp
··· 10 10 displayName?: string; 11 11 12 12 pronouns?: string; 13 + avatar?: url; 14 + 15 + @format("datetime") 16 + createdAt?: string; 17 + } 18 + 19 + model ProfileViewDetailed { 20 + @required did: did; 21 + @required handle: handle; 22 + 23 + @maxGraphemes(64) 24 + @maxLength(640) 25 + displayName?: string; 13 26 14 - /** Small image to be displayed on the profile. */ 15 - avatar?: Blob<#["image/png", "image/jpeg"], 1000000>; // 1mb image 27 + @maxGraphemes(256) 28 + @maxLength(2500) 29 + description?: string; 30 + 31 + pronouns?: string; 32 + website?: url; 33 + avatar?: url; 34 + banner?: url; 35 + 36 + recipesCount?: integer; 16 37 17 38 @format("datetime") 18 39 createdAt?: string;
+9
libs/lexicons/lexicons/profiles/getProfile.tsp
··· 1 + import "@typelex/emitter"; 2 + import "./defs.tsp"; 3 + 4 + namespace blue.recipes.actor.getProfile { 5 + @query() 6 + op Main( 7 + @required actor: atIdentifier, 8 + ): blue.recipes.actor.defs.ProfileViewDetailed; 9 + }
+1
libs/lexicons/lexicons/profiles/main.tsp
··· 1 1 import "./profile.tsp"; 2 2 import "./defs.tsp"; 3 + import "./getProfile.tsp";
+1 -2
libs/lexicons/lexicons/profiles/profile.tsp
··· 17 17 @maxLength(200) 18 18 pronouns?: string; 19 19 20 - @format("url") 21 - website?: string; 20 + website?: uri; 22 21 23 22 /** Small image to be displayed on the profile. */ 24 23 avatar?: Blob<#["image/png", "image/jpeg"], 1000000>; // 1mb image
+1
libs/lexicons/lib/index.ts
··· 1 1 export * as BlueRecipesActorDefs from "./types/blue/recipes/actor/defs.js"; 2 + export * as BlueRecipesActorGetProfile from "./types/blue/recipes/actor/getProfile.js"; 2 3 export * as BlueRecipesActorProfile from "./types/blue/recipes/actor/profile.js"; 3 4 export * as BlueRecipesFeedDefs from "./types/blue/recipes/feed/defs.js"; 4 5 export * as BlueRecipesFeedGetRecipe from "./types/blue/recipes/feed/getRecipe.js";
+39 -4
libs/lexicons/lib/types/blue/recipes/actor/defs.ts
··· 5 5 $type: /*#__PURE__*/ v.optional( 6 6 /*#__PURE__*/ v.literal("blue.recipes.actor.defs#profileViewBasic"), 7 7 ), 8 + avatar: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 9 + createdAt: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 10 + did: /*#__PURE__*/ v.didString(), 8 11 /** 9 - * Small image to be displayed on the profile. 10 - * @accept image/png, image/jpeg 11 - * @maxSize 1000000 12 + * @maxLength 640 13 + * @maxGraphemes 64 12 14 */ 13 - avatar: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.blob()), 15 + displayName: /*#__PURE__*/ v.optional( 16 + /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.string(), [ 17 + /*#__PURE__*/ v.stringLength(0, 640), 18 + /*#__PURE__*/ v.stringGraphemes(0, 64), 19 + ]), 20 + ), 21 + handle: /*#__PURE__*/ v.handleString(), 22 + pronouns: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 23 + }); 24 + const _profileViewDetailedSchema = /*#__PURE__*/ v.object({ 25 + $type: /*#__PURE__*/ v.optional( 26 + /*#__PURE__*/ v.literal("blue.recipes.actor.defs#profileViewDetailed"), 27 + ), 28 + avatar: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 29 + banner: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 14 30 createdAt: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 31 + /** 32 + * @maxLength 2500 33 + * @maxGraphemes 256 34 + */ 35 + description: /*#__PURE__*/ v.optional( 36 + /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.string(), [ 37 + /*#__PURE__*/ v.stringLength(0, 2500), 38 + /*#__PURE__*/ v.stringGraphemes(0, 256), 39 + ]), 40 + ), 15 41 did: /*#__PURE__*/ v.didString(), 16 42 /** 17 43 * @maxLength 640 ··· 25 51 ), 26 52 handle: /*#__PURE__*/ v.handleString(), 27 53 pronouns: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 54 + recipesCount: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.integer()), 55 + website: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 28 56 }); 29 57 30 58 type profileViewBasic$schematype = typeof _profileViewBasicSchema; 59 + type profileViewDetailed$schematype = typeof _profileViewDetailedSchema; 31 60 32 61 export interface profileViewBasicSchema extends profileViewBasic$schematype {} 62 + export interface profileViewDetailedSchema 63 + extends profileViewDetailed$schematype {} 33 64 34 65 export const profileViewBasicSchema = 35 66 _profileViewBasicSchema as profileViewBasicSchema; 67 + export const profileViewDetailedSchema = 68 + _profileViewDetailedSchema as profileViewDetailedSchema; 36 69 37 70 export interface ProfileViewBasic 38 71 extends v.InferInput<typeof profileViewBasicSchema> {} 72 + export interface ProfileViewDetailed 73 + extends v.InferInput<typeof profileViewDetailedSchema> {}
+31
libs/lexicons/lib/types/blue/recipes/actor/getProfile.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + import * as BlueRecipesActorDefs from "./defs.js"; 5 + 6 + const _mainSchema = /*#__PURE__*/ v.query("blue.recipes.actor.getProfile", { 7 + params: /*#__PURE__*/ v.object({ 8 + actor: /*#__PURE__*/ v.actorIdentifierString(), 9 + }), 10 + output: { 11 + type: "lex", 12 + get schema() { 13 + return BlueRecipesActorDefs.profileViewDetailedSchema; 14 + }, 15 + }, 16 + }); 17 + 18 + type main$schematype = typeof _mainSchema; 19 + 20 + export interface mainSchema extends main$schematype {} 21 + 22 + export const mainSchema = _mainSchema as mainSchema; 23 + 24 + export interface $params extends v.InferInput<mainSchema["params"]> {} 25 + export type $output = v.InferXRPCBodyInput<mainSchema["output"]>; 26 + 27 + declare module "@atcute/lexicons/ambient" { 28 + interface XRPCQueries { 29 + "blue.recipes.actor.getProfile": mainSchema; 30 + } 31 + }
+1 -1
libs/lexicons/lib/types/blue/recipes/actor/profile.ts
··· 49 49 /*#__PURE__*/ v.stringGraphemes(0, 20), 50 50 ]), 51 51 ), 52 - website: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 52 + website: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.genericUriString()), 53 53 }), 54 54 ); 55 55
+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
+1 -1
package.json
··· 19 19 "@atcute/atproto": "^3.1.9", 20 20 "@atcute/bluesky": "^3.2.11", 21 21 "@atcute/client": "^4.0.5", 22 - "@atcute/lexicons": "^1.2.4", 22 + "@atcute/lexicons": "^1.2.5", 23 23 "drizzle-orm": "^0.44.7" 24 24 } 25 25 }