├── .prettierignore ├── app ├── favicon.ico ├── sign-in │ ├── loading.tsx │ ├── layout.tsx │ └── page.tsx ├── product │ └── [id] │ │ ├── not-found.tsx │ │ └── page.tsx ├── not-found.tsx ├── user │ ├── loading.tsx │ ├── layout.tsx │ └── page.tsx ├── layout.tsx ├── all │ └── page.tsx ├── globals.css ├── page.tsx └── about │ └── page.tsx ├── postcss.config.js ├── utils ├── slow.ts └── cn.ts ├── prisma ├── migrations │ ├── migration_lock.toml │ ├── 20250919104318_add_featured_to_products │ │ └── migration.sql │ ├── 20250801131353_add_saved_products │ │ └── migration.sql │ ├── 20250917064507_add_category_model │ │ └── migration.sql │ ├── 20250824112830_init │ │ └── migration.sql │ └── 20250801100125_init │ │ └── migration.sql ├── schema.prisma └── seed.ts ├── next-env.d.ts ├── .prettierrc ├── next.config.ts ├── db.ts ├── components ├── ui │ ├── Card.tsx │ ├── Skeleton.tsx │ ├── SearchStatus.tsx │ ├── MotionWrappers.tsx │ ├── Divider.tsx │ ├── LinkButton.tsx │ ├── ShowMore.tsx │ ├── BackButton.tsx │ ├── LinkStatus.tsx │ ├── Button.tsx │ ├── Modal.tsx │ ├── ImagePlaceholder.tsx │ └── ProductCard.tsx ├── Header.tsx ├── Footer.tsx ├── banner │ ├── BannerContainer.tsx │ └── WelcomeBanner.tsx ├── internal │ ├── BoundaryProvider.tsx │ ├── BoundaryToggle.tsx │ └── Boundary.tsx ├── SortButton.tsx ├── Search.tsx └── Pagination.tsx ├── .gitignore ├── .env.sample ├── features ├── user │ ├── user-queries.ts │ └── components │ │ ├── Recommendations.tsx │ │ ├── UserProfile.tsx │ │ ├── SaveProductButton.tsx │ │ ├── Discounts.tsx │ │ └── SavedProducts.tsx ├── auth │ ├── components │ │ ├── AuthProvider.tsx │ │ └── LoginButton.tsx │ ├── auth-actions.ts │ └── auth-queries.ts ├── category │ ├── components │ │ ├── Categories.tsx │ │ ├── CategoryFilterButton.tsx │ │ ├── CategoryFilters.tsx │ │ └── FeaturedCategories.tsx │ └── category-queries.ts └── product │ ├── components │ ├── FeaturedProducts.tsx │ ├── ProductModal.tsx │ ├── Product.tsx │ ├── ProductList.tsx │ ├── Hero.tsx │ ├── Reviews.tsx │ └── ProductDetails.tsx │ ├── product-actions.ts │ └── product-queries.ts ├── tsconfig.json ├── LISCENSE ├── next16-commerce.code-workspace ├── package.json ├── README.md ├── eslint.config.mjs ├── .vscode └── snippets.code-snippets └── STEPS.md /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | yarn.lock 4 | pnpm-lock.yaml 5 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aurorascharff/next16-commerce/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | '@tailwindcss/postcss': {}, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /utils/slow.ts: -------------------------------------------------------------------------------- 1 | export async function slow(delay: number = 1000) { 2 | await new Promise(resolve => { 3 | return setTimeout(resolve, delay); 4 | }); 5 | } 6 | -------------------------------------------------------------------------------- /app/sign-in/loading.tsx: -------------------------------------------------------------------------------- 1 | export default function Loading() { 2 | return
; 3 | } 4 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (e.g., Git) 3 | provider = "postgresql" 4 | -------------------------------------------------------------------------------- /utils/cn.ts: -------------------------------------------------------------------------------- 1 | import { clsx } from 'clsx'; 2 | import { twMerge } from 'tailwind-merge'; 3 | import type { ClassValue } from 'clsx'; 4 | 5 | export function cn(...inputs: ClassValue[]) { 6 | return twMerge(clsx(inputs)); 7 | } 8 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | import "./.next/types/routes.d.ts"; 4 | 5 | // NOTE: This file should not be edited 6 | // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "bracketSpacing": true, 4 | "tabWidth": 2, 5 | "useTabs": false, 6 | "semi": true, 7 | "singleQuote": true, 8 | "trailingComma": "all", 9 | "arrowParens": "avoid", 10 | "endOfLine": "auto", 11 | "plugins": ["prettier-plugin-tailwindcss"] 12 | } 13 | -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from 'next'; 2 | 3 | const nextConfig: NextConfig = { 4 | cacheComponents: true, 5 | experimental: { 6 | inlineCss: true, 7 | staleTimes: { 8 | dynamic: 30, 9 | }, 10 | }, 11 | reactCompiler: true, 12 | typedRoutes: true, 13 | }; 14 | 15 | module.exports = nextConfig; 16 | -------------------------------------------------------------------------------- /db.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client'; 2 | 3 | const globalForPrisma = global as unknown as { 4 | prisma: PrismaClient | undefined; 5 | }; 6 | 7 | export const prisma = 8 | globalForPrisma.prisma ?? 9 | new PrismaClient({ 10 | // log: ['query'], 11 | }); 12 | 13 | if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma; 14 | -------------------------------------------------------------------------------- /prisma/migrations/20250919104318_add_featured_to_products/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropForeignKey 2 | ALTER TABLE "public"."Product" DROP CONSTRAINT "Product_category_fkey"; 3 | 4 | -- AlterTable 5 | ALTER TABLE "public"."Product" ADD COLUMN "featured" BOOLEAN NOT NULL DEFAULT false; 6 | 7 | -- CreateIndex 8 | CREATE INDEX "Product_featured_idx" ON "public"."Product"("featured"); 9 | -------------------------------------------------------------------------------- /components/ui/Card.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { cn } from '@/utils/cn'; 3 | 4 | type Props = { 5 | children: React.ReactNode; 6 | className?: string; 7 | }; 8 | 9 | export default function Card({ children, className }: Props) { 10 | return ( 11 |
12 | {children} 13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /app/sign-in/layout.tsx: -------------------------------------------------------------------------------- 1 | export default async function SignInLayout({ children }: LayoutProps<'/sign-in'>) { 2 | return ( 3 |
4 |
5 |

Welcome Back

6 |

Sign in to your account to continue shopping

7 |
8 | {children} 9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /app/product/[id]/not-found.tsx: -------------------------------------------------------------------------------- 1 | export default function NotFound() { 2 | return ( 3 |
4 |

404

5 |

Product Not Found

6 |

7 | The product you are looking for does not exist or has been moved. 8 |

9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /app/not-found.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function NotFound() { 4 | return ( 5 |
6 |

404

7 |

Page Not Found

8 |

9 | The page you are looking for does not exist or has been moved to a different location. 10 |

11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # database 12 | dev.db 13 | dev.db-journal 14 | 15 | # next.js 16 | /.next/ 17 | /out/ 18 | 19 | # production 20 | /build 21 | 22 | # misc 23 | .DS_Store 24 | *.pem 25 | 26 | # debug 27 | npm-debug.log* 28 | yarn-debug.log* 29 | yarn-error.log* 30 | 31 | # local env files 32 | .env*.local 33 | .env 34 | 35 | # vercel 36 | .vercel 37 | 38 | # typescript 39 | *.tsbuildinfo -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | # Environment variables declared in this file are automatically made available to Prisma. 2 | # See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema 3 | 4 | # Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB. 5 | # See the documentation for all the connection string options: https://pris.ly/d/connection-strings 6 | 7 | # # URL for local developement 8 | # DATABASE_URL="file:./dev.db" 9 | # Database connection string 10 | DATABASE_URL=secret 11 | # Shadow database connection string for sqlserver 12 | SHADOW_DATABASE_URL=secret -------------------------------------------------------------------------------- /components/ui/Skeleton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { cn } from '@/utils/cn'; 3 | 4 | type Props = { 5 | className?: string; 6 | }; 7 | 8 | export default function Skeleton({ className }: Props) { 9 | return ( 10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /components/ui/SearchStatus.tsx: -------------------------------------------------------------------------------- 1 | import { Loader2, Search } from 'lucide-react'; 2 | import React from 'react'; 3 | import { useFormStatus } from 'react-dom'; 4 | 5 | export default function SearchStatus({ searching = false }: { searching?: boolean }) { 6 | const { pending } = useFormStatus(); 7 | const isSearching = searching || pending; 8 | 9 | return ( 10 | <> 11 | {isSearching ? ( 12 |
13 |
15 | ) : ( 16 |