From affd43047fb3489e382cdefeddc605f8b7565a98 Mon Sep 17 00:00:00 2001 From: Braighton Polack Date: Sat, 3 May 2025 12:42:24 -0700 Subject: [PATCH 1/8] almost there with carousel functionality --- package.json | 1 + pnpm-lock.yaml | 28 ++ .../cocktails/cocktail-carousel.tsx | 86 ++++++ .../cocktails/paginated-cocktails.tsx | 4 +- .../cocktails/paginated-user-cocktails.tsx | 4 +- src/components/layout/home-layout.tsx | 8 +- src/components/ui/carousel.tsx | 260 ++++++++++++++++++ src/pages/home/random.tsx | 11 +- 8 files changed, 392 insertions(+), 10 deletions(-) create mode 100644 src/components/cocktails/cocktail-carousel.tsx create mode 100644 src/components/ui/carousel.tsx diff --git a/package.json b/package.json index f3a1c50..f297691 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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8f2cc5f..c6f34f6 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 @@ -1995,6 +1998,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==} @@ -5183,6 +5199,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: {} diff --git a/src/components/cocktails/cocktail-carousel.tsx b/src/components/cocktails/cocktail-carousel.tsx new file mode 100644 index 0000000..3d7987d --- /dev/null +++ b/src/components/cocktails/cocktail-carousel.tsx @@ -0,0 +1,86 @@ +import { Fragment, useState } from 'react' +import { trpc } from '~/utils/trpc' +import { Checkbox } from '../ui/checkbox' +import { Label } from '../ui/label' +import { Carousel, CarouselContent, CarouselItem } from '../ui/carousel' +import { Card, CardContent } from '../ui/card' + +export function CocktailCarousel() { + const [usingAvailableIngredients, setUsingAvailableIngredients] = useState< + boolean | 'indeterminate' + >(false) + + const cocktailsQuery = trpc.cocktail.list.useInfiniteQuery( + { + limit: 12, + usingAvailableIngredients: usingAvailableIngredients === true, + }, + { + getNextPageParam(lastPage) { + return lastPage.nextCursor + }, + }, + ) + + return ( +
+
+
+ + +
+
+ {/* Show carousel, have items */} + {cocktailsQuery.data?.pages?.[0]?.items?.length !== 0 && ( +
+ + + {cocktailsQuery.data?.pages.map((page, index) => ( + + {page.items.map((item) => ( + + + +
{item.name}
+
+
+
+ ))} +
+ ))} +
+
+
+ )} + {/* Empty state for no filters, search, and no items */} + {!usingAvailableIngredients && + cocktailsQuery.data?.pages?.[0]?.items?.length === 0 && ( +
+

+ No cocktails in the database yet! +

+
+ )} + {/* Empty state for search and no items */} + {usingAvailableIngredients && + cocktailsQuery.data?.pages?.[0]?.items?.length === 0 && ( +
+

Nothing found

+
+ )} +
+ ) +} 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/random.tsx b/src/pages/home/random.tsx index 75dcb93..8f4d92c 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 From a593a8439d9e2f45a78c060e1392e85eb85a984d Mon Sep 17 00:00:00 2001 From: Braighton Polack Date: Sat, 3 May 2025 18:21:07 -0700 Subject: [PATCH 2/8] finalize the carousel components --- .../cocktails/cocktail-carousel-card.tsx | 396 ++++++++++++++++++ .../cocktails/cocktail-carousel.tsx | 12 +- src/components/cocktails/cocktail-sheet.tsx | 2 +- src/pages/home/my-bar/ingredients.tsx | 2 +- src/pages/home/my-bar/inventory.tsx | 2 +- src/pages/home/random.tsx | 2 +- .../home/recipes/explore/[cocktailId].tsx | 2 +- src/pages/home/recipes/explore/index.tsx | 2 +- src/pages/home/recipes/saved/[cocktailId].tsx | 2 +- src/pages/home/recipes/saved/index.tsx | 2 +- src/pages/home/roadmap.tsx | 2 +- 11 files changed, 409 insertions(+), 17 deletions(-) create mode 100644 src/components/cocktails/cocktail-carousel-card.tsx 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 index 3d7987d..4832540 100644 --- a/src/components/cocktails/cocktail-carousel.tsx +++ b/src/components/cocktails/cocktail-carousel.tsx @@ -1,9 +1,9 @@ import { Fragment, useState } from 'react' import { trpc } from '~/utils/trpc' +import { Carousel, CarouselContent, CarouselItem } from '../ui/carousel' import { Checkbox } from '../ui/checkbox' import { Label } from '../ui/label' -import { Carousel, CarouselContent, CarouselItem } from '../ui/carousel' -import { Card, CardContent } from '../ui/card' +import { CocktailCarouselCard } from './cocktail-carousel-card' export function CocktailCarousel() { const [usingAvailableIngredients, setUsingAvailableIngredients] = useState< @@ -51,12 +51,8 @@ export function CocktailCarousel() { {cocktailsQuery.data?.pages.map((page, index) => ( {page.items.map((item) => ( - - - -
{item.name}
-
-
+ + ))}
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/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 8f4d92c..674d942 100644 --- a/src/pages/home/random.tsx +++ b/src/pages/home/random.tsx @@ -10,7 +10,7 @@ export const getServerSideProps = requireAuth(async () => { const Random: Page = () => { return (
-

Random

+

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. From 0dc856e761a5027af8a4a9425dcee0d6dabcd50c Mon Sep 17 00:00:00 2001 From: Braighton Polack Date: Wed, 9 Jul 2025 21:59:52 -0700 Subject: [PATCH 3/8] set up random ordering --- .../20250710035435_nuke_posts/migration.sql | 8 ++ prisma/schema.prisma | 12 -- .../cocktails/cocktail-carousel.tsx | 47 ++------ src/server/routers/_app.ts | 2 - src/server/routers/cocktail.ts | 55 +++++---- src/server/routers/post.ts | 104 ------------------ 6 files changed, 53 insertions(+), 175 deletions(-) create mode 100644 prisma/migrations/20250710035435_nuke_posts/migration.sql delete mode 100644 src/server/routers/post.ts 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.tsx b/src/components/cocktails/cocktail-carousel.tsx index 4832540..b90d769 100644 --- a/src/components/cocktails/cocktail-carousel.tsx +++ b/src/components/cocktails/cocktail-carousel.tsx @@ -1,19 +1,13 @@ -import { Fragment, useState } from 'react' +import { Fragment } from 'react' import { trpc } from '~/utils/trpc' import { Carousel, CarouselContent, CarouselItem } from '../ui/carousel' -import { Checkbox } from '../ui/checkbox' -import { Label } from '../ui/label' import { CocktailCarouselCard } from './cocktail-carousel-card' export function CocktailCarousel() { - const [usingAvailableIngredients, setUsingAvailableIngredients] = useState< - boolean | 'indeterminate' - >(false) - const cocktailsQuery = trpc.cocktail.list.useInfiniteQuery( { - limit: 12, - usingAvailableIngredients: usingAvailableIngredients === true, + limit: 3, + useRandomMode: true, }, { getNextPageParam(lastPage) { @@ -24,19 +18,6 @@ export function CocktailCarousel() { return (
    -
    -
    - - -
    -
    {/* Show carousel, have items */} {cocktailsQuery.data?.pages?.[0]?.items?.length !== 0 && (
    @@ -62,21 +43,13 @@ export function CocktailCarousel() {
    )} {/* Empty state for no filters, search, and no items */} - {!usingAvailableIngredients && - cocktailsQuery.data?.pages?.[0]?.items?.length === 0 && ( -
    -

    - No cocktails in the database yet! -

    -
    - )} - {/* Empty state for search and no items */} - {usingAvailableIngredients && - cocktailsQuery.data?.pages?.[0]?.items?.length === 0 && ( -
    -

    Nothing found

    -
    - )} + {cocktailsQuery.data?.pages?.[0]?.items?.length === 0 && ( +
    +

    + No cocktails in the database yet! +

    +
    + )}
    ) } 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..46bae42 100644 --- a/src/server/routers/cocktail.ts +++ b/src/server/routers/cocktail.ts @@ -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,39 @@ 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.floor(Math.random() * (totalCount - limit)), + }) + return { + items: items, + nextCursor: undefined, + } + } 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; - }), -}); From 803670114760074eac249620288304b2f210e63d Mon Sep 17 00:00:00 2001 From: Braighton Polack Date: Sat, 19 Jul 2025 01:02:53 -0700 Subject: [PATCH 4/8] finalize random swiper dumb crap, never do this again --- package.json | 1 + pnpm-lock.yaml | 9 +++ .../cocktails/cocktail-carousel.tsx | 55 +++++++++++++++++-- src/server/routers/cocktail.ts | 13 +++-- 4 files changed, 69 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index f297691..3a001b1 100644 --- a/package.json +++ b/package.json @@ -69,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 c6f34f6..9ce255c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -116,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 @@ -3490,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 @@ -6976,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/src/components/cocktails/cocktail-carousel.tsx b/src/components/cocktails/cocktail-carousel.tsx index b90d769..0c2e824 100644 --- a/src/components/cocktails/cocktail-carousel.tsx +++ b/src/components/cocktails/cocktail-carousel.tsx @@ -1,12 +1,25 @@ -import { Fragment } from 'react' +import { Cocktail } from '@prisma/client' +import { Fragment, useEffect, useState } from 'react' import { trpc } from '~/utils/trpc' -import { Carousel, CarouselContent, CarouselItem } from '../ui/carousel' +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: 3, + limit: 1, useRandomMode: true, }, { @@ -16,12 +29,36 @@ export function CocktailCarousel() { }, ) + useEffect(() => { + if (api) { + api.on('select', () => { + setCurrentSlide(api.selectedScrollSnap()) + }) + } + }, [api]) + + useEffect(() => { + console.log(cocktailsQuery.data?.pages) + 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) => ( - + ))} diff --git a/src/server/routers/cocktail.ts b/src/server/routers/cocktail.ts index 46bae42..2597d36 100644 --- a/src/server/routers/cocktail.ts +++ b/src/server/routers/cocktail.ts @@ -1,8 +1,9 @@ import { Prisma } from '@prisma/client' -import { protectedProcedure, router } from '../trpc' +import { TRPCError } from '@trpc/server' +import { v4 as uuidv4 } from 'uuid' import { z } from 'zod' import { prisma } from '../prisma' -import { TRPCError } from '@trpc/server' +import { protectedProcedure, router } from '../trpc' /** * Default selector for Cocktail. @@ -128,9 +129,13 @@ export const cocktailRouter = router({ take: limit, skip: Math.floor(Math.random() * (totalCount - limit)), }) + return { - items: items, - nextCursor: undefined, + items: items.map((item) => ({ + ...item, + randomId: uuidv4(), + })), + nextCursor: items[items.length - 1]?.id, } } else { const items = await prisma.cocktail.findMany({ From dde112ecf2644ef9676bdca81edb52344c46216b Mon Sep 17 00:00:00 2001 From: Braighton Polack Date: Sat, 19 Jul 2025 01:06:18 -0700 Subject: [PATCH 5/8] Update src/server/routers/cocktail.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/server/routers/cocktail.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/routers/cocktail.ts b/src/server/routers/cocktail.ts index 2597d36..699d1a2 100644 --- a/src/server/routers/cocktail.ts +++ b/src/server/routers/cocktail.ts @@ -127,7 +127,7 @@ export const cocktailRouter = router({ where: whereParams, select: defaultCocktailSelect, take: limit, - skip: Math.floor(Math.random() * (totalCount - limit)), + skip: Math.max(0, Math.floor(Math.random() * (totalCount - limit))), }) return { From 74a56828dc7f5b9f881d16b16ace572df829c06b Mon Sep 17 00:00:00 2001 From: Braighton Polack Date: Sat, 19 Jul 2025 01:07:05 -0700 Subject: [PATCH 6/8] Update src/server/routers/cocktail.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/server/routers/cocktail.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/routers/cocktail.ts b/src/server/routers/cocktail.ts index 699d1a2..6d777a2 100644 --- a/src/server/routers/cocktail.ts +++ b/src/server/routers/cocktail.ts @@ -133,7 +133,7 @@ export const cocktailRouter = router({ return { items: items.map((item) => ({ ...item, - randomId: uuidv4(), + randomId: `${item.id}-${Date.now()}`, })), nextCursor: items[items.length - 1]?.id, } From 6cc6fe609dde58607ba0a100fd8932b0d91f87b9 Mon Sep 17 00:00:00 2001 From: Braighton Polack Date: Sat, 19 Jul 2025 01:07:19 -0700 Subject: [PATCH 7/8] Update src/components/cocktails/cocktail-carousel.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/components/cocktails/cocktail-carousel.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/cocktails/cocktail-carousel.tsx b/src/components/cocktails/cocktail-carousel.tsx index 0c2e824..0b9248f 100644 --- a/src/components/cocktails/cocktail-carousel.tsx +++ b/src/components/cocktails/cocktail-carousel.tsx @@ -38,7 +38,6 @@ export function CocktailCarousel() { }, [api]) useEffect(() => { - console.log(cocktailsQuery.data?.pages) const totalPageItems = cocktailsQuery.data?.pages?.reduce( (acc, page) => acc + page.items?.length || 0, From e1e0c009b4deec26baa739f7217babf7e9f381aa Mon Sep 17 00:00:00 2001 From: Braighton Polack Date: Sat, 19 Jul 2025 01:09:20 -0700 Subject: [PATCH 8/8] Update cocktail.ts --- src/server/routers/cocktail.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/server/routers/cocktail.ts b/src/server/routers/cocktail.ts index 6d777a2..b61471c 100644 --- a/src/server/routers/cocktail.ts +++ b/src/server/routers/cocktail.ts @@ -1,6 +1,5 @@ import { Prisma } from '@prisma/client' import { TRPCError } from '@trpc/server' -import { v4 as uuidv4 } from 'uuid' import { z } from 'zod' import { prisma } from '../prisma' import { protectedProcedure, router } from '../trpc'