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
+
+
+
+ setBatchValue((prev) => {
+ return Math.max(0.5, prev - 1)
+ })
+ }
+ disabled={batchValue <= 0.5}
+ >
+
+ Decrease
+
+
+ {batchValue}
+
+
+ setBatchValue((prev) => {
+ if (prev === 0.5) {
+ return 1
+ } else {
+ return Math.min(64, prev + 1)
+ }
+ })
+ }
+ disabled={batchValue >= 64}
+ >
+
+ Increase
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Share this recipe
+
+
+
+
+
+
+
+ {!!userCocktailData ? : }
+
+
+
+ {!!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 (
-
+
- {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 (
+
+
+ Previous slide
+
+ )
+})
+CarouselPrevious.displayName = "CarouselPrevious"
+
+const CarouselNext = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps
+>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
+ const { orientation, scrollNext, canScrollNext } = useCarousel()
+
+ return (
+
+
+ Next slide
+
+ )
+})
+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.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.
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;
- }),
-});