diff --git a/package.json b/package.json index f3a1c50..3a001b1 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "1.0.0", + "embla-carousel-react": "^8.6.0", "fraction.js": "^5.2.1", "lucide-react": "^0.475.0", "next": "^15.1.3", @@ -68,6 +69,7 @@ "superjson": "^1.12.4", "tailwind-merge": "^3.0.1", "tailwindcss-animate": "^1.0.7", + "uuid": "^11.1.0", "zod": "^3.0.0" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8f2cc5f..9ce255c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -80,6 +80,9 @@ importers: cmdk: specifier: 1.0.0 version: 1.0.0(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + embla-carousel-react: + specifier: ^8.6.0 + version: 8.6.0(react@19.0.0) fraction.js: specifier: ^5.2.1 version: 5.2.1 @@ -113,6 +116,9 @@ importers: tailwindcss-animate: specifier: ^1.0.7 version: 1.0.7(tailwindcss@3.4.17) + uuid: + specifier: ^11.1.0 + version: 11.1.0 zod: specifier: ^3.0.0 version: 3.24.1 @@ -1995,6 +2001,19 @@ packages: electron-to-chromium@1.5.90: resolution: {integrity: sha512-C3PN4aydfW91Natdyd449Kw+BzhLmof6tzy5W1pFC5SpQxVXT+oyiyOG9AgYYSN9OdA/ik3YkCrpwqI8ug5Tug==} + embla-carousel-react@8.6.0: + resolution: {integrity: sha512-0/PjqU7geVmo6F734pmPqpyHqiM99olvyecY7zdweCw+6tKEXnrE90pBiBbMMU8s5tICemzpQ3hi5EpxzGW+JA==} + peerDependencies: + react: ^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + + embla-carousel-reactive-utils@8.6.0: + resolution: {integrity: sha512-fMVUDUEx0/uIEDM0Mz3dHznDhfX+znCCDCeIophYb1QGVM7YThSWX+wz11zlYwWFOr74b4QLGg0hrGPJeG2s4A==} + peerDependencies: + embla-carousel: 8.6.0 + + embla-carousel@8.6.0: + resolution: {integrity: sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==} + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -3474,6 +3493,10 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + uuid@11.1.0: + resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} + hasBin: true + uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} hasBin: true @@ -5183,6 +5206,18 @@ snapshots: electron-to-chromium@1.5.90: {} + embla-carousel-react@8.6.0(react@19.0.0): + dependencies: + embla-carousel: 8.6.0 + embla-carousel-reactive-utils: 8.6.0(embla-carousel@8.6.0) + react: 19.0.0 + + embla-carousel-reactive-utils@8.6.0(embla-carousel@8.6.0): + dependencies: + embla-carousel: 8.6.0 + + embla-carousel@8.6.0: {} + emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} @@ -6948,6 +6983,8 @@ snapshots: util-deprecate@1.0.2: {} + uuid@11.1.0: {} + uuid@8.3.2: {} validate-npm-package-license@3.0.4: diff --git a/prisma/migrations/20250710035435_nuke_posts/migration.sql b/prisma/migrations/20250710035435_nuke_posts/migration.sql new file mode 100644 index 0000000..337ae2e --- /dev/null +++ b/prisma/migrations/20250710035435_nuke_posts/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - You are about to drop the `Post` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropTable +DROP TABLE "Post"; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 6113714..052caee 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -10,18 +10,6 @@ generator client { provider = "prisma-client-js" } -model Post { - id String @id @default(uuid()) - title String - text String - - // To return `Date`s intact through the API we use transformers - // https://trpc.io/docs/v11/data-transformers - // This is unique so it can be used for cursor-based pagination - createdAt DateTime @default(now()) - updatedAt DateTime @default(now()) @updatedAt -} - // Users model User { id String @id @default(uuid()) diff --git a/src/components/cocktails/cocktail-carousel-card.tsx b/src/components/cocktails/cocktail-carousel-card.tsx new file mode 100644 index 0000000..10e204a --- /dev/null +++ b/src/components/cocktails/cocktail-carousel-card.tsx @@ -0,0 +1,396 @@ +import { Prisma } from '@prisma/client' +import { + Bookmark, + BookmarkCheck, + Info, + Minus, + Plus, + Settings2, + Share, +} from 'lucide-react' +import { Fragment, useState } from 'react' +import { + convertToPreferredUnits, + simplifyValue, +} from '~/utils/conversions/conversion.utils' +import { PreferredUnits } from '~/utils/conversions/preferred-units' +import { RouterOutput, trpc } from '~/utils/trpc' +import { Button } from '../ui/button' +import { + Card, + CardContent, + CardFooter, + CardHeader, + CardTitle, +} from '../ui/card' +import { Checkbox } from '../ui/checkbox' +import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover' +import { ToggleGroup, ToggleGroupItem } from '../ui/toggle-group' +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from '../ui/tooltip' +import { CocktailGlassIcon } from './cocktail-glass-icon' +import { Badge } from '../ui/badge' + +type CocktailOutput = RouterOutput['cocktail']['byId'] + +/** + * The CocktailCard component is a card that displays an + * overview of cocktail details. + */ +export function CocktailCarouselCard({ + cocktail, +}: { + cocktail: CocktailOutput +}) { + const [batchValue, setBatchValue] = useState(1) + const utils = trpc.useUtils() + const userQuery = trpc.user.current.useQuery() + const userCocktailQuery = trpc.userCocktail.byCocktailId.useQuery({ + cocktailId: cocktail?.id || '', + }) + + const updateUser = trpc.user.update.useMutation({ + async onSuccess() { + await userQuery.refetch() + }, + }) + const addUserCocktail = trpc.userCocktail.create.useMutation({ + async onSuccess() { + await userCocktailQuery.refetch() + await utils.userCocktail.list.invalidate() + }, + }) + const removeUserCocktail = trpc.userCocktail.delete.useMutation({ + async onSuccess() { + await userCocktailQuery.refetch() + await utils.userCocktail.list.invalidate() + }, + }) + + const { data: user } = userQuery + const { data: userCocktailData } = userCocktailQuery + + const handlePreferredUnitsChange = async (preferredUnits: PreferredUnits) => { + if (updateUser.isPending) { + return + } + + try { + await updateUser.mutateAsync({ preferredUnits }) + } catch (error) { + console.error('Failed to update preferred units - ', error) + } + } + + const handleSaveToggle = async () => { + if (addUserCocktail.isPending || removeUserCocktail.isPending) { + return + } + + if (!!userCocktailData) { + try { + await removeUserCocktail.mutateAsync({ cocktailId: cocktail?.id || '' }) + } catch (error) { + console.error('Failed to remove cocktail from saved - ', error) + } + } else { + try { + await addUserCocktail.mutateAsync({ cocktailId: cocktail?.id || '' }) + } catch (error) { + console.error('Failed to save cocktail - ', error) + } + } + } + + const handleShareClick = async () => { + if (navigator.share) { + try { + await navigator.share({ + title: cocktail?.name, + text: `Check out this cocktail recipe: ${cocktail?.name}`, + url: `${window.location.origin}/home/recipes/explore/${cocktail?.id || ''}`, + }) + } catch (error) { + console.log('Failed to share - ', error) + } + } else { + // Fallback for browsers that do not support the Web Share API + alert( + 'Sharing is not supported in this browser. Please copy the link manually.', + ) + } + } + + return ( + + {cocktail && ( + <> + + + {cocktail.glass && ( + + )} + {cocktail.name} + + + +
+
+ + + + + +
+
+ Preferred Units +
+ + + Metric + + + Imperial + + +
+ Batch Size +
+
+ +
+ {batchValue} +
+ +
+
+
+
+ + + + + + +

Share this recipe

+
+
+
+ + + + + + + {!!userCocktailData ? ( +

Remove from saved cocktails

+ ) : ( +

Save this cocktail

+ )} +
+
+
+
+
+
+ Ingredients + {/* Show a badge for the batch size */} + {batchValue !== 1 && ( + {batchValue}x batch size + )} +
+
+ {cocktail.ingredients + .sort((a, b) => (a.sort || 0) - (b.sort || 0)) + .map((ingredient) => { + const converted = convertToPreferredUnits( + ingredient.amount + ? new Prisma.Decimal(ingredient.amount).toNumber() * + batchValue + : 0, + ingredient.units, + user?.preferredUnits, + ) + + return ( + +
+ +
+
+ {simplifyValue(converted.value, converted.units)}{' '} + + {converted.units || ''} + +
+
+ + {ingredient.ingredient.name} + + {ingredient.optional && ( + + {' '} + (optional) + + )} + {(ingredient.note || + ingredient.substitutes.length > 0) && ( + + + + + + + {ingredient.note && ( +
{ingredient.note}
+ )} + {ingredient.substitutes.length > 0 && ( +
+ Substitutes:{' '} + {ingredient.substitutes.map( + (substitute, index) => ( + + {substitute} + {index !== + ingredient.substitutes.length - + 1 && ', '} + + ), + )} +
+ )} +
+
+
+ )} +
+
+ ) + })} +
+
+
+
+ Instructions +
+
+ {cocktail.instructions} +
+
+
+
+ +
+ {cocktail.glass && ( + + + + Glass + + + + {cocktail.glass} + + + )} + {cocktail.garnish && ( + + + + Garnish + + + + {cocktail.garnish} + + + )} +
+
+ + )} + {!cocktail && ( + +
+

+ Not a valid cocktail +

+
+
+ )} +
+ ) +} diff --git a/src/components/cocktails/cocktail-carousel.tsx b/src/components/cocktails/cocktail-carousel.tsx new file mode 100644 index 0000000..0b9248f --- /dev/null +++ b/src/components/cocktails/cocktail-carousel.tsx @@ -0,0 +1,99 @@ +import { Cocktail } from '@prisma/client' +import { Fragment, useEffect, useState } from 'react' +import { trpc } from '~/utils/trpc' +import { + Carousel, + CarouselApi, + CarouselContent, + CarouselItem, +} from '../ui/carousel' +import { CocktailCarouselCard } from './cocktail-carousel-card' + +interface CocktailWithRandomId extends Cocktail { + randomId: string +} + +export function CocktailCarousel() { + const [api, setApi] = useState() + const [currentSlide, setCurrentSlide] = useState(0) + + const cocktailsQuery = trpc.cocktail.list.useInfiniteQuery( + { + limit: 1, + useRandomMode: true, + }, + { + getNextPageParam(lastPage) { + return lastPage.nextCursor + }, + }, + ) + + useEffect(() => { + if (api) { + api.on('select', () => { + setCurrentSlide(api.selectedScrollSnap()) + }) + } + }, [api]) + + useEffect(() => { + const totalPageItems = + cocktailsQuery.data?.pages?.reduce( + (acc, page) => acc + page.items?.length || 0, + 0, + ) || 0 + + if (totalPageItems - currentSlide <= 2 && !cocktailsQuery.isLoading) { + setTimeout(() => { + cocktailsQuery.fetchNextPage() + }, 500) + } + }, [currentSlide, cocktailsQuery]) + + return ( +
+ {/* Show carousel, have items */} + {cocktailsQuery.data?.pages?.[0]?.items?.length !== 0 && ( +
+ + + {cocktailsQuery.data?.pages.map((page, index) => ( + + {page.items.map((item) => ( + + + + ))} + + ))} + + +
+ )} + {/* Empty state for no filters, search, and no items */} + {cocktailsQuery.data?.pages?.[0]?.items?.length === 0 && ( +
+

+ No cocktails in the database yet! +

+
+ )} +
+ ) +} diff --git a/src/components/cocktails/cocktail-sheet.tsx b/src/components/cocktails/cocktail-sheet.tsx index 1bd87ee..8000b1b 100644 --- a/src/components/cocktails/cocktail-sheet.tsx +++ b/src/components/cocktails/cocktail-sheet.tsx @@ -224,7 +224,7 @@ export function CocktailSheet({ disabled={batchValue >= 64} > - Decrease + Increase diff --git a/src/components/cocktails/paginated-cocktails.tsx b/src/components/cocktails/paginated-cocktails.tsx index 5a168f3..c963dbf 100644 --- a/src/components/cocktails/paginated-cocktails.tsx +++ b/src/components/cocktails/paginated-cocktails.tsx @@ -99,7 +99,7 @@ export function PaginatedCocktails({ {cocktailsQuery.hasNextPage && } - {/* Empty state for no filters, search, and no ingredients */} + {/* Empty state for no filters, search, and no items */} {!exploreSearch && !exploreUseAvailable && cocktailsQuery.data?.pages?.[0]?.items?.length === 0 && ( @@ -109,7 +109,7 @@ export function PaginatedCocktails({

)} - {/* Empty state for search and no ingredients */} + {/* Empty state for search and no items */} {(exploreSearch || exploreUseAvailable) && cocktailsQuery.data?.pages?.[0]?.items?.length === 0 && (
diff --git a/src/components/cocktails/paginated-user-cocktails.tsx b/src/components/cocktails/paginated-user-cocktails.tsx index f2b8a97..64f6ae8 100644 --- a/src/components/cocktails/paginated-user-cocktails.tsx +++ b/src/components/cocktails/paginated-user-cocktails.tsx @@ -99,7 +99,7 @@ export function PaginatedUserCocktails({ {userCocktailsQuery.hasNextPage && }
- {/* Empty state for no filters, search, and no ingredients */} + {/* Empty state for no filters, search, and no items */} {!savedSearch && !savedUseAvailable && userCocktailsQuery.data?.pages?.[0]?.items?.length === 0 && ( @@ -109,7 +109,7 @@ export function PaginatedUserCocktails({

)} - {/* Empty state for search and no ingredients */} + {/* Empty state for search and no items */} {(savedSearch || savedUseAvailable) && userCocktailsQuery.data?.pages?.[0]?.items?.length === 0 && (
diff --git a/src/components/layout/home-layout.tsx b/src/components/layout/home-layout.tsx index d65f043..801473e 100644 --- a/src/components/layout/home-layout.tsx +++ b/src/components/layout/home-layout.tsx @@ -17,14 +17,18 @@ import { DarkModeToggle } from './dark-mode-toggle' export default function HomeLayout({ children, pageNames, + disablePageScroll, }: { children: React.ReactNode pageNames: string[] + disablePageScroll?: boolean }) { return ( - +
@@ -53,7 +57,7 @@ export default function HomeLayout({
-
{children}
+
{children}
) diff --git a/src/components/ui/carousel.tsx b/src/components/ui/carousel.tsx new file mode 100644 index 0000000..d52c697 --- /dev/null +++ b/src/components/ui/carousel.tsx @@ -0,0 +1,260 @@ +import * as React from "react" +import useEmblaCarousel, { + type UseEmblaCarouselType, +} from "embla-carousel-react" +import { ArrowLeft, ArrowRight } from "lucide-react" + +import { cn } from "~/lib/utils" +import { Button } from "~/components/ui/button" + +type CarouselApi = UseEmblaCarouselType[1] +type UseCarouselParameters = Parameters +type CarouselOptions = UseCarouselParameters[0] +type CarouselPlugin = UseCarouselParameters[1] + +type CarouselProps = { + opts?: CarouselOptions + plugins?: CarouselPlugin + orientation?: "horizontal" | "vertical" + setApi?: (api: CarouselApi) => void +} + +type CarouselContextProps = { + carouselRef: ReturnType[0] + api: ReturnType[1] + scrollPrev: () => void + scrollNext: () => void + canScrollPrev: boolean + canScrollNext: boolean +} & CarouselProps + +const CarouselContext = React.createContext(null) + +function useCarousel() { + const context = React.useContext(CarouselContext) + + if (!context) { + throw new Error("useCarousel must be used within a ") + } + + return context +} + +const Carousel = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & CarouselProps +>( + ( + { + orientation = "horizontal", + opts, + setApi, + plugins, + className, + children, + ...props + }, + ref + ) => { + const [carouselRef, api] = useEmblaCarousel( + { + ...opts, + axis: orientation === "horizontal" ? "x" : "y", + }, + plugins + ) + const [canScrollPrev, setCanScrollPrev] = React.useState(false) + const [canScrollNext, setCanScrollNext] = React.useState(false) + + const onSelect = React.useCallback((api: CarouselApi) => { + if (!api) { + return + } + + setCanScrollPrev(api.canScrollPrev()) + setCanScrollNext(api.canScrollNext()) + }, []) + + const scrollPrev = React.useCallback(() => { + api?.scrollPrev() + }, [api]) + + const scrollNext = React.useCallback(() => { + api?.scrollNext() + }, [api]) + + const handleKeyDown = React.useCallback( + (event: React.KeyboardEvent) => { + if (event.key === "ArrowLeft") { + event.preventDefault() + scrollPrev() + } else if (event.key === "ArrowRight") { + event.preventDefault() + scrollNext() + } + }, + [scrollPrev, scrollNext] + ) + + React.useEffect(() => { + if (!api || !setApi) { + return + } + + setApi(api) + }, [api, setApi]) + + React.useEffect(() => { + if (!api) { + return + } + + onSelect(api) + api.on("reInit", onSelect) + api.on("select", onSelect) + + return () => { + api?.off("select", onSelect) + } + }, [api, onSelect]) + + return ( + +
+ {children} +
+
+ ) + } +) +Carousel.displayName = "Carousel" + +const CarouselContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const { carouselRef, orientation } = useCarousel() + + return ( +
+
+
+ ) +}) +CarouselContent.displayName = "CarouselContent" + +const CarouselItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const { orientation } = useCarousel() + + return ( +
+ ) +}) +CarouselItem.displayName = "CarouselItem" + +const CarouselPrevious = React.forwardRef< + HTMLButtonElement, + React.ComponentProps +>(({ className, variant = "outline", size = "icon", ...props }, ref) => { + const { orientation, scrollPrev, canScrollPrev } = useCarousel() + + return ( + + ) +}) +CarouselPrevious.displayName = "CarouselPrevious" + +const CarouselNext = React.forwardRef< + HTMLButtonElement, + React.ComponentProps +>(({ className, variant = "outline", size = "icon", ...props }, ref) => { + const { orientation, scrollNext, canScrollNext } = useCarousel() + + return ( + + ) +}) +CarouselNext.displayName = "CarouselNext" + +export { + type CarouselApi, + Carousel, + CarouselContent, + CarouselItem, + CarouselPrevious, + CarouselNext, +} diff --git a/src/pages/home/my-bar/ingredients.tsx b/src/pages/home/my-bar/ingredients.tsx index 93970ad..fcc3a5f 100644 --- a/src/pages/home/my-bar/ingredients.tsx +++ b/src/pages/home/my-bar/ingredients.tsx @@ -10,7 +10,7 @@ export const getServerSideProps = requireAuth(async () => { const Ingredients: Page = () => { return (
-

Ingredients

+

Ingredients

) diff --git a/src/pages/home/my-bar/inventory.tsx b/src/pages/home/my-bar/inventory.tsx index c755617..eed0610 100644 --- a/src/pages/home/my-bar/inventory.tsx +++ b/src/pages/home/my-bar/inventory.tsx @@ -10,7 +10,7 @@ export const getServerSideProps = requireAuth(async () => { const Inventory: Page = () => { return (
-

My Inventory

+

My Inventory

) diff --git a/src/pages/home/random.tsx b/src/pages/home/random.tsx index 75dcb93..674d942 100644 --- a/src/pages/home/random.tsx +++ b/src/pages/home/random.tsx @@ -1,3 +1,4 @@ +import { CocktailCarousel } from '~/components/cocktails/cocktail-carousel' import HomeLayout from '~/components/layout/home-layout' import { requireAuth } from '~/utils/requireAuth' import { Page } from '~/utils/types/page' @@ -8,15 +9,17 @@ export const getServerSideProps = requireAuth(async () => { const Random: Page = () => { return ( -
-

Random

-

Coming soon...

+
+

Random

+
+ +
) } Random.getLayout = (page) => ( - {page} + {page} ) export default Random diff --git a/src/pages/home/recipes/explore/[cocktailId].tsx b/src/pages/home/recipes/explore/[cocktailId].tsx index 26c9ae3..2b9fd2d 100644 --- a/src/pages/home/recipes/explore/[cocktailId].tsx +++ b/src/pages/home/recipes/explore/[cocktailId].tsx @@ -32,7 +32,7 @@ const CocktailModal: Page = () => { return (
-

Explore Cocktails

+

Explore Cocktails

{ const Explore: Page = () => { return (
-

Explore Cocktails

+

Explore Cocktails

) diff --git a/src/pages/home/recipes/saved/[cocktailId].tsx b/src/pages/home/recipes/saved/[cocktailId].tsx index be4a8a4..2668d2b 100644 --- a/src/pages/home/recipes/saved/[cocktailId].tsx +++ b/src/pages/home/recipes/saved/[cocktailId].tsx @@ -32,7 +32,7 @@ const CocktailModal: Page = () => { return (
-

Saved Cocktails

+

Saved Cocktails

{ const SavedCocktails: Page = () => { return (
-

Saved Cocktails

+

Saved Cocktails

) diff --git a/src/pages/home/roadmap.tsx b/src/pages/home/roadmap.tsx index 7afd1de..51505cd 100644 --- a/src/pages/home/roadmap.tsx +++ b/src/pages/home/roadmap.tsx @@ -9,7 +9,7 @@ export const getServerSideProps = requireAuth(async () => { const Roadmap: Page = () => { return (
-

Feature Roadmap

+

Feature Roadmap

A list of planned features ordered by priority.

  1. diff --git a/src/server/routers/_app.ts b/src/server/routers/_app.ts index 7b3c438..d197711 100644 --- a/src/server/routers/_app.ts +++ b/src/server/routers/_app.ts @@ -4,7 +4,6 @@ import { createCallerFactory, publicProcedure, router } from '../trpc' import { cocktailRouter } from './cocktail' import { ingredientRouter } from './ingredient' -import { postRouter } from './post' import { userRouter } from './user' import { userCocktailRouter } from './user-cocktail' import { userIngredientRouter } from './user-ingredient' @@ -12,7 +11,6 @@ import { userIngredientRouter } from './user-ingredient' export const appRouter = router({ healthcheck: publicProcedure.query(() => 'yay!'), user: userRouter, - post: postRouter, ingredient: ingredientRouter, userIngredient: userIngredientRouter, cocktail: cocktailRouter, diff --git a/src/server/routers/cocktail.ts b/src/server/routers/cocktail.ts index bb4a8c4..b61471c 100644 --- a/src/server/routers/cocktail.ts +++ b/src/server/routers/cocktail.ts @@ -1,8 +1,8 @@ import { Prisma } from '@prisma/client' -import { protectedProcedure, router } from '../trpc' +import { TRPCError } from '@trpc/server' import { z } from 'zod' import { prisma } from '../prisma' -import { TRPCError } from '@trpc/server' +import { protectedProcedure, router } from '../trpc' /** * Default selector for Cocktail. @@ -48,6 +48,7 @@ export const cocktailRouter = router({ z.object({ search: z.string().min(2).nullish(), usingAvailableIngredients: z.boolean().nullish(), + useRandomMode: z.boolean().nullish(), limit: z.number().min(1).max(100).nullish(), cursor: z.string().nullish(), }), @@ -55,7 +56,7 @@ export const cocktailRouter = router({ .query(async ({ ctx, input }) => { const userId = ctx.session.user.id const limit = input.limit ?? 25 - const { cursor, search, usingAvailableIngredients } = input + const { cursor, search, usingAvailableIngredients, useRandomMode } = input let availableIngredientIds: string[] = [] if (usingAvailableIngredients) { @@ -79,7 +80,7 @@ export const cocktailRouter = router({ // Build the filters const whereParams: Prisma.CocktailWhereInput = {} - + if (search) { whereParams.OR = [ // Name search @@ -119,25 +120,43 @@ export const cocktailRouter = router({ ] } - const items = await prisma.cocktail.findMany({ - where: whereParams, - select: defaultCocktailSelect, - take: limit + 1, - skip: cursor ? 1 : 0, - cursor: cursor - ? { - id: cursor, - } - : undefined, - orderBy: { - name: 'asc', - }, - }) + if (useRandomMode) { + const totalCount = await prisma.cocktail.count() + const items = await prisma.cocktail.findMany({ + where: whereParams, + select: defaultCocktailSelect, + take: limit, + skip: Math.max(0, Math.floor(Math.random() * (totalCount - limit))), + }) + + return { + items: items.map((item) => ({ + ...item, + randomId: `${item.id}-${Date.now()}`, + })), + nextCursor: items[items.length - 1]?.id, + } + } else { + const items = await prisma.cocktail.findMany({ + where: whereParams, + select: defaultCocktailSelect, + take: limit + 1, + skip: cursor ? 1 : 0, + cursor: cursor + ? { + id: cursor, + } + : undefined, + orderBy: { + name: 'asc', + }, + }) - return { - items: items, - nextCursor: - items.length > limit ? items[items.length - 1]?.id : undefined, + return { + items: items, + nextCursor: + items.length > limit ? items[items.length - 1]?.id : undefined, + } } }), byId: protectedProcedure diff --git a/src/server/routers/post.ts b/src/server/routers/post.ts deleted file mode 100644 index 9c10f22..0000000 --- a/src/server/routers/post.ts +++ /dev/null @@ -1,104 +0,0 @@ -/** - * - * This is an example router, you can delete this file and then update `../pages/api/trpc/[trpc].tsx` - */ -import { router, publicProcedure } from '../trpc'; -import type { Prisma } from '@prisma/client'; -import { TRPCError } from '@trpc/server'; -import { z } from 'zod'; -import { prisma } from '~/server/prisma'; - -/** - * Default selector for Post. - * It's important to always explicitly say which fields you want to return in order to not leak extra information - * @see https://github.com/prisma/prisma/issues/9353 - */ -const defaultPostSelect = { - id: true, - title: true, - text: true, - createdAt: true, - updatedAt: true, -} satisfies Prisma.PostSelect; - -export const postRouter = router({ - list: publicProcedure - .input( - z.object({ - limit: z.number().min(1).max(100).nullish(), - cursor: z.string().nullish(), - }), - ) - .query(async ({ input }) => { - /** - * For pagination docs you can have a look here - * @see https://trpc.io/docs/v11/useInfiniteQuery - * @see https://www.prisma.io/docs/concepts/components/prisma-client/pagination - */ - - const limit = input.limit ?? 50; - const { cursor } = input; - - const items = await prisma.post.findMany({ - select: defaultPostSelect, - // get an extra item at the end which we'll use as next cursor - take: limit + 1, - where: {}, - cursor: cursor - ? { - id: cursor, - } - : undefined, - orderBy: { - createdAt: 'desc', - }, - }); - let nextCursor: typeof cursor | undefined = undefined; - if (items.length > limit) { - // Remove the last item and use it as next cursor - - const nextItem = items.pop()!; - nextCursor = nextItem.id; - } - - return { - items: items.reverse(), - nextCursor, - }; - }), - byId: publicProcedure - .input( - z.object({ - id: z.string(), - }), - ) - .query(async ({ input }) => { - const { id } = input; - const post = await prisma.post.findUnique({ - where: { id }, - select: defaultPostSelect, - }); - if (!post) { - throw new TRPCError({ - code: 'NOT_FOUND', - message: `No post with id '${id}'`, - }); - } - return post; - }), - add: publicProcedure - .input( - z.object({ - id: z.string().uuid().optional(), - title: z.string().min(1).max(32), - text: z.string().min(1), - }), - ) - .mutation(async ({ input }) => { - const post = await prisma.post.create({ - data: input, - select: defaultPostSelect, - }); - return post; - }), -});