{"id":25580342,"url":"https://github.com/craftingweb/cozynest","last_synced_at":"2025-07-09T16:38:32.578Z","repository":{"id":276905043,"uuid":"930673595","full_name":"craftingweb/CozyNest","owner":"craftingweb","description":null,"archived":false,"fork":false,"pushed_at":"2025-02-20T15:04:20.000Z","size":4285,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-02-20T16:22:59.259Z","etag":null,"topics":["nextjs","prisma","react","shadcn-ui","supabase","typescript"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/craftingweb.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2025-02-11T02:42:14.000Z","updated_at":"2025-02-20T15:04:24.000Z","dependencies_parsed_at":"2025-02-11T04:37:35.632Z","dependency_job_id":null,"html_url":"https://github.com/craftingweb/CozyNest","commit_stats":null,"previous_names":["craftingweb/cozynest"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/craftingweb%2FCozyNest","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/craftingweb%2FCozyNest/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/craftingweb%2FCozyNest/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/craftingweb%2FCozyNest/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/craftingweb","download_url":"https://codeload.github.com/craftingweb/CozyNest/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":239952637,"owners_count":19723924,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["nextjs","prisma","react","shadcn-ui","supabase","typescript"],"created_at":"2025-02-21T04:15:35.393Z","updated_at":"2025-02-21T04:15:36.330Z","avatar_url":"https://github.com/craftingweb.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"### Next App\n\n```sh\n\nnpx create-next-app@14 store\n```\n\n```sh\nnpm run dev\n```\n\n### Remove Boilerplate\n\n- in globals.css remove all code after directives\n- page.tsx\n\n```tsx\nfunction HomePage() {\n  return \u003ch1 className='text-3xl'\u003eHomePage\u003c/h1\u003e;\n}\nexport default HomePage;\n```\n\n- layout.tsx\n\n```tsx\nexport const metadata: Metadata = {\n  title: 'Next Store',\n  description: 'A nifty store built with Next.js',\n};\n```\n\n- get a hold of the README.MD\n\n### Create Pages\n\n- about\n- admin\n- cart\n- favorites\n- orders\n- products\n- reviews\n\n- new file - pageName/page.tsx\n\n```tsx\nfunction AboutPage() {\n  return \u003cdiv\u003eAboutPage\u003c/div\u003e;\n}\nexport default AboutPage;\n```\n\n### Starter\n\nStarter already has shadcn installed and configured. 👍\n\n- components\n  - ui\n  - cart\n  - form\n  - global\n  - home\n  - navbar\n  - products\n  - single-product\n\n### Navbar - Setup\n\n- create\n\n- navbar\n  - CartButton\n  - DarkMode\n  - LinksDropdown\n  - Logo\n  - Navbar\n  - NavSearch\n  - SignOutLink\n  - UserIcon\n\n### Container Component\n\n- create globals/Container.tsx\n\n```tsx\nimport { cn } from '@/lib/utils';\n\nfunction Container({\n  children,\n  className,\n}: {\n  children: React.ReactNode;\n  className?: string;\n}) {\n  return (\n    \u003cdiv className={cn('mx-auto max-w-6xl xl:max-w-7xl px-8', className)}\u003e\n      {children}\n    \u003c/div\u003e\n  );\n}\n\nexport default Container;\n```\n\ncn() function takes any number of arguments (which are expected to be strings or falsy values), filters out any falsy values (like false, null, undefined, 0, NaN, and empty string \"\"), and then joins the remaining strings into a single string with spaces in between.\n\n### Navbar Component\n\n```tsx\nimport Logo from './Logo';\nimport LinksDropdown from './LinksDropdown';\nimport DarkMode from './DarkMode';\nimport CartButton from './CartButton';\nimport NavSearch from './NavSearch';\nimport Container from '../global/Container';\nfunction Navbar() {\n  return (\n    \u003cnav className='border-b '\u003e\n      \u003cContainer className='flex flex-col sm:flex-row  sm:justify-between sm:items-center flex-wrap gap-4 py-8'\u003e\n        \u003cLogo /\u003e\n        \u003cNavSearch /\u003e\n        \u003cdiv className='flex gap-4 items-center '\u003e\n          \u003cCartButton /\u003e\n          \u003cDarkMode /\u003e\n          \u003cLinksDropdown /\u003e\n        \u003c/div\u003e\n      \u003c/Container\u003e\n    \u003c/nav\u003e\n  );\n}\nexport default Navbar;\n```\n\n- layout.tsx\n\n```tsx\nimport Navbar from '@/components/navbar/Navbar';\nimport Container from '@/components/global/Container';\n\nreturn (\n  \u003chtml lang='en'\u003e\n    \u003cbody className={inter.className}\u003e\n      \u003cNavbar /\u003e\n      \u003cContainer className='py-20'\u003e{children}\u003c/Container\u003e\n    \u003c/body\u003e\n  \u003c/html\u003e\n);\n```\n\n### Logo\n\n```sh\nnpm install react-icons\n```\n\n[React Icons](https://react-icons.github.io/react-icons/)\n\nLogo.tsx\n\n```tsx\nimport Link from 'next/link';\nimport { Button } from '../ui/button';\nimport { LuArmchair } from 'react-icons/lu';\nimport { VscCode } from 'react-icons/vsc';\n\nfunction Logo() {\n  return (\n    \u003cButton size='icon' asChild\u003e\n      \u003cLink href='/'\u003e\n        \u003cVscCode className='w-6 h-6' /\u003e\n      \u003c/Link\u003e\n    \u003c/Button\u003e\n  );\n}\n\nexport default Logo;\n```\n\n### NavSearch Component\n\n```tsx\nimport { Input } from '../ui/input';\n\nfunction NavSearch() {\n  return (\n    \u003cInput\n      type='search'\n      placeholder='search product...'\n      className='max-w-xs dark:bg-muted '\n    /\u003e\n  );\n}\nexport default NavSearch;\n```\n\n### CartButton Component\n\n```tsx\nimport { Button } from '@/components/ui/button';\nimport { LuShoppingCart } from 'react-icons/lu';\nimport Link from 'next/link';\nasync function CartButton() {\n  // temp\n  const numItemsInCart = 9;\n  return (\n    \u003cButton\n      asChild\n      variant='outline'\n      size='icon'\n      className='flex justify-center items-center relative'\n    \u003e\n      \u003cLink href='/cart'\u003e\n        \u003cLuShoppingCart /\u003e\n        \u003cspan className='absolute -top-3 -right-3 bg-primary text-white rounded-full h-6 w-6 flex items-center justify-center text-xs'\u003e\n          {numItemsInCart}\n        \u003c/span\u003e\n      \u003c/Link\u003e\n    \u003c/Button\u003e\n  );\n}\nexport default CartButton;\n```\n\n### Theme\n\n[Theming Options](https://ui.shadcn.com/docs/theming)\n[Themes](https://ui.shadcn.com/themes)\n\n- replace css variables in in globals.css\n\n### Providers\n\n- create app/providers.tsx\n\n```tsx\n'use client';\n\nfunction Providers({ children }: { children: React.ReactNode }) {\n  return \u003c\u003e{children}\u003c/\u003e;\n}\nexport default Providers;\n```\n\nlayout.tsx\n\n```tsx\nimport Providers from './providers';\n\nreturn (\n  \u003chtml lang='en' suppressHydrationWarning\u003e\n    \u003cbody className={inter.className}\u003e\n      \u003cProviders\u003e\n        \u003cNavbar /\u003e\n        \u003cContainer className='py-20'\u003e{children}\u003c/Container\u003e\n      \u003c/Providers\u003e\n    \u003c/body\u003e\n  \u003c/html\u003e\n);\n```\n\n### Shadcn DarkMode\n\n[Next.js Dark Mode](https://ui.shadcn.com/docs/dark-mode/next)\n\n```sh\nnpm install next-themes\n```\n\n- create app/theme-provider.tsx\n\n```tsx\n'use client';\n\nimport * as React from 'react';\nimport { ThemeProvider as NextThemesProvider } from 'next-themes';\nimport { type ThemeProviderProps } from 'next-themes/dist/types';\n\nexport function ThemeProvider({ children, ...props }: ThemeProviderProps) {\n  return \u003cNextThemesProvider {...props}\u003e{children}\u003c/NextThemesProvider\u003e;\n}\n```\n\nproviders.tsx\n\n```tsx\n'use client';\nimport { ThemeProvider } from './theme-provider';\n\nfunction Providers({ children }: { children: React.ReactNode }) {\n  return (\n    \u003cThemeProvider\n      attribute='class'\n      defaultTheme='system'\n      enableSystem\n      disableTransitionOnChange\n    \u003e\n      {children}\n    \u003c/ThemeProvider\u003e\n  );\n}\nexport default Providers;\n```\n\n### DarkMode Component\n\n- make sure you export as default !!!\n\n```tsx\n'use client';\n\nimport * as React from 'react';\nimport { MoonIcon, SunIcon } from '@radix-ui/react-icons';\nimport { useTheme } from 'next-themes';\n\nimport { Button } from '@/components/ui/button';\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n} from '@/components/ui/dropdown-menu';\n\nexport default function ModeToggle() {\n  const { setTheme } = useTheme();\n\n  return (\n    \u003cDropdownMenu\u003e\n      \u003cDropdownMenuTrigger asChild\u003e\n        \u003cButton variant='outline' size='icon'\u003e\n          \u003cSunIcon className='h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0' /\u003e\n          \u003cMoonIcon className='absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100' /\u003e\n          \u003cspan className='sr-only'\u003eToggle theme\u003c/span\u003e\n        \u003c/Button\u003e\n      \u003c/DropdownMenuTrigger\u003e\n      \u003cDropdownMenuContent align='end'\u003e\n        \u003cDropdownMenuItem onClick={() =\u003e setTheme('light')}\u003e\n          Light\n        \u003c/DropdownMenuItem\u003e\n        \u003cDropdownMenuItem onClick={() =\u003e setTheme('dark')}\u003e\n          Dark\n        \u003c/DropdownMenuItem\u003e\n        \u003cDropdownMenuItem onClick={() =\u003e setTheme('system')}\u003e\n          System\n        \u003c/DropdownMenuItem\u003e\n      \u003c/DropdownMenuContent\u003e\n    \u003c/DropdownMenu\u003e\n  );\n}\n```\n\n### Links\n\n- create utils/links.ts\n\n```ts\ntype NavLink = {\n  href: string;\n  label: string;\n};\n\nexport const links: NavLink[] = [\n  { href: '/', label: 'home' },\n  { href: '/about', label: 'about' },\n  { href: '/products', label: 'products' },\n  { href: '/favorites', label: 'favorites' },\n  { href: '/cart', label: 'cart' },\n  { href: '/orders', label: 'orders' },\n];\n```\n\n### LinksDropdown Component\n\n```tsx\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n  DropdownMenuSeparator,\n} from '@/components/ui/dropdown-menu';\nimport { LuAlignLeft } from 'react-icons/lu';\nimport Link from 'next/link';\nimport { Button } from '../ui/button';\nimport { links } from '@/utils/links';\n\nfunction LinksDropdown() {\n  return (\n    \u003cDropdownMenu\u003e\n      \u003cDropdownMenuTrigger asChild\u003e\n        \u003cButton variant='outline' className='flex gap-4 max-w-[100px]'\u003e\n          \u003cLuAlignLeft className='w-6 h-6' /\u003e\n        \u003c/Button\u003e\n      \u003c/DropdownMenuTrigger\u003e\n      \u003cDropdownMenuContent className='w-40' align='start' sideOffset={10}\u003e\n        {links.map((link) =\u003e {\n          return (\n            \u003cDropdownMenuItem key={link.href}\u003e\n              \u003cLink href={link.href} className='capitalize w-full'\u003e\n                {link.label}\n              \u003c/Link\u003e\n            \u003c/DropdownMenuItem\u003e\n          );\n        })}\n      \u003c/DropdownMenuContent\u003e\n    \u003c/DropdownMenu\u003e\n  );\n}\nexport default LinksDropdown;\n```\n\n### Supabase\n\n[Docs](https://supabase.com/)\n\n- create account and organization\n- create project\n- setup password in .env (optional)\n- add .env to .gitignore !!!\n- it will take few minutes\n\n### Prisma\n\n- install prisma vs-code extension\n\nPrisma ORM is a database toolkit that simplifies database access in web applications. It allows developers to interact with databases using a type-safe and auto-generated API, making database operations easier and more secure.\n\n- Prisma server: A standalone infrastructure component sitting on top of your database.\n- Prisma client: An auto-generated library that connects to the Prisma server and lets you read, write and stream data in your database. It is used for data access in your applications.\n\n```sh\nnpm install prisma --save-dev\nnpm install @prisma/client\n```\n\n```sh\nnpx prisma init\n```\n\n### Setup Instance\n\nIn development, the command next dev clears Node.js cache on run. This in turn initializes a new PrismaClient instance each time due to hot reloading that creates a connection to the database. This can quickly exhaust the database connections as each PrismaClient instance holds its own connection pool.\n\n(Prisma Instance)[https://www.prisma.io/docs/guides/other/troubleshooting-orm/help-articles/nextjs-prisma-client-dev-practices#solution]\n\n- create utils/db.ts\n\n```ts\nimport { PrismaClient } from '@prisma/client';\n\nconst prismaClientSingleton = () =\u003e {\n  return new PrismaClient();\n};\n\ntype PrismaClientSingleton = ReturnType\u003ctypeof prismaClientSingleton\u003e;\n\nconst globalForPrisma = globalThis as unknown as {\n  prisma: PrismaClientSingleton | undefined;\n};\n\nconst prisma = globalForPrisma.prisma ?? prismaClientSingleton();\n\nexport default prisma;\n\nif (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;\n```\n\n### Connect Supabase with Prisma\n\n[Useful Info](https://supabase.com/partners/integrations/prisma)\n\n- add to .env\n\n```bash\nDATABASE_URL=\"\"\nDIRECT_URL=\"\"\n```\n\n- DATABASE_URL : Transaction + Password + \"?pgbouncer=true\u0026connection_limit=1\"\n- DIRECT_URL : Session + Password\n\n```prisma\ndatasource db {\n  provider          = \"postgresql\"\n  url               = env(\"DATABASE_URL\")\n  directUrl         = env(\"DIRECT_URL\")\n}\n\ngenerator client {\n  provider = \"prisma-client-js\"\n}\n\nmodel TestProfile {\nid  String @id @default(uuid())\nname String\n\n```\n\n- npx prisma migrate dev --name init\n- npx prisma db push\n\nnpx prisma migrate dev --name init creates a new migration for your database schema\nchanges and applies it, while npx prisma db push directly updates the database schema without creating a migration. In the context of databases, a migration is set of operations, that modify the database schema, helping it evolve over time while preserving existing data.\n\n```bash\nnpx prisma db push\n```\n\n```bash\nnpx prisma studio\n```\n\n## Optional - Prisma Crud\n\n[Prisma Docs](https://www.prisma.io/docs/concepts/components/prisma-client/crud)\n\n- Create Single Record\n\n```js\nconst task = await prisma.task.create({\n  data: {\n    content: 'some task',\n  },\n});\n```\n\n- Get All Records\n\n```js\nconst tasks = await prisma.task.findMany();\n```\n\n- Get record by ID or unique identifier\n\n```js\n// By unique identifier\nconst user = await prisma.user.findUnique({\n  where: {\n    email: 'elsa@prisma.io',\n  },\n});\n\n// By ID\nconst task = await prisma.task.findUnique({\n  where: {\n    id: id,\n  },\n});\n```\n\n- Update Record\n\n```js\nconst updateTask = await prisma.task.update({\n  where: {\n    id: id,\n  },\n  data: {\n    content: 'updated task',\n  },\n});\n```\n\n- Update or create records\n\n```js\nconst upsertTask = await prisma.task.upsert({\n  where: {\n    id: id,\n  },\n  update: {\n    content: 'some value',\n  },\n  create: {\n    content: 'some value',\n  },\n});\n```\n\n- Delete a single record\n\n```js\nconst deleteTask = await prisma.task.delete({\n  where: {\n    id: id,\n  },\n});\n```\n\n### Practice Prisma Queries\n\nabout/page.tsx\n\n```tsx\nimport db from '@/utils/db';\n\nasync function AboutPage() {\n  const profile = await db.testProfile.create({\n    data: {\n      name: 'random name',\n    },\n  });\n\n  const users = await db.testProfile.findMany();\n\n  return (\n    \u003cdiv\u003e\n      {users.map((user) =\u003e {\n        return (\n          \u003ch2 key={user.id} className='text-2xl font-bold'\u003e\n            {user.name}\n          \u003c/h2\u003e\n        );\n      })}\n    \u003c/div\u003e\n  );\n}\nexport default AboutPage;\n```\n\n### Product Model\n\n```prisma\n\nmodel Product {\n  id           String     @id @default(uuid())\n  name        String\n  company     String\n  description String\n  featured   Boolean\n  image       String\n  price       Int\n  createdAt    DateTime   @default(now())\n  updatedAt    DateTime   @updatedAt\n  clerkId  String\n}\n\n```\n\n- stop server\n\n```bash\nnpx prisma db push\nnpx prisma studio\nnpm run dev\n```\n\n### Products JSON\n\n- create prisma/products.json\n\n```json\n[\n  {\n    \"name\": \"avant-garde lamp\",\n    \"company\": \"Modenza\",\n    \"description\": \"Cloud bread VHS hell of banjo bicycle rights jianbing umami mumblecore etsy 8-bit pok pok +1 wolf. Vexillologist yr dreamcatcher waistcoat, authentic chillwave trust fund. Viral typewriter fingerstache pinterest pork belly narwhal. Schlitz venmo everyday carry kitsch pitchfork chillwave iPhone taiyaki trust fund hashtag kinfolk microdosing gochujang live-edge\",\n    \"featured\": true,\n    \"image\": \"https://images.pexels.com/photos/943150/pexels-photo-943150.jpeg?auto=compress\u0026cs=tinysrgb\u0026w=1600\",\n    \"price\": 100,\n    \"clerkId\": \"clerkId\"\n  },\n  {\n    \"name\": \"chic chair\",\n    \"company\": \"Luxora\",\n    \"description\": \"Cloud bread VHS hell of banjo bicycle rights jianbing umami mumblecore etsy 8-bit pok pok +1 wolf. Vexillologist yr dreamcatcher waistcoat, authentic chillwave trust fund. Viral typewriter fingerstache pinterest pork belly narwhal. Schlitz venmo everyday carry kitsch pitchfork chillwave iPhone taiyaki trust fund hashtag kinfolk microdosing gochujang live-edge\",\n    \"featured\": true,\n    \"image\": \"https://images.pexels.com/photos/5705090/pexels-photo-5705090.jpeg?auto=compress\u0026cs=tinysrgb\u0026w=1600\",\n    \"price\": 200,\n    \"clerkId\": \"clerkId\"\n  },\n  {\n    \"name\": \"comfy bed\",\n    \"company\": \"Homestead\",\n    \"description\": \"Cloud bread VHS hell of banjo bicycle rights jianbing umami mumblecore etsy 8-bit pok pok +1 wolf. Vexillologist yr dreamcatcher waistcoat, authentic chillwave trust fund. Viral typewriter fingerstache pinterest pork belly narwhal. Schlitz venmo everyday carry kitsch pitchfork chillwave iPhone taiyaki trust fund hashtag kinfolk microdosing gochujang live-edge\",\n    \"featured\": true,\n    \"image\": \"https://images.pexels.com/photos/1034584/pexels-photo-1034584.jpeg?auto=compress\u0026cs=tinysrgb\u0026w=1600\",\n    \"price\": 300,\n    \"clerkId\": \"clerkId\"\n  },\n  {\n    \"name\": \"contemporary sofa\",\n    \"company\": \"Comfora\",\n    \"description\": \"Cloud bread VHS hell of banjo bicycle rights jianbing umami mumblecore etsy 8-bit pok pok +1 wolf. Vexillologist yr dreamcatcher waistcoat, authentic chillwave trust fund. Viral typewriter fingerstache pinterest pork belly narwhal. Schlitz venmo everyday carry kitsch pitchfork chillwave iPhone taiyaki trust fund hashtag kinfolk microdosing gochujang live-edge\",\n    \"featured\": false,\n    \"image\": \"https://images.pexels.com/photos/1571459/pexels-photo-1571459.jpeg?auto=compress\u0026cs=tinysrgb\u0026w=1600\",\n    \"price\": 400,\n    \"clerkId\": \"clerkId\"\n  }\n]\n```\n\n### Seed File\n\n- create prisma/seed.js\n\n```js\nconst { PrismaClient } = require('@prisma/client');\nconst products = require('./products.json');\nconst prisma = new PrismaClient();\n\nasync function main() {\n  for (const product of products) {\n    await prisma.product.create({\n      data: product,\n    });\n  }\n}\nmain()\n  .then(async () =\u003e {\n    await prisma.$disconnect();\n  })\n  .catch(async (e) =\u003e {\n    console.error(e);\n    await prisma.$disconnect();\n    process.exit(1);\n  });\n```\n\n```sh\nnode prisma/seed\n```\n\n- check prisma studio\n\n### Create More Components\n\n- global\n\n  - EmptyList\n  - SectionTitle\n  - LoadingContainer\n\n- home\n\n  - FeaturedProducts\n  - Hero\n  - HeroCarousel\n\n- products\n  - FavoriteToggleButton\n  - FavoriteToggleForm\n  - ProductsContainer\n  - ProductsGrid\n  - ProductsList\n\n### Home Page\n\n```tsx\nimport FeaturedProducts from '@/components/home/FeaturedProducts';\nimport Hero from '@/components/home/Hero';\n\nfunction HomPage() {\n  return (\n    \u003c\u003e\n      \u003cHero /\u003e\n      \u003cFeaturedProducts /\u003e\n    \u003c/\u003e\n  );\n}\nexport default HomPage;\n```\n\n### SectionTitle Component\n\n```tsx\nimport { Separator } from '@/components/ui/separator';\n\nfunction SectionTitle({ text }: { text: string }) {\n  return (\n    \u003cdiv\u003e\n      \u003ch2 className='text-3xl font-medium tracking-wider capitalize mb-8'\u003e\n        {text}\n      \u003c/h2\u003e\n      \u003cSeparator /\u003e\n    \u003c/div\u003e\n  );\n}\nexport default SectionTitle;\n```\n\n### EmptyList Component\n\n```tsx\nimport { cn } from '@/lib/utils';\n\nfunction EmptyList({\n  heading = 'No items found.',\n  className,\n}: {\n  heading?: string;\n  className?: string;\n}) {\n  return \u003ch2 className={cn('text-xl ', className)}\u003e{heading}\u003c/h2\u003e;\n}\n\nexport default EmptyList;\n```\n\n### FetchFeaturedProducts and FetchAllProducts\n\n- create utils/actions.ts\n\n```ts\nimport db from '@/utils/db';\n\nexport const fetchFeaturedProducts = async () =\u003e {\n  const products = await db.product.findMany({\n    where: {\n      featured: true,\n    },\n  });\n  return products;\n};\n\nexport const fetchAllProducts = () =\u003e {\n  return db.product.findMany({\n    orderBy: {\n      createdAt: 'desc',\n    },\n  });\n};\n```\n\n### FeaturedProducts Component\n\n```tsx\nimport { fetchFeaturedProducts } from '@/utils/actions';\nimport EmptyList from '../global/EmptyList';\nimport SectionTitle from '../global/SectionTitle';\nimport ProductsGrid from '../products/ProductsGrid';\nasync function FeaturedProducts() {\n  const products = await fetchFeaturedProducts();\n  if (products.length === 0) return \u003cEmptyList /\u003e;\n  return (\n    \u003csection className='pt-24'\u003e\n      \u003cSectionTitle text='featured products' /\u003e\n      \u003cProductsGrid products={products} /\u003e\n    \u003c/section\u003e\n  );\n}\nexport default FeaturedProducts;\n```\n\n### FormatCurrency\n\n- utils/format.ts\n\n```ts\nexport const formatCurrency = (amount: number | null) =\u003e {\n  const value = amount || 0;\n  return new Intl.NumberFormat('en-US', {\n    style: 'currency',\n    currency: 'USD',\n  }).format(value);\n};\n```\n\n### FavoriteToggleButton\n\n```tsx\nimport { FaHeart } from 'react-icons/fa';\nimport { Button } from '@/components/ui/button';\nfunction FavoriteToggleButton({ productId }: { productId: string }) {\n  return (\n    \u003cButton size='icon' variant='outline' className='p-2 cursor-pointer'\u003e\n      \u003cFaHeart /\u003e\n    \u003c/Button\u003e\n  );\n}\nexport default FavoriteToggleButton;\n```\n\n### ProductsGrid\n\n```tsx\nimport { Product } from '@prisma/client';\nimport { formatCurrency } from '@/utils/format';\nimport { Card, CardContent } from '@/components/ui/card';\nimport Link from 'next/link';\nimport Image from 'next/image';\nimport FavoriteToggleButton from './FavoriteToggleButton';\n\nfunction ProductsGrid({ products }: { products: Product[] }) {\n  return (\n    \u003cdiv className='pt-12 grid gap-4 md:grid-cols-2 lg:grid-cols-3'\u003e\n      {products.map((product) =\u003e {\n        const { name, price, image } = product;\n        const productId = product.id;\n        const dollarsAmount = formatCurrency(price);\n        return (\n          \u003carticle key={productId} className='group relative'\u003e\n            \u003cLink href={`/products/${productId}`}\u003e\n              \u003cCard className='transform group-hover:shadow-xl transition-shadow duration-500'\u003e\n                \u003cCardContent className='p-4'\u003e\n                  \u003cdiv className='relative h-64 md:h-48 rounded overflow-hidden '\u003e\n                    \u003cImage\n                      src={image}\n                      alt={name}\n                      fill\n                      sizes='(max-width:768px) 100vw,(max-width:1200px) 50vw,33vw'\n                      priority\n                      className='rounded w-full object-cover transform group-hover:scale-110 transition-transform duration-500'\n                    /\u003e\n                  \u003c/div\u003e\n                  \u003cdiv className='mt-4 text-center'\u003e\n                    \u003ch2 className='text-lg  capitalize'\u003e{name}\u003c/h2\u003e\n                    \u003cp className='text-muted-foreground  mt-2'\u003e\n                      {dollarsAmount}\n                    \u003c/p\u003e\n                  \u003c/div\u003e\n                \u003c/CardContent\u003e\n              \u003c/Card\u003e\n            \u003c/Link\u003e\n            \u003cdiv className='absolute top-7 right-7 z-5'\u003e\n              \u003cFavoriteToggleButton productId={productId} /\u003e\n            \u003c/div\u003e\n          \u003c/article\u003e\n        );\n      })}\n    \u003c/div\u003e\n  );\n}\nexport default ProductsGrid;\n```\n\n### RemotePatterns\n\n```mjs\n/** @type {import('next').NextConfig} */\nconst nextConfig = {\n  images: {\n    remotePatterns: [\n      {\n        protocol: 'https',\n        hostname: 'images.pexels.com',\n      },\n    ],\n  },\n};\n\nexport default nextConfig;\n```\n\n### Hero Component\n\n```tsx\nimport Link from 'next/link';\nimport { Button } from '@/components/ui/button';\nimport HeroCarousel from './HeroCarousel';\n\nfunction Hero() {\n  return (\n    \u003csection className='grid grid-cols-1 lg:grid-cols-2 gap-24 items-center'\u003e\n      \u003cdiv\u003e\n        \u003ch1 className='max-w-2xl font-bold text-4xl tracking-tight sm:text-6xl'\u003e\n          We are changing the way people shop\n        \u003c/h1\u003e\n        \u003cp className='mt-8 max-w-xl text-lg leading-8 text-muted-foreground'\u003e\n          Lorem ipsum dolor sit amet consectetur adipisicing elit. Cumque et\n          voluptas saepe in quae voluptate, laborum maiores possimus illum\n          reprehenderit aut delectus veniam cum perferendis unde sint doloremque\n          non nam.\n        \u003c/p\u003e\n        \u003cButton asChild size='lg' className='mt-10'\u003e\n          \u003cLink href='/products'\u003eOur Products\u003c/Link\u003e\n        \u003c/Button\u003e\n      \u003c/div\u003e\n      \u003cHeroCarousel /\u003e\n    \u003c/section\u003e\n  );\n}\nexport default Hero;\n```\n\n### Product Images\n\n[Pexels](https://www.pexels.com/)\n\nHeroCarousel\n\n```tsx\nimport {\n  Carousel,\n  CarouselContent,\n  CarouselItem,\n  CarouselNext,\n  CarouselPrevious,\n} from '@/components/ui/carousel';\nimport { Card, CardContent } from '@/components/ui/card';\nimport Image from 'next/image';\nimport hero1 from '@/public/images/hero1.jpg';\nimport hero2 from '@/public/images/hero2.jpg';\nimport hero3 from '@/public/images/hero3.jpg';\nimport hero4 from '@/public/images/hero4.jpg';\n\nconst carouselImages = [hero1, hero2, hero3, hero4];\n\nfunction HeroCarousel() {\n  return (\n    \u003cdiv className='hidden lg:block'\u003e\n      \u003cCarousel\u003e\n        \u003cCarouselContent\u003e\n          {carouselImages.map((image, index) =\u003e {\n            return (\n              \u003cCarouselItem key={index}\u003e\n                \u003cCard\u003e\n                  \u003cCardContent className='p-2'\u003e\n                    \u003cImage\n                      src={image}\n                      alt='hero'\n                      className='w-full h-[24rem] rounded-md object-cover'\n                    /\u003e\n                  \u003c/CardContent\u003e\n                \u003c/Card\u003e\n              \u003c/CarouselItem\u003e\n            );\n          })}\n        \u003c/CarouselContent\u003e\n        \u003cCarouselPrevious /\u003e\n        \u003cCarouselNext /\u003e\n      \u003c/Carousel\u003e\n    \u003c/div\u003e\n  );\n}\nexport default HeroCarousel;\n```\n\n### About Page\n\n```tsx\nfunction AboutPage() {\n  return (\n    \u003csection\u003e\n      \u003ch1 className='flex flex-wrap gap-2 sm:gap-x-6 items-center justify-center text-4xl font-bold leading-none tracking-wide sm:text-6xl'\u003e\n        We love\n        \u003cspan className='bg-primary py-2 px-4 rounded-lg tracking-widest text-white'\u003e\n          store\n        \u003c/span\u003e\n      \u003c/h1\u003e\n      \u003cp className='mt-6 text-lg tracking-wide leading-8 max-w-2xl mx-auto text-muted-foreground'\u003e\n        Lorem ipsum dolor sit amet consectetur adipisicing elit. Vero hic\n        distinctio ducimus temporibus nobis autem laboriosam repellat, magni\n        fugiat minima excepturi neque, tenetur possimus nihil atque! Culpa nulla\n        labore nam?\n      \u003c/p\u003e\n    \u003c/section\u003e\n  );\n}\nexport default AboutPage;\n```\n\n### Suspense Component\n\napp/page.tsx\n\n```tsx\nimport FeaturedProducts from '@/components/home/FeaturedProducts';\nimport Hero from '@/components/home/Hero';\nimport LoadingContainer from '@/components/global/LoadingContainer';\nimport { Suspense } from 'react';\nfunction HomPage() {\n  return (\n    \u003c\u003e\n      \u003cHero /\u003e\n      \u003cSuspense fallback={\u003cLoadingContainer /\u003e}\u003e\n        \u003cFeaturedProducts /\u003e\n      \u003c/Suspense\u003e\n    \u003c/\u003e\n  );\n}\nexport default HomPage;\n```\n\n### LoadingContainer Component\n\n```tsx\nimport { Skeleton } from '../ui/skeleton';\nimport { Card, CardContent } from '../ui/card';\n\nfunction LoadingContainer() {\n  return (\n    \u003cdiv className='pt-12 grid gap-4 md:grid-cols-2 lg:grid-cols-3'\u003e\n      \u003cLoadingProduct /\u003e\n      \u003cLoadingProduct /\u003e\n      \u003cLoadingProduct /\u003e\n    \u003c/div\u003e\n  );\n}\n\nfunction LoadingProduct() {\n  return (\n    \u003cCard\u003e\n      \u003cCardContent className='p-4'\u003e\n        \u003cSkeleton className='h-48 w-full' /\u003e\n        \u003cSkeleton className='h-4 w-3/4 mt-4' /\u003e\n        \u003cSkeleton className='h-4 w-1/4 mt-4' /\u003e\n      \u003c/CardContent\u003e\n    \u003c/Card\u003e\n  );\n}\nexport default LoadingContainer;\n```\n\n### Products Page - Loading\n\n- create app/products/loading.tsx\n\n```tsx\n'use client';\n\nimport LoadingContainer from '@/components/global/LoadingContainer';\n\nfunction loading() {\n  return \u003cLoadingContainer /\u003e;\n}\nexport default loading;\n```\n\n### Products Page\n\n```tsx\nimport ProductsContainer from '@/components/products/ProductsContainer';\n\nasync function ProductsPage({\n  searchParams,\n}: {\n  searchParams: { layout?: string; search?: string };\n}) {\n  const layout = searchParams.layout || 'grid';\n  const search = searchParams.search || '';\n  return (\n    \u003c\u003e\n      \u003cProductsContainer layout={layout} search={search} /\u003e\n    \u003c/\u003e\n  );\n}\nexport default ProductsPage;\n```\n\n### ProductsContainer Component\n\n```tsx\nimport ProductsGrid from './ProductsGrid';\nimport ProductsList from './ProductsList';\nimport { LuLayoutGrid, LuList } from 'react-icons/lu';\nimport { Button } from '@/components/ui/button';\nimport { Separator } from '@/components/ui/separator';\nimport { fetchAllProducts } from '@/utils/actions';\nimport Link from 'next/link';\n\nasync function ProductsContainer({\n  layout,\n  search,\n}: {\n  layout: string;\n  search: string;\n}) {\n  const products = await fetchAllProducts();\n  const totalProducts = products.length;\n  const searchTerm = search ? `\u0026search=${search}` : '';\n  return (\n    \u003c\u003e\n      {/* HEADER */}\n      \u003csection\u003e\n        \u003cdiv className='flex justify-between items-center'\u003e\n          \u003ch4 className='font-medium text-lg'\u003e\n            {totalProducts} product{totalProducts \u003e 1 \u0026\u0026 's'}\n          \u003c/h4\u003e\n          \u003cdiv className='flex gap-x-4'\u003e\n            \u003cButton\n              variant={layout === 'grid' ? 'default' : 'ghost'}\n              size='icon'\n              asChild\n            \u003e\n              \u003cLink href={`/products?layout=grid${searchTerm}`}\u003e\n                \u003cLuLayoutGrid /\u003e\n              \u003c/Link\u003e\n            \u003c/Button\u003e\n            \u003cButton\n              variant={layout === 'list' ? 'default' : 'ghost'}\n              size='icon'\n              asChild\n            \u003e\n              \u003cLink href={`/products?layout=list${searchTerm}`}\u003e\n                \u003cLuList /\u003e\n              \u003c/Link\u003e\n            \u003c/Button\u003e\n          \u003c/div\u003e\n        \u003c/div\u003e\n        \u003cSeparator className='mt-4' /\u003e\n      \u003c/section\u003e\n      {/* PRODUCTS */}\n      \u003cdiv\u003e\n        {totalProducts === 0 ? (\n          \u003ch5 className='text-2xl mt-16'\u003e\n            Sorry, no products matched your search...\n          \u003c/h5\u003e\n        ) : layout === 'grid' ? (\n          \u003cProductsGrid products={products} /\u003e\n        ) : (\n          \u003cProductsList products={products} /\u003e\n        )}\n      \u003c/div\u003e\n    \u003c/\u003e\n  );\n}\nexport default ProductsContainer;\n```\n\n### ProductsList Component\n\n```tsx\nimport { formatCurrency } from '@/utils/format';\nimport Link from 'next/link';\nimport { Card, CardContent } from '@/components/ui/card';\nimport { Product } from '@prisma/client';\nimport Image from 'next/image';\nimport FavoriteToggleButton from './FavoriteToggleButton';\nfunction ProductsList({ products }: { products: Product[] }) {\n  return (\n    \u003cdiv className='mt-12 grid gap-y-8'\u003e\n      {products.map((product) =\u003e {\n        const { name, price, image, company } = product;\n        const dollarsAmount = formatCurrency(price);\n        const productId = product.id;\n        return (\n          \u003carticle key={productId} className='group relative'\u003e\n            \u003cLink href={`/products/${productId}`}\u003e\n              \u003cCard className='transform group-hover:shadow-xl transition-shadow duration-500'\u003e\n                \u003cCardContent className='p-8 gap-y-4 grid md:grid-cols-3'\u003e\n                  \u003cdiv className='relative h-64  md:h-48 md:w-48'\u003e\n                    \u003cImage\n                      src={image}\n                      alt={name}\n                      fill\n                      sizes='(max-width:768px) 100vw,(max-width:1200px) 50vw,33vw'\n                      priority\n                      className='w-full rounded-md object-cover'\n                    /\u003e\n                  \u003c/div\u003e\n\n                  \u003cdiv\u003e\n                    \u003ch2 className='text-xl font-semibold capitalize'\u003e{name}\u003c/h2\u003e\n                    \u003ch4 className='text-muted-foreground'\u003e{company}\u003c/h4\u003e\n                  \u003c/div\u003e\n                  \u003cp className='text-muted-foreground text-lg md:ml-auto'\u003e\n                    {dollarsAmount}\n                  \u003c/p\u003e\n                \u003c/CardContent\u003e\n              \u003c/Card\u003e\n            \u003c/Link\u003e\n            \u003cdiv className='absolute bottom-8 right-8 z-5'\u003e\n              \u003cFavoriteToggleButton productId={productId} /\u003e\n            \u003c/div\u003e\n          \u003c/article\u003e\n        );\n      })}\n    \u003c/div\u003e\n  );\n}\nexport default ProductsList;\n```\n\n### NavSearch\n\n- install use-debounce\n\n```sh\nnpm i use-debounce\n```\n\n```tsx\n'use client';\nimport { Input } from '../ui/input';\nimport { useSearchParams, useRouter } from 'next/navigation';\nimport { useDebouncedCallback } from 'use-debounce';\nimport { useState, useEffect } from 'react';\n\nfunction NavSearch() {\n  const searchParams = useSearchParams();\n  const { replace } = useRouter();\n  const [search, setSearch] = useState(\n    searchParams.get('search')?.toString() || ''\n  );\n  const handleSearch = useDebouncedCallback((value: string) =\u003e {\n    const params = new URLSearchParams(searchParams);\n    if (value) {\n      params.set('search', value);\n    } else {\n      params.delete('search');\n    }\n    replace(`/products?${params.toString()}`);\n  }, 300);\n\n  useEffect(() =\u003e {\n    if (!searchParams.get('search')) {\n      setSearch('');\n    }\n  }, [searchParams.get('search')]);\n  return (\n    \u003cInput\n      type='search'\n      placeholder='search product...'\n      className='max-w-xs dark:bg-muted '\n      onChange={(e) =\u003e {\n        setSearch(e.target.value);\n        handleSearch(e.target.value);\n      }}\n      value={search}\n    /\u003e\n  );\n}\nexport default NavSearch;\n```\n\n### Search Argument\n\n- refactor\n\nProductsContainer.tsx\n\n```tsx\nconst products = await fetchAllProducts({ search });\n```\n\n- actions\n\n```ts\nexport const fetchAllProducts = ({ search = '' }: { search: string }) =\u003e {\n  return db.product.findMany({\n    where: {\n      OR: [\n        { name: { contains: search, mode: 'insensitive' } },\n        { company: { contains: search, mode: 'insensitive' } },\n      ],\n    },\n    orderBy: {\n      createdAt: 'desc',\n    },\n  });\n};\n```\n\n### Wrap NavSearch in Suspense\n\n[useSearchParams Error](https://nextjs.org/docs/messages/missing-suspense-with-csr-bailout)\n\nNavbar.tsx\n\n```tsx\nimport { Suspense } from 'react';\n\nreturn (\n  \u003c\u003e\n    \u003cSuspense\u003e\n      \u003cNavSearch /\u003e\n    \u003c/Suspense\u003e\n  \u003c/\u003e\n);\n```\n\n### Single Product / Single Product - Setup\n\n- actions.ts\n\n```ts\nimport { redirect } from 'next/navigation';\n\nexport const fetchSingleProduct = async (productId: string) =\u003e {\n  const product = await db.product.findUnique({\n    where: {\n      id: productId,\n    },\n  });\n  if (!product) {\n    redirect('/products');\n  }\n  return product;\n};\n```\n\n### Single Product - Components\n\n- create components/single-product\n  - AddToCart\n  - BreadCrumbs\n  - ProductRating\n\nAddToCart.tsx\n\n```tsx\nimport { Button } from '../ui/button';\n\nfunction AddToCart({ productId }: { productId: string }) {\n  return (\n    \u003cButton className='capitalize mt-8' size='lg'\u003e\n      add to cart\n    \u003c/Button\u003e\n  );\n}\nexport default AddToCart;\n```\n\nBreadCrumbs.tsx\n\n```tsx\nimport {\n  Breadcrumb,\n  BreadcrumbItem,\n  BreadcrumbLink,\n  BreadcrumbList,\n  BreadcrumbPage,\n  BreadcrumbSeparator,\n} from '@/components/ui/breadcrumb';\n\nfunction BreadCrumbs({ name }: { name: string }) {\n  return (\n    \u003cBreadcrumb\u003e\n      \u003cBreadcrumbList\u003e\n        \u003cBreadcrumbItem\u003e\n          \u003cBreadcrumbLink href='/' className='capitalize text-lg'\u003e\n            home\n          \u003c/BreadcrumbLink\u003e\n        \u003c/BreadcrumbItem\u003e\n\n        \u003cBreadcrumbSeparator /\u003e\n        \u003cBreadcrumbItem\u003e\n          \u003cBreadcrumbLink href='/products' className='capitalize text-lg'\u003e\n            products\n          \u003c/BreadcrumbLink\u003e\n        \u003c/BreadcrumbItem\u003e\n\n        \u003cBreadcrumbSeparator /\u003e\n        \u003cBreadcrumbItem\u003e\n          \u003cBreadcrumbPage className='capitalize text-lg'\u003e{name}\u003c/BreadcrumbPage\u003e\n        \u003c/BreadcrumbItem\u003e\n      \u003c/BreadcrumbList\u003e\n    \u003c/Breadcrumb\u003e\n  );\n}\nexport default BreadCrumbs;\n```\n\nProductRating.tsx\n\n```tsx\nimport { FaStar } from 'react-icons/fa';\n\nasync function ProductRating({ productId }: { productId: string }) {\n  const rating = 4.2;\n  const count = 25;\n\n  const className = `flex gap-1 items-center text-md mt-1 mb-4`;\n  const countValue = `(${count}) reviews`;\n  return (\n    \u003cspan className={className}\u003e\n      \u003cFaStar className='w-3 h-3' /\u003e\n      {rating} {countValue}\n    \u003c/span\u003e\n  );\n}\n\nexport default ProductRating;\n```\n\n### Single Product - Page\n\n- create app/products/[id]/page.tsx\n\n```tsx\nimport BreadCrumbs from '@/components/single-product/BreadCrumbs';\nimport { fetchSingleProduct } from '@/utils/actions';\nimport Image from 'next/image';\nimport { formatCurrency } from '@/utils/format';\nimport FavoriteToggleButton from '@/components/products/FavoriteToggleButton';\nimport AddToCart from '@/components/single-product/AddToCart';\nimport ProductRating from '@/components/single-product/ProductRating';\nasync function SingleProductPage({ params }: { params: { id: string } }) {\n  const product = await fetchSingleProduct(params.id);\n  const { name, image, company, description, price } = product;\n  const dollarsAmount = formatCurrency(price);\n  return (\n    \u003csection\u003e\n      \u003cBreadCrumbs name={product.name} /\u003e\n      \u003cdiv className='mt-6 grid gap-y-8 lg:grid-cols-2 lg:gap-x-16'\u003e\n        {/* IMAGE FIRST COL */}\n        \u003cdiv className='relative h-full'\u003e\n          \u003cImage\n            src={image}\n            alt={name}\n            fill\n            sizes='(max-width:768px) 100vw,(max-width:1200px) 50vw,33vw'\n            priority\n            className='w-full rounded-md object-cover'\n          /\u003e\n        \u003c/div\u003e\n        {/* PRODUCT INFO SECOND COL */}\n        \u003cdiv\u003e\n          \u003cdiv className='flex gap-x-8 items-center'\u003e\n            \u003ch1 className='capitalize text-3xl font-bold'\u003e{name}\u003c/h1\u003e\n            \u003cFavoriteToggleButton productId={params.id} /\u003e\n          \u003c/div\u003e\n          \u003cProductRating productId={params.id} /\u003e\n          \u003ch4 className='text-xl mt-2'\u003e{company}\u003c/h4\u003e\n          \u003cp className='mt-3 text-md bg-muted inline-block p-2 rounded-md'\u003e\n            {dollarsAmount}\n          \u003c/p\u003e\n          \u003cp className='mt-6 leading-8 text-muted-foreground'\u003e{description}\u003c/p\u003e\n          \u003cAddToCart productId={params.id} /\u003e\n        \u003c/div\u003e\n      \u003c/div\u003e\n    \u003c/section\u003e\n  );\n}\nexport default SingleProductPage;\n```\n\n### Deploy On Vercel\n\n- create vercel account\n  [Vercel](https://vercel.com)\n- create github repository\n- double check .gitignore\n- update package.json\n\n```json\n\"scripts\": {\n    \"dev\": \"next dev\",\n    \"build\": \"npx prisma generate \u0026\u0026 next build\",\n    \"start\": \"next start\",\n    \"lint\": \"next lint\"\n  },\n```\n\n- push it up to github\n\n```bash\ngit init\ngit add .\ngit commit -m \"first commit\"\n```\n\n- deploy on vercel\n- setup env variables\n\n### Toast Component\n\n[Toast](https://ui.shadcn.com/docs/components/toast)\n\nproviders.tsx\n\n```tsx\n'use client';\nimport { ThemeProvider } from './theme-provider';\nimport { Toaster } from '@/components/ui/toaster';\n\nfunction Providers({ children }: { children: React.ReactNode }) {\n  return (\n    \u003c\u003e\n      \u003cToaster /\u003e\n      \u003cThemeProvider\n        attribute='class'\n        defaultTheme='system'\n        enableSystem\n        disableTransitionOnChange\n      \u003e\n        {children}\n      \u003c/ThemeProvider\u003e\n    \u003c/\u003e\n  );\n}\nexport default Providers;\n```\n\n### Clerk\n\n[Clerk Docs](https://clerk.com/)\n[Clerk + Next.js Setup](https://clerk.com/docs/quickstarts/nextjs)\n\n- create new application\n\n```sh\nnpm install @clerk/nextjs\n```\n\n- create .env.local\n\n```bash\nNEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=\nCLERK_SECRET_KEY=\n```\n\nIn Next.js, environment variables that start with NEXT*PUBLIC* are exposed to the browser. This means they can be accessed in your front-end code.\n\nFor example, NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY can be used in both server-side and client-side code.\n\nOn the other hand, CLERK_SECRET_KEY is a server-side environment variable. It's not exposed to the browser, making it suitable for storing sensitive data like API secrets.\n\nlayout.tsx\n\n```tsx\nimport { ClerkProvider } from '@clerk/nextjs';\n\nreturn (\n  \u003cClerkProvider\u003e\n    \u003chtml lang='en' suppressHydrationWarning\u003e\n      \u003cbody className={inter.className}\u003e\n        \u003cProviders\u003e\n          \u003cNavbar /\u003e\n          \u003cContainer className='py-20'\u003e{children}\u003c/Container\u003e\n        \u003c/Providers\u003e\n      \u003c/body\u003e\n    \u003c/html\u003e\n  \u003c/ClerkProvider\u003e\n);\n```\n\n- create middleware.ts\n\n```ts\nimport { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';\n\nconst isPublicRoute = createRouteMatcher(['/', '/products(.*)', '/about']);\n\nexport default clerkMiddleware((auth, req) =\u003e {\n  if (!isPublicRoute(req)) auth().protect();\n});\n\nexport const config = {\n  matcher: ['/((?!.*\\\\..*|_next).*)', '/', '/(api|trpc)(.*)'],\n};\n```\n\n- restart dev server\n\n### SignUp/SignIn and Customize Avatar (optional)\n\n- customization\n  - avatars\n\n### SignOutButton Component\n\n```tsx\n'use client';\nimport { SignOutButton } from '@clerk/nextjs';\nimport { useToast } from '../ui/use-toast';\nimport Link from 'next/link';\n\nfunction SignOutLink() {\n  const { toast } = useToast();\n  const handleLogout = () =\u003e {\n    toast({ description: 'Logging Out...' });\n  };\n  return (\n    \u003cSignOutButton\u003e\n      \u003cLink href='/' className='w-full text-left' onClick={handleLogout}\u003e\n        Logout\n      \u003c/Link\u003e\n    \u003c/SignOutButton\u003e\n  );\n}\nexport default SignOutLink;\n```\n\n### UserIcon Component\n\n```tsx\nimport { LuUser2 } from 'react-icons/lu';\nimport { currentUser } from '@clerk/nextjs/server';\nasync function UserIcon() {\n  const user = await currentUser();\n  const profileImage = user?.imageUrl;\n  if (profileImage)\n    return (\n      \u003cimg src={profileImage} className='w-6 h-6 rounded-full object-cover' /\u003e\n    );\n  return \u003cLuUser2 className='w-6 h-6 bg-primary rounded-full text-white' /\u003e;\n}\nexport default UserIcon;\n```\n\n### LinksDropdown - Complete\n\n```tsx\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n  DropdownMenuSeparator,\n} from '@/components/ui/dropdown-menu';\nimport { LuAlignLeft } from 'react-icons/lu';\nimport Link from 'next/link';\nimport { Button } from '../ui/button';\nimport { links } from '@/utils/links';\nimport UserIcon from './UserIcon';\nimport SignOutLink from './SignOutLink';\nimport { SignInButton, SignUpButton, SignedIn, SignedOut } from '@clerk/nextjs';\n\nfunction LinksDropdown() {\n  return (\n    \u003cDropdownMenu\u003e\n      \u003cDropdownMenuTrigger asChild\u003e\n        \u003cButton variant='outline' className='flex gap-4 max-w-[100px]'\u003e\n          \u003cLuAlignLeft className='w-6 h-6' /\u003e\n          \u003cUserIcon /\u003e\n        \u003c/Button\u003e\n      \u003c/DropdownMenuTrigger\u003e\n      \u003cDropdownMenuContent className='w-48' align='start' sideOffset={10}\u003e\n        \u003cSignedOut\u003e\n          \u003cDropdownMenuItem\u003e\n            \u003cSignInButton mode='modal'\u003e\n              \u003cbutton className='w-full text-left'\u003eLogin\u003c/button\u003e\n            \u003c/SignInButton\u003e\n          \u003c/DropdownMenuItem\u003e\n          \u003cDropdownMenuSeparator /\u003e\n          \u003cDropdownMenuItem\u003e\n            \u003cSignUpButton mode='modal'\u003e\n              \u003cbutton className='w-full text-left'\u003eRegister\u003c/button\u003e\n            \u003c/SignUpButton\u003e\n          \u003c/DropdownMenuItem\u003e\n        \u003c/SignedOut\u003e\n        \u003cSignedIn\u003e\n          {links.map((link) =\u003e {\n            return (\n              \u003cDropdownMenuItem key={link.href}\u003e\n                \u003cLink href={link.href} className='capitalize w-full'\u003e\n                  {link.label}\n                \u003c/Link\u003e\n              \u003c/DropdownMenuItem\u003e\n            );\n          })}\n          \u003cDropdownMenuSeparator /\u003e\n          \u003cDropdownMenuItem\u003e\n            \u003cSignOutLink /\u003e\n          \u003c/DropdownMenuItem\u003e\n        \u003c/SignedIn\u003e\n      \u003c/DropdownMenuContent\u003e\n    \u003c/DropdownMenu\u003e\n  );\n}\nexport default LinksDropdown;\n```\n\n### Admin Links\n\n- utils/links.ts\n\n```ts\ntype NavLink = {\n  href: string;\n  label: string;\n};\n\nexport const links: NavLink[] = [\n  { href: '/', label: 'home' },\n  { href: '/about', label: 'about' },\n  { href: '/products', label: 'products' },\n  { href: '/favorites', label: 'favorites' },\n  { href: '/cart', label: 'cart' },\n  { href: '/orders', label: 'orders' },\n  { href: '/admin/sales', label: 'dashboard' },\n];\n\nexport const adminLinks: NavLink[] = [\n  { href: '/admin/sales', label: 'sales' },\n  { href: '/admin/products', label: 'my products' },\n  { href: '/admin/products/create', label: 'create product' },\n];\n```\n\n### Admin Pages\n\n- remove existing page.tsx\n\n- admin\n  - products\n    - [id]/edit/page.tsx\n    - create/page.tsx\n    - page.tsx\n  - sales/page.tsx\n  - layout.tsx\n  - Sidebar.tsx\n\nSidebar.tsx\n\n```tsx\n'use client';\nimport { adminLinks } from '@/utils/links';\nimport Link from 'next/link';\nimport { usePathname } from 'next/navigation';\nimport { Button } from '@/components/ui/button';\n\nfunction Sidebar() {\n  const pathname = usePathname();\n\n  return (\n    \u003caside\u003e\n      {adminLinks.map((link) =\u003e {\n        const isActivePage = pathname === link.href;\n        const variant = isActivePage ? 'default' : 'ghost';\n        return (\n          \u003cButton\n            asChild\n            className='w-full mb-2 capitalize font-normal justify-start'\n            variant={variant}\n          \u003e\n            \u003cLink key={link.href} href={link.href}\u003e\n              {link.label}\n            \u003c/Link\u003e\n          \u003c/Button\u003e\n        );\n      })}\n    \u003c/aside\u003e\n  );\n}\nexport default Sidebar;\n```\n\nlayout.tsx\n\n```tsx\nimport { Separator } from '@/components/ui/separator';\nimport Sidebar from './Sidebar';\n\nfunction DashboardLayout({ children }: { children: React.ReactNode }) {\n  return (\n    \u003c\u003e\n      \u003ch2 className='text-2xl pl-4'\u003eDashboard\u003c/h2\u003e\n      \u003cSeparator className='mt-2' /\u003e\n      \u003csection className='grid lg:grid-cols-12 gap-12 mt-12'\u003e\n        \u003cdiv className='lg:col-span-2'\u003e\n          \u003cSidebar /\u003e\n        \u003c/div\u003e\n        \u003cdiv className='lg:col-span-10 px-4'\u003e{children}\u003c/div\u003e\n      \u003c/section\u003e\n    \u003c/\u003e\n  );\n}\nexport default DashboardLayout;\n```\n\n### Restrict Access - Middleware\n\n```ts\nimport { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';\nimport { NextResponse } from 'next/server';\n\nconst isPublicRoute = createRouteMatcher(['/', '/products(.*)', '/about']);\nconst isAdminRoute = createRouteMatcher(['/admin(.*)']);\n\nexport default clerkMiddleware(async (auth, req) =\u003e {\n  // console.log(auth().userId);\n\n  const isAdminUser = auth().userId === process.env.ADMIN_USER_ID;\n\n  if (isAdminRoute(req) \u0026\u0026 !isAdminUser) {\n    return NextResponse.redirect(new URL('/', req.url));\n  }\n  if (!isPublicRoute(req)) auth().protect();\n});\n\nexport const config = {\n  matcher: ['/((?!.*\\\\..*|_next).*)', '/', '/(api|trpc)(.*)'],\n};\n```\n\n- add userId to .env\n\n```sh\nADMIN_USER_ID=\n```\n\n### Restrict Access - LinksDropdown\n\n```tsx\nimport { auth } from '@clerk/nextjs/server';\nfunction LinksDropdown() {\n  const { userId } = auth();\n  const isAdmin = userId === process.env.ADMIN_USER_ID;\n  return (\n    \u003c\u003e\n      {links.map((link) =\u003e {\n        if (link.label === 'dashboard' \u0026\u0026 !isAdmin) return null;\n        return (\n          \u003cDropdownMenuItem key={link.href}\u003e\n            \u003cLink href={link.href} className='capitalize w-full'\u003e\n              {link.label}\n            \u003c/Link\u003e\n          \u003c/DropdownMenuItem\u003e\n        );\n      })}\n    \u003c/\u003e\n  );\n}\n```\n\n### Create Product - Setup\n\n```tsx\nimport { Label } from '@/components/ui/label';\nimport { Input } from '@/components/ui/input';\nimport { Button } from '@/components/ui/button';\n\nconst createProductAction = async (formData: FormData) =\u003e {\n  'use server';\n  const name = formData.get('name') as string;\n  console.log(name);\n};\n\nfunction CreateProductPage() {\n  return (\n    \u003csection\u003e\n      \u003ch1 className='text-2xl font-semibold mb-8 capitalize'\u003ecreate product\u003c/h1\u003e\n      \u003cdiv className='border p-8 rounded-md'\u003e\n        \u003cform action={createProductAction}\u003e\n          \u003cdiv className='mb-2'\u003e\n            \u003cLabel htmlFor='name'\u003eProduct Name\u003c/Label\u003e\n            \u003cInput id='name' name='name' type='text' /\u003e\n          \u003c/div\u003e\n          \u003cButton type='submit' size='lg'\u003e\n            Submit\n          \u003c/Button\u003e\n        \u003c/form\u003e\n      \u003c/div\u003e\n    \u003c/section\u003e\n  );\n}\nexport default CreateProductPage;\n```\n\n### Faker Library\n\n```sh\nnpm install @faker-js/faker --save-dev\n```\n\n[Docs](https://fakerjs.dev/guide/)\n\n```tsx\nimport { faker } from '@faker-js/faker';\n\nfunction CreateProductPage() {\n  const name = faker.commerce.productName();\n  const company = faker.company.name();\n  const description = faker.lorem.paragraph({ min: 10, max: 12 });\n\n  return \u003cInput id='name' name='name' type='text' defaultValue={name} /\u003e;\n}\nexport default CreateProductPage;\n```\n\n### Form Components - Setup\n\n- components/form\n  - Buttons\n  - CheckBoxInput\n  - FormContainer\n  - FormInput\n  - ImageInput\n  - ImageInputContainer\n  - PriceInput\n  - TextAreaInput\n\nFormInput.tsx\n\n```tsx\nimport { Label } from '../ui/label';\nimport { Input } from '../ui/input';\n\ntype FormInputProps = {\n  name: string;\n  type: string;\n  label?: string;\n  defaultValue?: string;\n  placeholder?: string;\n};\n\nfunction FormInput({\n  label,\n  name,\n  type,\n  defaultValue,\n  placeholder,\n}: FormInputProps) {\n  return (\n    \u003cdiv className='mb-2'\u003e\n      \u003cLabel htmlFor={name} className='capitalize'\u003e\n        {label || name}\n      \u003c/Label\u003e\n      \u003cInput\n        id={name}\n        name={name}\n        type={type}\n        defaultValue={defaultValue}\n        placeholder={placeholder}\n        required\n      /\u003e\n    \u003c/div\u003e\n  );\n}\n\nexport default FormInput;\n```\n\n### PriceInput Component\n\n```tsx\nimport { Label } from '../ui/label';\nimport { Input } from '../ui/input';\n\nconst name = 'price';\ntype FormInputNumberProps = {\n  defaultValue?: number;\n};\n\nfunction PriceInput({ defaultValue }: FormInputNumberProps) {\n  return (\n    \u003cdiv className='mb-2'\u003e\n      \u003cLabel htmlFor='price' className='capitalize'\u003e\n        Price ($)\n      \u003c/Label\u003e\n      \u003cInput\n        id={name}\n        type='number'\n        name={name}\n        min={0}\n        defaultValue={defaultValue || 100}\n        required\n      /\u003e\n    \u003c/div\u003e\n  );\n}\nexport default PriceInput;\n```\n\n### ImageInput Component\n\n```tsx\nimport { Label } from '../ui/label';\nimport { Input } from '../ui/input';\n\nfunction ImageInput() {\n  const name = 'image';\n  return (\n    \u003cdiv className='mb-2'\u003e\n      \u003cLabel htmlFor={name} className='capitalize'\u003e\n        Image\n      \u003c/Label\u003e\n      \u003cInput id={name} name={name} type='file' required accept='image/*' /\u003e\n    \u003c/div\u003e\n  );\n}\nexport default ImageInput;\n```\n\n### TextAreaInput Component\n\n```tsx\nimport { Label } from '@/components/ui/label';\nimport { Textarea } from '@/components/ui/textarea';\n\ntype TextAreaInputProps = {\n  name: string;\n  labelText?: string;\n  defaultValue?: string;\n};\n\nfunction TextAreaInput({ name, labelText, defaultValue }: TextAreaInputProps) {\n  return (\n    \u003cdiv className='mb-2'\u003e\n      \u003cLabel htmlFor={name} className='capitalize'\u003e\n        {labelText || name}\n      \u003c/Label\u003e\n      \u003cTextarea\n        id={name}\n        name={name}\n        defaultValue={defaultValue}\n        rows={5}\n        required\n        className='leading-loose'\n      /\u003e\n    \u003c/div\u003e\n  );\n}\n\nexport default TextAreaInput;\n```\n\n### CheckBoxInput Component\n\n```tsx\n'use client';\n\nimport { Checkbox } from '@/components/ui/checkbox';\n\ntype CheckboxInputProps = {\n  name: string;\n  label: string;\n  defaultChecked?: boolean;\n};\n\nexport default function CheckboxInput({\n  name,\n  label,\n  defaultChecked = false,\n}: CheckboxInputProps) {\n  return (\n    \u003cdiv className='flex items-center space-x-2'\u003e\n      \u003cCheckbox id={name} name={name} defaultChecked={defaultChecked} /\u003e\n      \u003clabel\n        htmlFor={name}\n        className='text-sm leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 capitalize'\n      \u003e\n        {label}\n      \u003c/label\u003e\n    \u003c/div\u003e\n  );\n}\n```\n\n### Submit Button\n\ncomponents/form/Buttons.tsx\n\n```tsx\n'use client';\n\nimport { ReloadIcon } from '@radix-ui/react-icons';\nimport { useFormStatus } from 'react-dom';\nimport { Button } from '@/components/ui/button';\nimport { cn } from '@/lib/utils';\nimport { SignInButton } from '@clerk/nextjs';\nimport { FaRegHeart, FaHeart } from 'react-icons/fa';\nimport { LuTrash2, LuPenSquare } from 'react-icons/lu';\n\ntype btnSize = 'default' | 'lg' | 'sm';\n\ntype SubmitButtonProps = {\n  className?: string;\n  text?: string;\n  size?: btnSize;\n};\n\nexport function SubmitButton({\n  className = '',\n  text = 'submit',\n  size = 'lg',\n}: SubmitButtonProps) {\n  const { pending } = useFormStatus();\n\n  return (\n    \u003cButton\n      type='submit'\n      disabled={pending}\n      className={cn('capitalize', className)}\n      size={size}\n    \u003e\n      {pending ? (\n        \u003c\u003e\n          \u003cReloadIcon className='mr-2 h-4 w-4 animate-spin' /\u003e\n          Please wait...\n        \u003c/\u003e\n      ) : (\n        text\n      )}\n    \u003c/Button\u003e\n  );\n}\n```\n\n### FormContainer Component\n\n- create utils/types.ts\n\n```ts\nexport type actionFunction = (\n  prevState: any,\n  formData: FormData\n) =\u003e Promise\u003c{ message: string }\u003e;\n\nexport type CartItem = {\n  productId: string;\n  image: string;\n  title: string;\n  price: string;\n  amount: number;\n  company: string;\n};\n\nexport type CartState = {\n  cartItems: CartItem[];\n  numItemsInCart: number;\n  cartTotal: number;\n  shipping: number;\n  tax: number;\n  orderTotal: number;\n};\n```\n\nFormContainer.tsx\n\n```tsx\n'use client';\n\nimport { useFormState } from 'react-dom';\nimport { useEffect } from 'react';\nimport { useToast } from '@/components/ui/use-toast';\nimport { actionFunction } from '@/utils/types';\n\nconst initialState = {\n  message: '',\n};\n\nfunction FormContainer({\n  action,\n  children,\n}: {\n  action: actionFunction;\n  children: React.ReactNode;\n}) {\n  const [state, formAction] = useFormState(action, initialState);\n  const { toast } = useToast();\n  useEffect(() =\u003e {\n    if (state.message) {\n      toast({ description: state.message });\n    }\n  }, [state]);\n  return \u003cform action={formAction}\u003e{children}\u003c/form\u003e;\n}\nexport default FormContainer;\n```\n\n### Create Product Page - Complete\n\n- actions.ts\n\n```ts\n'use server';\n\nexport const createProductAction = async (\n  prevState: any,\n  formData: FormData\n): Promise\u003c{ message: string }\u003e =\u003e {\n  return { message: 'product created' };\n};\n```\n\npage.tsx\n\n```tsx\nimport FormInput from '@/components/form/FormInput';\nimport { SubmitButton } from '@/components/form/Buttons';\nimport FormContainer from '@/components/form/FormContainer';\nimport { createProductAction } from '@/utils/actions';\nimport ImageInput from '@/components/form/ImageInput';\nimport PriceInput from '@/components/form/PriceInput';\nimport TextAreaInput from '@/components/form/TextAreaInput';\nimport { faker } from '@faker-js/faker';\nimport CheckboxInput from '@/components/form/CheckboxInput';\n\nfunction CreateProduct() {\n  const name = faker.commerce.productName();\n  const company = faker.company.name();\n  // const description = faker.commerce.productDescription();\n  const description = faker.lorem.paragraph({ min: 10, max: 12 });\n\n  return (\n    \u003csection\u003e\n      \u003ch1 className='text-2xl font-semibold mb-8 capitalize'\u003ecreate product\u003c/h1\u003e\n      \u003cdiv className='border p-8 rounded-md'\u003e\n        \u003cFormContainer action={createProductAction}\u003e\n          \u003cdiv className='grid gap-4 md:grid-cols-2 my-4'\u003e\n            \u003cFormInput\n              type='text'\n              name='name'\n              label='product name'\n              defaultValue={name}\n            /\u003e\n            \u003cFormInput\n              type='text'\n              name='company'\n              label='company'\n              defaultValue={company}\n            /\u003e\n            \u003cPriceInput /\u003e\n            \u003cImageInput /\u003e\n          \u003c/div\u003e\n          \u003cTextAreaInput\n            name='description'\n            labelText='product description'\n            defaultValue={description}\n          /\u003e\n          \u003cdiv className='mt-6'\u003e\n            \u003cCheckboxInput name='featured' label='featured' /\u003e\n          \u003c/div\u003e\n\n          \u003cSubmitButton text='Create Product' className='mt-8' /\u003e\n        \u003c/FormContainer\u003e\n      \u003c/div\u003e\n    \u003c/section\u003e\n  );\n}\nexport default CreateProduct;\n```\n\n### Helper Functions\n\n- actions.ts\n\n```ts\nimport { auth, currentUser } from '@clerk/nextjs/server';\n\nconst renderError = (error: unknown): { message: string } =\u003e {\n  console.log(error);\n  return {\n    message: error instanceof Error ? error.message : 'An error occurred',\n  };\n};\n\nconst getAuthUser = async () =\u003e {\n  const user = await currentUser();\n  if (!user) {\n    throw new Error('You must be logged in to access this route');\n  }\n  return user;\n};\n```\n\n### CreateProductAction - First Approach\n\n- get/store product images in public/images\n\n```ts\nexport const createProductAction = async (\n  prevState: any,\n  formData: FormData\n): Promise\u003c{ message: string }\u003e =\u003e {\n  const user = await getAuthUser();\n\n  try {\n    const name = formData.get('name') as string;\n    const company = formData.get('company') as string;\n    const price = Number(formData.get('price') as string);\n    const image = formData.get('image') as File;\n    const description = formData.get('description') as string;\n    const featured = Boolean(formData.get('featured') as string);\n\n    await db.product.create({\n      data: {\n        name,\n        company,\n        price,\n        image: '/images/product-1.jpg',\n        description,\n        featured,\n        clerkId: user.id,\n      },\n    });\n    return { message: 'product created' };\n  } catch (error) {\n    return renderError(error);\n  }\n};\n```\n\n### Problems\n\n- lots of code code just to access input values\n- no validation (only html one)\n\n### Zod\n\nZod is a JavaScript library for building schemas and validating data, providing type safety and error handling.\n\n```sh\nnpm install zod\n```\n\n[Docs](https://zod.dev/?id=basic-usage)\n\n- setup utils/schemas.ts\n\n```ts\nimport { z, ZodSchema } from 'zod';\n\nexport const productSchema = z.object({\n  name: z.string().min(4),\n  company: z.string().min(4),\n  price: z.coerce.number().int().min(0),\n  description: z.string(),\n  featured: z.coerce.boolean(),\n});\n```\n\n- actions.ts\n\n```ts\nexport const createProductAction = async (\n  prevState: any,\n  formData: FormData\n): Promise\u003c{ message: string }\u003e =\u003e {\n  const user = await getAuthUser();\n\n  try {\n    const rawData = Object.fromEntries(formData);\n    const validatedFields = productSchema.parse(rawData);\n\n    await db.product.create({\n      data: {\n        ...validatedFields,\n        image: '/images/product-1.jpg',\n        clerkId: user.id,\n      },\n    });\n    return { message: 'product created' };\n  } catch (error) {\n    return renderError(error);\n  }\n};\n```\n\n### Problem\n\n- error messages are not user friendly\n\nschemas.ts\n\n```ts\nimport { z, ZodSchema } from 'zod';\n\nexport const productSchema = z.object({\n  name: z\n    .string()\n    .min(2, {\n      message: 'name must be at least 2 characters.',\n    })\n    .max(100, {\n      message: 'name must be less than 100 characters.',\n    }),\n  company: z.string(),\n  featured: z.coerce.boolean(),\n  price: z.coerce.number().int().min(0, {\n    message: 'price must be a positive number.',\n  }),\n  description: z.string().refine(\n    (description) =\u003e {\n      const wordCount = description.split(' ').length;\n      return wordCount \u003e= 10 \u0026\u0026 wordCount \u003c= 1000;\n    },\n    {\n      message: 'description must be between 10 and 1000 words.',\n    }\n  ),\n});\n```\n\n```ts\ntry {\n    const rawData = Object.fromEntries(formData);\n    const validatedFields = productSchema.safeParse(rawData);\n\n    if (!validatedFields.success) {\n      const errors = validatedFields.error.errors.map((error) =\u003e error.message);\n      throw new Error(errors.join(','));\n    }\n\n    await db.product.create({\n      data: {\n        ...validatedFields.data,\n        image: '/images/product-1.jpg',\n        clerkId: user.id,\n      },\n    });\n    return { message: 'product created' };\n  }\n```\n\n### ValidateWithZodSchema\n\nschemas.ts\n\n```ts\nexport function validateWithZodSchema\u003cT\u003e(\n  schema: ZodSchema\u003cT\u003e,\n  data: unknown\n): T {\n  const result = schema.safeParse(data);\n  if (!result.success) {\n    const errors = result.error.errors.map((error) =\u003e error.message);\n    throw new Error(errors.join(', '));\n  }\n  return result.data;\n}\n```\n\nactions.ts\n\n```ts\ntry {\n    const rawData = Object.fromEntries(formData);\n    const validatedFields = validateWithZodSchema(productSchema, rawData);\n\n    await db.product.create({\n      data: {\n        ...validatedFields,\n        image: '/images/product-1.jpg',\n        clerkId: user.id,\n      },\n    });\n    return { message: 'product created' };\n  }\n```\n\n### Image Upload\n\nschemas.ts\n\n```ts\nexport const imageSchema = z.object({\n  image: validateImageFile(),\n});\n\nfunction validateImageFile() {\n  const maxUploadSize = 1024 * 1024;\n  const acceptedFileTypes = ['image/'];\n  return z\n    .instanceof(File)\n    .refine((file) =\u003e {\n      return !file || file.size \u003c= maxUploadSize;\n    }, `File size must be less than 1 MB`)\n    .refine((file) =\u003e {\n      return (\n        !file || acceptedFileTypes.some((type) =\u003e file.type.startsWith(type))\n      );\n    }, 'File must be an image');\n}\n```\n\nactions.ts\n\n```ts\ntry {\n    const rawData = Object.fromEntries(formData);\n    const file = formData.get('image') as File;\n    const validatedFields = validateWithZodSchema(productSchema, rawData);\n    const validatedFile = validateWithZodSchema(imageSchema, { image: file });\n    console.log(validatedFile);\n\n    await db.product.create({\n      data: {\n        ...validatedFields,\n        image: '/images/product-1.jpg',\n        clerkId: user.id,\n      },\n    });\n    return { message: 'product created' };\n  }\n```\n\n### Create Bucket, Setup Policy and API Keys\n\n```env\nSUPABASE_URL=\nSUPABASE_KEY=\n```\n\n### Setup Supabase\n\n```sh\nnpm install @supabase/supabase-js\n```\n\nutils/supabase.ts\n\n```ts\nimport { createClient } from '@supabase/supabase-js';\n\nconst bucket = 'your-bucket-name';\n\n// Create a single supabase client for interacting with your database\nexport const supabase = createClient(\n  process.env.SUPABASE_URL as string,\n  process.env.SUPABASE_KEY as string\n);\n\nexport const uploadImage = async (image: File) =\u003e {\n  const timestamp = Date.now();\n  // const newName = `/users/${timestamp}-${image.name}`;\n  const newName = `${timestamp}-${image.name}`;\n\n  const { data, error } = await supabase.storage\n    .from(bucket)\n    .upload(newName, image, {\n      cacheControl: '3600',\n    });\n  if (!data) throw new Error('Image upload failed');\n  return supabase.storage.from(bucket).getPublicUrl(newName).data.publicUrl;\n};\n```\n\n### Create Product Action - Complete\n\n- actions.ts\n\n```ts\nexport const createProductAction = async (\n  prevState: any,\n  formData: FormData\n): Promise\u003c{ message: string }\u003e =\u003e {\n  const user = await getAuthUser();\n\n  try {\n    const rawData = Object.fromEntries(formData);\n    const file = formData.get('image') as File;\n    const validatedFields = validateWithZodSchema(productSchema, rawData);\n    const validatedFile = validateWithZodSchema(imageSchema, { image: file });\n    const fullPath = await uploadImage(validatedFile.image);\n\n    await db.product.create({\n      data: {\n        ...validatedFields,\n        image: fullPath,\n        clerkId: user.id,\n      },\n    });\n  } catch (error) {\n    return renderError(error);\n  }\n  redirect('/admin/products');\n};\n```\n\n- add supabase url to remote patterns\n\nnext.config.mjs\n\n```tsx\n/** @type {import('next').NextConfig} */\nconst nextConfig = {\n  images: {\n    remotePatterns: [\n      {\n        protocol: 'https',\n        hostname: 'images.pexels.com',\n      },\n      {\n        protocol: 'https',\n        hostname: 'pldbjxhkrlailuixuvhz.supabase.co',\n      },\n    ],\n  },\n};\n\nexport default nextConfig;\n```\n\n### Fetch Products - Admin\n\n- actions.ts\n\n```ts\nconst getAdminUser = async () =\u003e {\n  const user = await getAuthUser();\n  if (user.id !== process.env.ADMIN_USER_ID) redirect('/');\n  return user;\n};\n// refactor createProductAction\n\nexport const fetchAdminProducts = async () =\u003e {\n  await getAdminUser();\n  const products = await db.product.findMany({\n    orderBy: {\n      createdAt: 'desc',\n    },\n  });\n  return products;\n};\n```\n\n### Admin Products Page\n\n- app/admin/products/page.tsx\n\n```tsx\nimport EmptyList from '@/components/global/EmptyList';\nimport { fetchAdminProducts } from '@/utils/actions';\nimport Link from 'next/link';\n\nimport { formatCurrency } from '@/utils/format';\nimport {\n  Table,\n  TableBody,\n  TableCaption,\n  TableCell,\n  TableHead,\n  TableHeader,\n  TableRow,\n} from '@/components/ui/table';\n\nasync function ItemsPage() {\n  const items = await fetchAdminProducts();\n  if (items.length === 0) return \u003cEmptyList /\u003e;\n  return (\n    \u003csection\u003e\n      \u003cTable\u003e\n        \u003cTableCaption className='capitalize'\u003e\n          total products : {items.length}\n        \u003c/TableCaption\u003e\n        \u003cTableHeader\u003e\n          \u003cTableRow\u003e\n            \u003cTableHead\u003eProduct Name\u003c/TableHead\u003e\n            \u003cTableHead\u003eCompany\u003c/TableHead\u003e\n            \u003cTableHead\u003ePrice\u003c/TableHead\u003e\n            \u003cTableHead\u003eActions\u003c/TableHead\u003e\n          \u003c/TableRow\u003e\n        \u003c/TableHeader\u003e\n        \u003cTableBody\u003e\n          {items.map((item) =\u003e {\n            const { id: productId, name, company, price } = item;\n            return (\n              \u003cTableRow key={productId}\u003e\n                \u003cTableCell\u003e\n                  \u003cLink\n                    href={`/products/${productId}`}\n                    className='underline text-muted-foreground tracking-wide capitalize'\n                  \u003e\n                    {name}\n                  \u003c/Link\u003e\n                \u003c/TableCell\u003e\n                \u003cTableCell\u003e{company}\u003c/TableCell\u003e\n                \u003cTableCell\u003e{formatCurrency(price)}\u003c/TableCell\u003e\n\n                \u003cTableCell className='flex items-center gap-x-2'\u003e\u003c/TableCell\u003e\n              \u003c/TableRow\u003e\n            );\n          })}\n        \u003c/TableBody\u003e\n      \u003c/Table\u003e\n    \u003c/section\u003e\n  );\n}\n\nexport default ItemsPage;\n```\n\n### Icon Button\n\n```tsx\ntype actionType = 'edit' | 'delete';\nexport const IconButton = ({ actionType }: { actionType: actionType }) =\u003e {\n  const { pending } = useFormStatus();\n\n  const renderIcon = () =\u003e {\n    switch (actionType) {\n      case 'edit':\n        return \u003cLuPenSquare /\u003e;\n      case 'delete':\n        return \u003cLuTrash2 /\u003e;\n      default:\n        const never: never = actionType;\n        throw new Error(`Invalid action type: ${never}`);\n    }\n  };\n\n  return (\n    \u003cButton\n      type='submit'\n      size='icon'\n      variant='link'\n      className='p-2 cursor-pointer'\n    \u003e\n      {pending ? \u003cReloadIcon className=' animate-spin' /\u003e : renderIcon()}\n    \u003c/Button\u003e\n  );\n};\n```\n\n### Delete Product Action\n\n- actions.ts\n\n```ts\nimport { revalidatePath } from 'next/cache';\n\nexport const deleteProductAction = async (prevState: { productId: string }) =\u003e {\n  const { productId } = prevState;\n  await getAdminUser();\n\n  try {\n    await db.product.delete({\n      where: {\n        id: productId,\n      },\n    });\n\n    revalidatePath('/admin/products');\n    return { message: 'product removed' };\n  } catch (error) {\n    return renderError(error);\n  }\n};\n```\n\n### Admin Products Page - Complete\n\n```tsx\nimport FormContainer from '@/components/form/FormContainer';\nimport { IconButton } from '@/components/form/Buttons';\nimport { deleteProductAction } from '@/utils/actions';\n\nreturn (\n  \u003c\u003e\n    \u003cTableCell className='flex items-center gap-x-2'\u003e\n      \u003cLink href={`/admin/products/${productId}/edit`}\u003e\n        \u003cIconButton actionType='edit'\u003e\u003c/IconButton\u003e\n      \u003c/Link\u003e\n      \u003cDeleteProduct productId={productId} /\u003e\n    \u003c/TableCell\u003e\n  \u003c/\u003e\n);\n\nfunction DeleteProduct({ productId }: { productId: string }) {\n  const deleteProduct = deleteProductAction.bind(null, { productId });\n  return (\n    \u003cFormContainer action={deleteProduct}\u003e\n      \u003cIconButton actionType='delete' /\u003e\n    \u003c/FormContainer\u003e\n  );\n}\n```\n\n### Remove Image From Supabase\n\n- utils/supabase.ts\n\n```ts\nexport const deleteImage = (url: string) =\u003e {\n  const imageName = url.split('/').pop();\n  if (!imageName) throw new Error('Invalid URL');\n  return supabase.storage.from(bucket).remove([imageName]);\n};\n```\n\n```ts\nexport const deleteProductAction = async (prevState: { productId: string }) =\u003e {\n  const { productId } = prevState;\n  await getAdminUser();\n  try {\n    const product = await db.product.delete({\n      where: {\n        id: productId,\n      },\n    });\n    await deleteImage(product.image);\n    revalidatePath('/admin/products');\n    return { message: 'product removed' };\n  } catch (error) {\n    return renderError(error);\n  }\n};\n```\n\n### FetchAdminProductDetails, UpdateProductAction and updateProductImageAction\n\n- actions.ts\n\n```ts\nexport const fetchAdminProductDetails = async (productId: string) =\u003e {\n  await getAdminUser();\n  const product = await db.product.findUnique({\n    where: {\n      id: productId,\n    },\n  });\n  if (!product) redirect('/admin/products');\n  return product;\n};\n\nexport const updateProductAction = async (\n  prevState: any,\n  formData: FormData\n) =\u003e {\n  return { message: 'Product updated successfully' };\n};\nexport const updateProductImageAction = async (\n  prevState: any,\n  formData: FormData\n) =\u003e {\n  return { message: 'Product Image updated successfully' };\n};\n```\n\n### Edit Product Page\n\n- app/admin/products/[id]/edit/page.tsx\n\n```tsx\nimport { fetchAdminProductDetails, updateProductAction } from '@/utils/actions';\nimport FormContainer from '@/components/form/FormContainer';\nimport FormInput from '@/components/form/FormInput';\nimport PriceInput from '@/components/form/PriceInput';\nimport TextAreaInput from '@/components/form/TextAreaInput';\nimport { SubmitButton } from '@/components/form/Buttons';\nimport CheckboxInput from '@/components/form/CheckboxInput';\nasync function EditProductPage({ params }: { params: { id: string } }) {\n  const { id } = params;\n  const product = await fetchAdminProductDetails(id);\n  const { name, company, description, featured, price } = product;\n  return (\n    \u003csection\u003e\n      \u003ch1 className='text-2xl font-semibold mb-8 capitalize'\u003eupdate product\u003c/h1\u003e\n      \u003cdiv className='border p-8 rounded-md'\u003e\n        {/* Image Input Container */}\n        \u003cFormContainer action={updateProductAction}\u003e\n          \u003cdiv className='grid gap-4 md:grid-cols-2 my-4'\u003e\n            \u003cinput type='hidden' name='id' value={id} /\u003e\n            \u003cFormInput\n              type='text'\n              name='name'\n              label='product name'\n              defaultValue={name}\n            /\u003e\n            \u003cFormInput\n              type='text'\n              name='company'\n              label='company'\n              defaultValue={company}\n            /\u003e\n\n            \u003cPriceInput defaultValue={price} /\u003e\n          \u003c/div\u003e\n          \u003cTextAreaInput\n            name='description'\n            labelText='product description'\n            defaultValue={description}\n          /\u003e\n          \u003cdiv className='mt-6'\u003e\n            \u003cCheckboxInput\n              name='featured'\n              label='featured'\n              defaultChecked={featured}\n            /\u003e\n          \u003c/div\u003e\n          \u003cSubmitButton text='update product' className='mt-8' /\u003e\n        \u003c/FormContainer\u003e\n      \u003c/div\u003e\n    \u003c/section\u003e\n  );\n}\nexport default EditProductPage;\n```\n\n### UpdateProductAction\n\nactions.ts\n\n```ts\nexport const updateProductAction = async (\n  prevState: any,\n  formData: FormData\n) =\u003e {\n  await getAdminUser();\n  try {\n    const productId = formData.get('id') as string;\n    const rawData = Object.fromEntries(formData);\n\n    const validatedFields = validateWithZodSchema(productSchema, rawData);\n\n    await db.product.update({\n      where: {\n        id: productId,\n      },\n      data: {\n        ...validatedFields,\n      },\n    });\n    revalidatePath(`/admin/products/${productId}/edit`);\n    return { message: 'Product updated successfully' };\n  } catch (error) {\n    return renderError(error);\n  }\n};\n```\n\n### UpdateImageContainer Component\n\n```tsx\n'use client';\nimport { useState } from 'react';\nimport Image from 'next/image';\nimport { Button } from '../ui/button';\nimport FormContainer from './FormContainer';\nimport ImageInput from './ImageInput';\nimport { SubmitButton } from './Buttons';\nimport { type actionFunction } from '@/utils/types';\n\ntype ImageInputContainerProps = {\n  image: string;\n  name: string;\n  action: actionFunction;\n  text: string;\n  children?: React.ReactNode;\n};\n\nfunction ImageInputContainer(props: ImageInputContainerProps) {\n  const { image, name, action, text } = props;\n  const [isUpdateFormVisible, setUpdateFormVisible] = useState(false);\n\n  return (\n    \u003cdiv className='mb-8'\u003e\n      \u003cImage\n        src={image}\n        width={200}\n        height={200}\n        className='rounded-md object-cover mb-4 w-[200px] h-[200px]'\n        alt={name}\n      /\u003e\n\n      \u003cButton\n        variant='outline'\n        size='sm'\n        onClick={() =\u003e setUpdateFormVisible((prev) =\u003e !prev)}\n      \u003e\n        {text}\n      \u003c/Button\u003e\n      {isUpdateFormVisible \u0026\u0026 (\n        \u003cdiv className='max-w-md mt-4'\u003e\n          \u003cFormContainer action={action}\u003e\n            {props.children}\n            \u003cImageInput /\u003e\n            \u003cSubmitButton size='sm' /\u003e\n          \u003c/FormContainer\u003e\n        \u003c/div\u003e\n      )}\n    \u003c/div\u003e\n  );\n}\nexport default ImageInputContainer;\n```\n\nEditProductPage.tsx\n\n```tsx\nreturn (\n  \u003cdiv className='border p-8 rounded-md'\u003e\n    {/* Image Input Container */}\n    \u003cImageInputContainer\n      action={updateProductImageAction}\n      name={name}\n      image={product.image}\n      text='update image'\n    \u003e\n      \u003cinput type='hidden' name='id' value={id} /\u003e\n      \u003cinput type='hidden' name='url' value={product.image} /\u003e\n    \u003c/ImageInputContainer\u003e\n  \u003c/div\u003e\n);\n```\n\n### UpdateProductImageAction\n\n- actions.ts\n\n```ts\nexport const updateProductImageAction = async (\n  prevState: any,\n  formData: FormData\n) =\u003e {\n  await getAuthUser();\n  try {\n    const image = formData.get('image') as File;\n    const productId = formData.get('id') as string;\n    const oldImageUrl = formData.get('url') as string;\n\n    const validatedFile = validateWithZodSchema(imageSchema, { image });\n    const fullPath = await uploadImage(validatedFile.image);\n    await deleteImage(oldImageUrl);\n    await db.product.update({\n      where: {\n        id: productId,\n      },\n      data: {\n        image: fullPath,\n      },\n    });\n    revalidatePath(`/admin/products/${productId}/edit`);\n    return { message: 'Product Image updated successfully' };\n  } catch (error) {\n    return renderError(error);\n  }\n};\n```\n\n### LoadingTable\n\n- create components/global/LoadingTable.tsx\n\n```tsx\nimport { Skeleton } from '../ui/skeleton';\n\nfunction LoadingTable({ rows = 5 }: { rows?: number }) {\n  const tableRows = Array.from({ length: rows }, (_, index) =\u003e {\n    return (\n      \u003cdiv className='mb-4' key={index}\u003e\n        \u003cSkeleton className='w-full h-8 rounded' /\u003e\n      \u003c/div\u003e\n    );\n  });\n  return \u003c\u003e{tableRows}\u003c/\u003e;\n}\nexport default LoadingTable;\n```\n\n- create admin/products/loading.tsx\n\n```tsx\n'use client';\n\nimport LoadingTable from '@/components/global/LoadingTable';\n\nfunction loading() {\n  return \u003cLoadingTable /\u003e;\n}\nexport default loading;\n```\n\n### Favorite Model\n\n```prisma\nmodel Product {\nfavorites Favorite[]\n}\n\nmodel Favorite {\n  id        String   @id @default(uuid())\n  clerkId  String\n  product   Product  @relation(fields: [productId], references: [id], onDelete: Cascade)\n  productId String\n  createdAt DateTime @default(now())\n  updatedAt DateTime @updatedAt\n}\n```\n\n```sh\nnpx prisma db push\n```\n\n- restart server\n\n### CardSignIn Button\n\n- components/form/Buttons.tsx\n\n```tsx\nexport const CardSignInButton = () =\u003e {\n  return (\n    \u003cSignInButton mode='modal'\u003e\n      \u003cButton\n        type='button'\n        size='icon'\n        variant='outline'\n        className='p-2 cursor-pointer'\n        asChild\n      \u003e\n        \u003cFaRegHeart /\u003e\n      \u003c/Button\u003e\n    \u003c/SignInButton\u003e\n  );\n};\n\nexport const CardSubmitButton = ({ isFavorite }: { isFavorite: boolean }) =\u003e {\n  const { pending } = useFormStatus();\n  return (\n    \u003cButton\n      type='submit'\n      size='icon'\n      variant='outline'\n      className=' p-2 cursor-pointer'\n    \u003e\n      {pending ? (\n        \u003cReloadIcon className=' animate-spin' /\u003e\n      ) : isFavorite ? (\n        \u003cFaHeart /\u003e\n      ) : (\n        \u003cFaRegHeart /\u003e\n      )}\n    \u003c/Button\u003e\n  );\n};\n```\n\n### FetchFavoriteId\n\n- actions.ts\n\n```ts\nexport const fetchFavoriteId = async ({ productId }: { productId: string }) =\u003e {\n  const user = await getAuthUser();\n  const favorite = await db.favorite.findFirst({\n    where: {\n      productId,\n      clerkId: user.id,\n    },\n    select: {\n      id: true,\n    },\n  });\n  return favorite?.id || null;\n};\n\nexport const toggleFavoriteAction = async () =\u003e {\n  return { message: 'toggle favorite action' };\n};\n```\n\n### FavoriteToggleButton\n\n- components/products/FavoriteToggleButton.tsx\n\n```tsx\nimport { auth } from '@clerk/nextjs/server';\nimport { CardSignInButton } from '../form/Buttons';\nimport { fetchFavoriteId } from '@/utils/actions';\nimport FavoriteToggleForm from './FavoriteToggleForm';\nasync function FavoriteToggleButton({ productId }: { productId: string }) {\n  const { userId } = auth();\n  if (!userId) return \u003cCardSignInButton /\u003e;\n  const favoriteId = await fetchFavoriteId({ productId });\n\n  return \u003cFavoriteToggleForm favoriteId={favoriteId} productId={productId} /\u003e;\n}\nexport default FavoriteToggleButton;\n```\n\n### FavoriteToggleForm\n\n```tsx\n'use client';\n\nimport { usePathname } from 'next/navigation';\nimport FormContainer from '../form/FormContainer';\nimport { toggleFavoriteAction } from '@/utils/actions';\nimport { CardSubmitButton } from '../form/Buttons';\n\ntype FavoriteToggleFormProps = {\n  productId: string;\n  favoriteId: string | null;\n};\n\nfunction FavoriteToggleForm({\n  productId,\n  favoriteId,\n}: FavoriteToggleFormProps) {\n  const pathname = usePathname();\n  const toggleAction = toggleFavoriteAction.bind(null, {\n    productId,\n    favoriteId,\n    pathname,\n  });\n  return (\n    \u003cFormContainer action={toggleAction}\u003e\n      \u003cCardSubmitButton isFavorite={favoriteId ? true : false} /\u003e\n    \u003c/FormContainer\u003e\n  );\n}\nexport default FavoriteToggleForm;\n```\n\n### FavoriteToggleForm\n\n- actions.ts\n\n```ts\nexport const toggleFavoriteAction = async (prevState: {\n  productId: string;\n  favoriteId: string | null;\n  pathname: string;\n}) =\u003e {\n  const user = await getAuthUser();\n  const { productId, favoriteId, pathname } = prevState;\n  try {\n    if (favoriteId) {\n      await db.favorite.delete({\n        where: {\n          id: favoriteId,\n        },\n      });\n    } else {\n      await db.favorite.create({\n        data: {\n          productId,\n          clerkId: user.id,\n        },\n      });\n    }\n    revalidatePath(pathname);\n    return { message: favoriteId ? 'Removed from Faves' : 'Added to Faves' };\n  } catch (error) {\n    return renderError(error);\n  }\n};\n```\n\n- test in home, products and single product page\n\n### FetchUserFavorites\n\n```ts\nexport const fetchUserFavorites = async () =\u003e {\n  const user = await getAuthUser();\n  const favorites = await db.favorite.findMany({\n    where: {\n      clerkId: user.id,\n    },\n    include: {\n      product: true,\n    },\n  });\n  return favorites;\n};\n```\n\n### Favorites Page\n\n- favorites/loading.tsx\n\n```tsx\n'use client';\n\nimport LoadingContainer from '@/components/global/LoadingContainer';\n\nfunction loading() {\n  return \u003cLoadingContainer /\u003e;\n}\nexport default loading;\n```\n\npage.tsx\n\n```tsx\nimport { fetchUserFavorites } from '@/utils/actions';\nimport SectionTitle from '@/components/global/SectionTitle';\nimport ProductsGrid from '@/components/products/ProductsGrid';\n\nasync function FavoritesPage() {\n  const favorites = await fetchUserFavorites();\n  if (favorites.length === 0)\n    return \u003cSectionTitle text='You have no favorites yet.' /\u003e;\n  return (\n    \u003cdiv\u003e\n      \u003cSectionTitle text='Favorites' /\u003e\n      \u003cProductsGrid products={favorites.map((favorite) =\u003e favorite.product)} /\u003e\n    \u003c/div\u003e\n  );\n}\n\nexport default FavoritesPage;\n```\n\n### React Share\n\n[React Share](https://www.npmjs.com/package/react-share)\n\n```sh\nnpm i react-share\n```\n\n- create NEXT_PUBLIC_WEBSITE_URL in .env\n- get url from vercel\n\ncomponents/single-product/ShareButton.tsx\n\n```tsx\n'use client';\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from '@/components/ui/popover';\nimport { Button } from '../ui/button';\nimport { LuShare2 } from 'react-icons/lu';\n\nimport {\n  TwitterShareButton,\n  EmailShareButton,\n  LinkedinShareButton,\n  TwitterIcon,\n  EmailIcon,\n  LinkedinIcon,\n} from 'react-share';\n\nfunction ShareButton({ productId, name }: { productId: string; name: string }) {\n  const url = process.env.NEXT_PUBLIC_WEBSITE_URL;\n  const shareLink = `${url}/products/${productId}`;\n\n  return (\n    \u003cPopover\u003e\n      \u003cPopoverTrigger asChild\u003e\n        \u003cButton variant='outline' size='icon' className='p-2'\u003e\n          \u003cLuShare2 /\u003e\n        \u003c/Button\u003e\n      \u003c/PopoverTrigger\u003e\n      \u003cPopoverContent\n        side='top'\n        align='end'\n        sideOffset={10}\n        className='flex items-center gap-x-2 justify-center w-full'\n      \u003e\n        \u003cTwitterShareButton url={shareLink} title={name}\u003e\n          \u003cTwitterIcon size={32} round /\u003e\n        \u003c/TwitterShareButton\u003e\n        \u003cLinkedinShareButton url={shareLink} title={name}\u003e\n          \u003cLinkedinIcon size={32} round /\u003e\n        \u003c/LinkedinShareButton\u003e\n        \u003cEmailShareButton url={shareLink} subject={name}\u003e\n          \u003cEmailIcon size={32} round /\u003e\n        \u003c/EmailShareButton\u003e\n      \u003c/PopoverContent\u003e\n    \u003c/Popover\u003e\n  );\n}\nexport default ShareButton;\n```\n\n- products/[id]/page.tsx\n\n```tsx\nimport ShareButton from '@/components/single-product/ShareButton';\n\nreturn (\n  \u003cdiv className='flex gap-x-8 items-center'\u003e\n    \u003ch1 className='capitalize text-3xl font-bold'\u003e{name}\u003c/h1\u003e\n    \u003cdiv className='flex items-center gap-x-2'\u003e\n      \u003cFavoriteToggleButton productId={params.id} /\u003e\n      \u003cShareButton name={product.name} productId={params.id} /\u003e\n    \u003c/div\u003e\n  \u003c/div\u003e\n);\n```\n\n### Review Model\n\n```prisma\nmodel Product {\n    reviews Review []\n}\nmodel Review {\n  id        String   @id @default(uuid())\n  clerkId  String\n  rating Int\n  comment String\n  authorName String\n  authorImageUrl String\n  createdAt DateTime @default(now())\n  updatedAt DateTime @updatedAt\n   product   Product  @relation(fields: [productId], references: [id], onDelete: Cascade)\n  productId String\n}\n```\n\n```sh\nnpx prisma db push\n```\n\n- restar the server\n\n### Review Components and Actions\n\n- actions.ts\n\n```ts\nexport const createReviewAction = async (\n  prevState: any,\n  formData: FormData\n) =\u003e {\n  return { message: 'review submitted successfully' };\n};\n\nexport const fetchProductReviews = async () =\u003e {};\nexport const fetchProductReviewsByUser = async () =\u003e {};\nexport const deleteReviewAction = async () =\u003e {};\nexport const findExistingReview = async () =\u003e {};\nexport const fetchProductRating = async () =\u003e {};\n```\n\n- components/reviews\n  - RatingInput.tsx\n  - Comment.tsx\n  - ProductReviews.tsx\n  - Rating.tsx\n  - ReviewCard.tsx\n  - SubmitReview.tsx\n\n### RatingInput Component\n\n```tsx\nimport { Label } from '@/components/ui/label';\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from '@/components/ui/select';\n\nconst RatingInput = ({\n  name,\n  labelText,\n}: {\n  name: string;\n  labelText?: string;\n}) =\u003e {\n  const numbers = Array.from({ length: 5 }, (_, i) =\u003e {\n    const value = i + 1;\n    return value.toString();\n  }).reverse();\n\n  return (\n    \u003cdiv className='mb-2 max-w-xs'\u003e\n      \u003cLabel htmlFor={name} className='capitalize'\u003e\n        {labelText || name}\n      \u003c/Label\u003e\n      \u003cSelect defaultValue={numbers[0]} name={name} required\u003e\n        \u003cSelectTrigger\u003e\n          \u003cSelectValue /\u003e\n        \u003c/SelectTrigger\u003e\n        \u003cSelectContent\u003e\n          {numbers.map((number) =\u003e {\n            return (\n              \u003cSelectItem key={number} value={number}\u003e\n                {number}\n              \u003c/SelectItem\u003e\n            );\n          })}\n        \u003c/SelectContent\u003e\n      \u003c/Select\u003e\n    \u003c/div\u003e\n  );\n};\n\nexport default RatingInput;\n```\n\n### SubmitReview Component\n\n```tsx\n'use client';\nimport { useState } from 'react';\nimport { SubmitButton } from '@/components/form/Buttons';\nimport FormContainer from '@/components/form/FormContainer';\nimport { Card } from '@/components/ui/card';\nimport RatingInput from '@/components/reviews/RatingInput';\nimport TextAreaInput from '@/components/form/TextAreaInput';\nimport { Button } from '@/components/ui/button';\nimport { createReviewAction } from '@/utils/actions';\nimport { useUser } from '@clerk/nextjs';\nfunction SubmitReview({ productId }: { productId: string }) {\n  const [isReviewFormVisible, setIsReviewFormVisible] = useState(false);\n  const { user } = useUser();\n  return (\n    \u003cdiv\u003e\n      \u003cButton\n        size='lg'\n        className='capitalize'\n        onClick={() =\u003e setIsReviewFormVisible((prev) =\u003e !prev)}\n      \u003e\n        leave review\n      \u003c/Button\u003e\n      {isReviewFormVisible \u0026\u0026 (\n        \u003cCard className='p-8 mt-8'\u003e\n          \u003cFormContainer action={createReviewAction}\u003e\n            \u003cinput type='hidden' name='productId' value={productId} /\u003e\n            \u003cinput\n              type='hidden'\n              name='authorName'\n              value={user?.firstName || 'user'}\n            /\u003e\n            \u003cinput\n              type='hidden'\n              name='authorImageUrl'\n              value={user?.imageUrl || ''}\n            /\u003e\n            \u003cRatingInput name='rating' /\u003e\n            \u003cTextAreaInput\n              name='comment'\n              labelText='feedback'\n              defaultValue='Outstanding product!!!'\n            /\u003e\n            \u003cSubmitButton className='mt-4' /\u003e\n          \u003c/FormContainer\u003e\n        \u003c/Card\u003e\n      )}\n    \u003c/div\u003e\n  );\n}\n\nexport default SubmitReview;\n```\n\n- render in app/products/[id]/page.tsx after second column\n\n```tsx\nimport SubmitReview from '@/components/reviews/SubmitReview';\nimport ProductReviews from '@/components/reviews/ProductReviews';\n\nreturn (\n  \u003c\u003e\n    \u003cProductReviews productId={params.id} /\u003e\n    \u003cSubmitReview productId={params.id} /\u003e\n  \u003c/\u003e\n);\n```\n\n### Create Review Action\n\n- schemas.ts\n\n```ts\nexport const reviewSchema = z.object({\n  productId: z.string().refine((value) =\u003e value !== '', {\n    message: 'Product ID cannot be empty',\n  }),\n  authorName: z.string().refine((value) =\u003e value !== '', {\n    message: 'Author name cannot be empty',\n  }),\n  authorImageUrl: z.string().refine((value) =\u003e value !== '', {\n    message: 'Author image URL cannot be empty',\n  }),\n  rating: z.coerce\n    .number()\n    .int()\n    .min(1, { message: 'Rating must be at least 1' })\n    .max(5, { message: 'Rating must be at most 5' }),\n  comment: z\n    .string()\n    .min(10, { message: 'Comment must be at least 10 characters long' })\n    .max(1000, { message: 'Comment must be at most 1000 characters long' }),\n});\n```\n\n- actions.ts\n\n```ts\nexport const createReviewAction = async (\n  prevState: any,\n  formData: FormData\n) =\u003e {\n  const user = await getAuthUser();\n  try {\n    const rawData = Object.fromEntries(formData);\n\n    const validatedFields = validateWithZodSchema(reviewSchema, rawData);\n\n    await db.review.create({\n      data: {\n        ...validatedFields,\n        clerkId: user.id,\n      },\n    });\n    revalidatePath(`/products/${validatedFields.productId}`);\n    return { message: 'Review submitted successfully' };\n  } catch (error) {\n    return renderError(error);\n  }\n};\n```\n\n### Rating Component\n\n```tsx\nimport { FaStar, FaRegStar } from 'react-icons/fa';\n\nfunction Rating({ rating }: { rating: number }) {\n  // rating = 2\n  // 1 \u003c= 2 true\n  // 2 \u003c= 2 true\n  // 3 \u003c= 2 false\n  // ....\n  const stars = Array.from({ length: 5 }, (_, i) =\u003e i + 1 \u003c= rating);\n\n  return (\n    \u003cdiv className='flex items-center gap-x-1'\u003e\n      {stars.map((isFilled, i) =\u003e {\n        const className = `w-3 h-3 ${\n          isFilled ? 'text-primary' : 'text-gray-400'\n        }`;\n        return isFilled ? (\n          \u003cFaStar className={className} key={i} /\u003e\n        ) : (\n          \u003cFaRegStar className={className} key={i} /\u003e\n        );\n      })}\n    \u003c/div\u003e\n  );\n}\n\nexport default Rating;\n```\n\n### Comment Component\n\n```tsx\n'use client';\nimport { useState } from 'react';\nimport { Button } from '@/components/ui/button';\nfunction Comment({ comment }: { comment: string }) {\n  const [isExpanded, setIsExpanded] = useState(false);\n\n  const toggleExpanded = () =\u003e {\n    setIsExpanded(!isExpanded);\n  };\n  const longComment = comment.length \u003e 130;\n  const displayComment =\n    longComment \u0026\u0026 !isExpanded ? `${comment.slice(0, 130)}...` : comment;\n\n  return (\n    \u003cdiv\u003e\n      \u003cp className='text-sm'\u003e{displayComment}\u003c/p\u003e\n      {longComment \u0026\u0026 (\n        \u003cButton\n          variant='link'\n          className='pl-0 text-muted-foreground'\n          onClick={toggleExpanded}\n        \u003e\n          {isExpanded ? 'Show Less' : 'Show More'}\n        \u003c/Button\u003e\n      )}\n    \u003c/div\u003e\n  );\n}\n\nexport default Comment;\n```\n\n### Fetch Product Reviews\n\n```ts\nexport const fetchProductReviews = async (productId: string) =\u003e {\n  const reviews = await db.review.findMany({\n    where: {\n      productId,\n    },\n    orderBy: {\n      createdAt: 'desc',\n    },\n  });\n  return reviews;\n};\n```\n\n### Product Reviews\n\n```tsx\nimport { fetchProductReviews } from '@/utils/actions';\n\nimport ReviewCard from './ReviewCard';\nimport SectionTitle from '../global/SectionTitle';\nasync function ProductReviews({ productId }: { productId: string }) {\n  const reviews = await fetchProductReviews(productId);\n\n  return (\n    \u003cdiv className='mt-16'\u003e\n      \u003cSectionTitle text='product reviews' /\u003e\n\n      \u003cdiv className='grid md:grid-cols-2 gap-8 my-8'\u003e\n        {reviews.map((review) =\u003e {\n          const { comment, rating, authorImageUrl, authorName } = review;\n          const reviewInfo = {\n            comment,\n            rating,\n            image: authorImageUrl,\n            name: authorName,\n          };\n          return \u003cReviewCard key={review.id} reviewInfo={reviewInfo} /\u003e;\n        })}\n      \u003c/div\u003e\n    \u003c/div\u003e\n  );\n}\nexport default ProductReviews;\n```\n\n### ReviewCard\n\n```tsx\nimport { Card, CardContent, CardHeader } from '@/components/ui/card';\nimport Rating from './Rating';\nimport Comment from './Comment';\nimport Image from 'next/image';\n\ntype ReviewCardProps = {\n  reviewInfo: {\n    comment: string;\n    rating: number;\n    name: string;\n    image: string;\n  };\n  children?: React.ReactNode;\n};\n\nfunction ReviewCard({ reviewInfo, children }: ReviewCardProps) {\n  return (\n    \u003cCard className='relative'\u003e\n      \u003cCardHeader\u003e\n        \u003cdiv className='flex items-center'\u003e\n          \u003cImage\n            src={reviewInfo.image}\n            alt={reviewInfo.name}\n            width={48}\n            height={48}\n            className='w-12 h-12 rounded-full object-cover'\n          /\u003e\n          \u003cdiv className='ml-4'\u003e\n            \u003ch3 className='text-sm font-bold capitalize mb-1'\u003e\n              {reviewInfo.name}\n            \u003c/h3\u003e\n            \u003cRating rating={reviewInfo.rating} /\u003e\n          \u003c/div\u003e\n        \u003c/div\u003e\n      \u003c/CardHeader\u003e\n      \u003cCardContent\u003e\n        \u003cComment comment={reviewInfo.comment} /\u003e\n      \u003c/CardContent\u003e\n      \u003cdiv className='absolute top-3 right-3'\u003e{children}\u003c/div\u003e\n    \u003c/Card\u003e\n  );\n}\nexport default ReviewCard;\n```\n\n- next.config.mjs\n\n```mjs\n {\n  protocol: 'https',\n  hostname: 'img.clerk.com',\n },\n```\n\n### fetchProductRating\n\n```ts\nexport const fetchProductRating = async (productId: string) =\u003e {\n  const result = await db.review.groupBy({\n    by: ['productId'],\n    _avg: {\n      rating: true,\n    },\n    _count: {\n      rating: true,\n    },\n    where: {\n      productId,\n    },\n  });\n\n  // empty array if no reviews\n  return {\n    rating: result[0]?._avg.rating?.toFixed(1) ?? 0,\n    count: result[0]?._count.rating ?? 0,\n  };\n};\n```\n\n### ProductRating\n\n- components/single-product/ProductRating.tsx\n\n```tsx\nconst { rating, count } = await fetchProductRating(productId);\n```\n\n### FetchProductReviewsByUser and DeleteReview Action\n\n```ts\nexport const fetchProductReviewsByUser = async () =\u003e {\n  const user = await getAuthUser();\n  const reviews = await db.review.findMany({\n    where: {\n      clerkId: user.id,\n    },\n    select: {\n      id: true,\n      rating: true,\n      comment: true,\n      product: {\n        select: {\n          image: true,\n          name: true,\n        },\n      },\n    },\n  });\n  return reviews;\n};\nexport const deleteReviewAction = async (prevState: { reviewId: string }) =\u003e {\n  const { reviewId } = prevState;\n  const user = await getAuthUser();\n\n  try {\n    await db.review.delete({\n      where: {\n        id: reviewId,\n        clerkId: user.id,\n      },\n    });\n\n    revalidatePath('/reviews');\n    return { message: 'Review deleted successfully' };\n  } catch (error) {\n    return renderError(error);\n  }\n};\n```\n\n### Reviews Page\n\n- setup \"reviews\" link in utils/links.ts\n- create app/reviews/page.tsx and app/reviews/loading.tsx\n\npage.tsx\n\n```tsx\nimport { deleteReviewAction, fetchProductReviewsByUser } from '@/utils/actions';\nimport ReviewCard from '@/components/reviews/ReviewCard';\nimport SectionTitle from '@/components/global/SectionTitle';\nimport FormContainer from '@/components/form/FormContainer';\nimport { IconButton } from '@/components/form/Buttons';\nasync function ReviewsPage() {\n  const reviews = await fetchProductReviewsByUser();\n  if (reviews.length === 0)\n    return \u003cSectionTitle text='you have no reviews yet' /\u003e;\n\n  return (\n    \u003c\u003e\n      \u003cSectionTitle text='Your Reviews' /\u003e\n      \u003csection className='grid md:grid-cols-2 gap-8 mt-4 '\u003e\n        {reviews.map((review) =\u003e {\n          const { comment, rating } = review;\n          const { name, image } = review.product;\n          const reviewInfo = {\n            comment,\n            rating,\n            name,\n            image,\n          };\n          return (\n            \u003cReviewCard key={review.id} reviewInfo={reviewInfo}\u003e\n              \u003cDeleteReview reviewId={review.id} /\u003e\n            \u003c/ReviewCard\u003e\n          );\n        })}\n      \u003c/section\u003e\n    \u003c/\u003e\n  );\n}\n\nconst DeleteReview = ({ reviewId }: { reviewId: string }) =\u003e {\n  const deleteReview = deleteReviewAction.bind(null, { reviewId });\n  return (\n    \u003cFormContainer action={deleteReview}\u003e\n      \u003cIconButton actionType='delete' /\u003e\n    \u003c/FormContainer\u003e\n  );\n};\n\nexport default ReviewsPage;\n```\n\nloading.tsx\n\n```tsx\n'use client';\n\nimport { Card, CardHeader } from '@/components/ui/card';\nimport { Skeleton } from '@/components/ui/skeleton';\nfunction loading() {\n  return (\n    \u003csection className='grid md:grid-cols-2 gap-8 mt-4 '\u003e\n      \u003cReviewLoadingCard /\u003e\n      \u003cReviewLoadingCard /\u003e\n    \u003c/section\u003e\n  );\n}\n\nconst ReviewLoadingCard = () =\u003e {\n  return (\n    \u003cCard\u003e\n      \u003cCardHeader\u003e\n        \u003cdiv className='flex items-center'\u003e\n          \u003cSkeleton className='w-12 h-12 rounded-full' /\u003e\n          \u003cdiv className='ml-4'\u003e\n            \u003cSkeleton className='w-[150px] h-4 mb-2' /\u003e\n            \u003cSkeleton className='w-[100px] h-4' /\u003e\n          \u003c/div\u003e\n        \u003c/div\u003e\n      \u003c/CardHeader\u003e\n    \u003c/Card\u003e\n  );\n};\n\nexport default loading;\n```\n\n### Restrict Access\n\nactions.ts\n\n```ts\nexport const findExistingReview = async (userId: string, productId: string) =\u003e {\n  return db.review.findFirst({\n    where: {\n      clerkId: userId,\n      productId,\n    },\n  });\n};\n```\n\n- app/products/[id]/page.tsx\n\n```tsx\nimport { fetchSingleProduct, findExistingReview } from '@/utils/actions';\nimport { auth } from '@clerk/nextjs/server';\n\nasync function SingleProductPage({ params }: { params: { id: string } }) {\n  const { userId } = auth();\n  const reviewDoesNotExist =\n    userId \u0026\u0026 !(await findExistingReview(userId, product.id));\n\n  return (\n    \u003c\u003e\n      \u003cProductReviews productId={params.id} /\u003e\n      {reviewDoesNotExist \u0026\u0026 \u003cSubmitReview productId={params.id} /\u003e}\n    \u003c/\u003e\n  );\n}\n```\n\n### Cart and CartItem Model\n\n- prisma/schema.prisma\n\n```prisma\nmodel Product{\ncartItems CartItem[]\n}\nmodel Cart {\n  id        String   @id @default(uuid())\n  clerkId  String\n  cartItems CartItem[]\n  numItemsInCart Int @default(0)\n  cartTotal Int @default(0)\n  shipping Int @default(5)\n  tax Int @default(0)\n  taxRate Float @default(0.1)\n  orderTotal Int @default(0)\n  createdAt DateTime @default(now())\n  updatedAt DateTime @updatedAt\n}\n\nmodel CartItem {\n  id        String   @id @default(uuid())\n  product   Product  @relation(fields: [productId], references: [id], onDelete: Cascade)\n  productId String\n  cart     Cart     @relation(fields: [cartId], references: [id], onDelete: Cascade)\n  cartId   String\n  amount  Int\n  createdAt DateTime @default(now())\n  updatedAt DateTime @updatedAt\n}\n\n```\n\n- actions.ts\n\n```ts\nexport const fetchCartItems = async () =\u003e {};\n\nconst fetchProduct = async () =\u003e {};\n\nexport const fetchOrCreateCart = async () =\u003e {};\n\nconst updateOrCreateCartItem = async () =\u003e {};\n\nexport const updateCart = async () =\u003e {};\n\nexport const addToCartAction = async () =\u003e {};\n\nexport const removeCartItemAction = async () =\u003e {};\n\nexport const updateCartItemAction = async () =\u003e {};\n```\n\n### FetchCartItems\n\n- actions.ts\n\n```ts\nexport const fetchCartItems = async () =\u003e {\n  const { userId } = auth();\n\n  const cart = await db.cart.findFirst({\n    where: {\n      clerkId: userId ?? '',\n    },\n    select: {\n      numItemsInCart: true,\n    },\n  });\n  return cart?.numItemsInCart || 0;\n};\n```\n\n- components/navbar/CartButton.tsx\n\n```tsx\nasync function CartButton() {\n  const numItemsInCart = await fetchCartItems();\n}\n```\n\n### ProductSignInButton Component\n\n- components/form/Buttons.tsx\n\n```tsx\nexport const ProductSignInButton = () =\u003e {\n  return (\n    \u003cSignInButton mode='modal'\u003e\n      \u003cButton type='button' size='default' className='mt-8'\u003e\n        Please Sign In\n      \u003c/Button\u003e\n    \u003c/SignInButton\u003e\n  );\n};\n```\n\n### SelectProductAmount Component\n\n- create components/single-product/SelectProductAmount.tsx\n\n```tsx\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from '@/components/ui/select';\n\nexport enum Mode {\n  SingleProduct = 'singleProduct',\n  CartItem = 'cartItem',\n}\n\ntype SelectProductAmountProps = {\n  mode: Mode.SingleProduct;\n  amount: number;\n  setAmount: (value: number) =\u003e void;\n};\n\ntype SelectCartItemAmountProps = {\n  mode: Mode.CartItem;\n  amount: number;\n  setAmount: (value: number) =\u003e Promise\u003cvoid\u003e;\n  isLoading: boolean;\n};\n\nfunction SelectProductAmount(\n  props: SelectProductAmountProps | SelectCartItemAmountProps\n) {\n  const { mode, amount, setAmount } = props;\n\n  const cartItem = mode === Mode.CartItem;\n\n  return (\n    \u003c\u003e\n      \u003ch4 className='mb-2'\u003eAmount : \u003c/h4\u003e\n      \u003cSelect\n        defaultValue={amount.toString()}\n        onValueChange={(value) =\u003e setAmount(Number(value))}\n        disabled={cartItem ? props.isLoading : false}\n      \u003e\n        \u003cSelectTrigger className={cartItem ? 'w-[100px]' : 'w-[150px]'}\u003e\n          \u003cSelectValue placeholder={amount} /\u003e\n        \u003c/SelectTrigger\u003e\n        \u003cSelectContent\u003e\n          {Array.from({ length: cartItem ? amount + 10 : 10 }, (_, index) =\u003e {\n            const selectValue = (index + 1).toString();\n            return (\n              \u003cSelectItem key={index} value={selectValue}\u003e\n                {selectValue}\n              \u003c/SelectItem\u003e\n            );\n          })}\n        \u003c/SelectContent\u003e\n      \u003c/Select\u003e\n    \u003c/\u003e\n  );\n}\nexport default SelectProductAmount;\n```\n\n### AddToCart Component\n\n```tsx\n'use client';\nimport { useState } from 'react';\nimport SelectProductAmount from './SelectProductAmount';\nimport { Mode } from './SelectProductAmount';\nimport FormContainer from '../form/FormContainer';\nimport { SubmitButton } from '../form/Buttons';\nimport { addToCartAction } from '@/utils/actions';\nimport { useAuth } from '@clerk/nextjs';\nimport { ProductSignInButton } from '../form/Buttons';\n\nfunction AddToCart({ productId }: { productId: string }) {\n  const [amount, setAmount] = useState(1);\n  const { userId } = useAuth();\n  return (\n    \u003cdiv className='mt-4'\u003e\n      \u003cSelectProductAmount\n        mode={Mode.SingleProduct}\n        amount={amount}\n        setAmount={setAmount}\n      /\u003e\n      {userId ? (\n        \u003cFormContainer action={addToCartAction}\u003e\n          \u003cinput type='hidden' name='productId' value={productId} /\u003e\n          \u003cinput type='hidden' name='amount' value={amount} /\u003e\n          \u003cSubmitButton text='add to cart' size='default' className='mt-8' /\u003e\n        \u003c/FormContainer\u003e\n      ) : (\n        \u003cProductSignInButton /\u003e\n      )}\n    \u003c/div\u003e\n  );\n}\nexport default AddToCart;\n```\n\n### AddToCart Action\n\n- actions.ts\n\n```ts\nimport { Cart } from '@prisma/client';\n\nconst fetchProduct = async (productId: string) =\u003e {\n  const product = await db.product.findUnique({\n    where: {\n      id: productId,\n    },\n  });\n\n  if (!product) {\n    throw new Error('Product not found');\n  }\n  return product;\n};\nconst includeProductClause = {\n  cartItems: {\n    include: {\n      product: true,\n    },\n  },\n};\n\nexport const fetchOrCreateCart = async ({\n  userId,\n  errorOnFailure = false,\n}: {\n  userId: string;\n  errorOnFailure?: boolean;\n}) =\u003e {\n  let cart = await db.cart.findFirst({\n    where: {\n      clerkId: userId,\n    },\n    include: includeProductClause,\n  });\n\n  if (!cart \u0026\u0026 errorOnFailure) {\n    throw new Error('Cart not found');\n  }\n\n  if (!cart) {\n    cart = await db.cart.create({\n      data: {\n        clerkId: userId,\n      },\n      include: includeProductClause,\n    });\n  }\n\n  return cart;\n};\n\nconst updateOrCreateCartItem = async ({\n  productId,\n  cartId,\n  amount,\n}: {\n  productId: string;\n  cartId: string;\n  amount: number;\n}) =\u003e {\n  let cartItem = await db.cartItem.findFirst({\n    where: {\n      productId,\n      cartId,\n    },\n  });\n\n  if (cartItem) {\n    cartItem = await db.cartItem.update({\n      where: {\n        id: cartItem.id,\n      },\n      data: {\n        amount: cartItem.amount + amount,\n      },\n    });\n  } else {\n    cartItem = await db.cartItem.create({\n      data: { amount, productId, cartId },\n    });\n  }\n};\n\nexport const updateCart = async (cart: Cart) =\u003e {\n  const cartItems = await db.cartItem.findMany({\n    where: {\n      cartId: cart.id,\n    },\n    include: {\n      product: true, // Include the related product\n    },\n  });\n\n  let numItemsInCart = 0;\n  let cartTotal = 0;\n\n  for (const item of cartItems) {\n    numItemsInCart += item.amount;\n    cartTotal += item.amount * item.product.price;\n  }\n  const tax = cart.taxRate * cartTotal;\n  const shipping = cartTotal ? cart.shipping : 0;\n  const orderTotal = cartTotal + tax + shipping;\n\n  await db.cart.update({\n    where: {\n      id: cart.id,\n    },\n    data: {\n      numItemsInCart,\n      cartTotal,\n      tax,\n      orderTotal,\n    },\n  });\n};\n\nexport const addToCartAction = async (prevState: any, formData: FormData) =\u003e {\n  const user = await getAuthUser();\n  try {\n    const productId = formData.get('productId') as string;\n    const amount = Number(formData.get('amount'));\n    await fetchProduct(productId);\n    const cart = await fetchOrCreateCart({ userId: user.id });\n    await updateOrCreateCartItem({ productId, cartId: cart.id, amount });\n    await updateCart(cart);\n  } catch (error) {\n    return renderError(error);\n  }\n  redirect('/cart');\n};\n```\n\n### Cart Page\n\n- create components/cart\n\n  - CartItemColumns.tsx\n  - CartItemsList.tsx\n  - CartTotals.tsx\n  - ThirdColumn.tsx\n\n- app/cart/page.tsx\n\n```tsx\nimport CartItemsList from '@/components/cart/CartItemsList';\nimport CartTotals from '@/components/cart/CartTotals';\nimport SectionTi","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcraftingweb%2Fcozynest","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcraftingweb%2Fcozynest","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcraftingweb%2Fcozynest/lists"}