├── .example-env ├── .gitignore ├── README.md ├── app ├── [locale] │ ├── (auth) │ │ ├── layout.tsx │ │ ├── sign-in │ │ │ ├── credentials-signin-form.tsx │ │ │ ├── google-signin-form.tsx │ │ │ └── page.tsx │ │ └── sign-up │ │ │ ├── page.tsx │ │ │ └── signup-form.tsx │ ├── (home) │ │ ├── layout.tsx │ │ └── page.tsx │ ├── (root) │ │ ├── account │ │ │ ├── layout.tsx │ │ │ ├── manage │ │ │ │ ├── name │ │ │ │ │ ├── page.tsx │ │ │ │ │ └── profile-form.tsx │ │ │ │ └── page.tsx │ │ │ ├── orders │ │ │ │ ├── [id] │ │ │ │ │ └── page.tsx │ │ │ │ └── page.tsx │ │ │ └── page.tsx │ │ ├── cart │ │ │ ├── [itemId] │ │ │ │ ├── cart-add-item.tsx │ │ │ │ └── page.tsx │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ ├── page │ │ │ └── [slug] │ │ │ │ └── page.tsx │ │ ├── product │ │ │ └── [slug] │ │ │ │ ├── page.tsx │ │ │ │ └── review-list.tsx │ │ └── search │ │ │ └── page.tsx │ ├── admin │ │ ├── admin-nav.tsx │ │ ├── layout.tsx │ │ ├── orders │ │ │ ├── [id] │ │ │ │ └── page.tsx │ │ │ └── page.tsx │ │ ├── overview │ │ │ ├── date-range-picker.tsx │ │ │ ├── overview-report.tsx │ │ │ ├── page.tsx │ │ │ ├── sales-area-chart.tsx │ │ │ ├── sales-category-pie-chart.tsx │ │ │ └── table-chart.tsx │ │ ├── products │ │ │ ├── [id] │ │ │ │ └── page.tsx │ │ │ ├── create │ │ │ │ └── page.tsx │ │ │ ├── page.tsx │ │ │ ├── product-form.tsx │ │ │ └── product-list.tsx │ │ ├── settings │ │ │ ├── carousel-form.tsx │ │ │ ├── common-form.tsx │ │ │ ├── currency-form.tsx │ │ │ ├── delivery-date-form.tsx │ │ │ ├── language-form.tsx │ │ │ ├── page.tsx │ │ │ ├── payment-method-form.tsx │ │ │ ├── setting-form.tsx │ │ │ ├── setting-nav.tsx │ │ │ └── site-info-form.tsx │ │ ├── users │ │ │ ├── [id] │ │ │ │ ├── page.tsx │ │ │ │ └── user-edit-form.tsx │ │ │ └── page.tsx │ │ └── web-pages │ │ │ ├── [id] │ │ │ └── page.tsx │ │ │ ├── create │ │ │ └── page.tsx │ │ │ ├── page.tsx │ │ │ └── web-page-form.tsx │ ├── checkout │ │ ├── [id] │ │ │ ├── page.tsx │ │ │ ├── payment-form.tsx │ │ │ ├── stripe-form.tsx │ │ │ └── stripe-payment-success │ │ │ │ └── page.tsx │ │ ├── checkout-footer.tsx │ │ ├── checkout-form.tsx │ │ ├── layout.tsx │ │ └── page.tsx │ ├── error.tsx │ ├── favicon.ico │ ├── layout.tsx │ ├── loading.tsx │ └── not-found.tsx ├── api │ ├── auth │ │ └── [...nextauth] │ │ │ └── route.ts │ ├── products │ │ └── browsing-history │ │ │ └── route.ts │ ├── uploadthing │ │ ├── core.ts │ │ └── route.ts │ └── webhooks │ │ └── stripe │ │ └── route.tsx └── globals.css ├── auth.config.ts ├── auth.ts ├── components.json ├── components ├── shared │ ├── action-button.tsx │ ├── app-initializer.tsx │ ├── browsing-history-list.tsx │ ├── cart-sidebar.tsx │ ├── client-providers.tsx │ ├── collapsible-on-mobile.tsx │ ├── color-provider.tsx │ ├── delete-dialog.tsx │ ├── footer.tsx │ ├── header │ │ ├── cart-button.tsx │ │ ├── index.tsx │ │ ├── language-switcher.tsx │ │ ├── menu.tsx │ │ ├── search.tsx │ │ ├── sidebar.tsx │ │ ├── theme-switcher.tsx │ │ └── user-button.tsx │ ├── home │ │ ├── home-card.tsx │ │ └── home-carousel.tsx │ ├── order │ │ └── order-details-form.tsx │ ├── pagination.tsx │ ├── product │ │ ├── add-to-browsing-history.tsx │ │ ├── add-to-cart.tsx │ │ ├── image-hover.tsx │ │ ├── product-card.tsx │ │ ├── product-gallery.tsx │ │ ├── product-price.tsx │ │ ├── product-slider.tsx │ │ ├── product-sort-selector.tsx │ │ ├── rating-summary.tsx │ │ ├── rating.tsx │ │ └── select-variant.tsx │ ├── separator-or.tsx │ └── theme-provider.tsx └── ui │ ├── alert-dialog.tsx │ ├── badge.tsx │ ├── button.tsx │ ├── calendar.tsx │ ├── card.tsx │ ├── carousel.tsx │ ├── checkbox.tsx │ ├── collapsible.tsx │ ├── dialog.tsx │ ├── drawer.tsx │ ├── dropdown-menu.tsx │ ├── form.tsx │ ├── input.tsx │ ├── label.tsx │ ├── popover.tsx │ ├── progress.tsx │ ├── radio-group.tsx │ ├── scroll-area.tsx │ ├── select.tsx │ ├── separator.tsx │ ├── sheet.tsx │ ├── skeleton.tsx │ ├── table.tsx │ ├── textarea.tsx │ ├── toast.tsx │ └── toaster.tsx ├── emails ├── ask-review-order-items.tsx ├── index.tsx └── purchase-receipt.tsx ├── eslint.config.mjs ├── hooks ├── use-browsing-history.ts ├── use-cart-sidebar.ts ├── use-cart-store.ts ├── use-color-store.ts ├── use-device-type.ts ├── use-is-mounted.ts ├── use-setting-store.ts └── use-toast.ts ├── i18n-config.ts ├── i18n ├── request.ts └── routing.ts ├── lessons ├── 00-introduction.md ├── 01-install-ai-tools-and-vscode-extensions.md ├── 02-create-next-app.md ├── 03-create-website-layout.md ├── 04-create-home-page-carousel.md ├── 05-connect-to-mongodb-and-seed-products.md ├── 06-create-home-cards.md ├── 07-create-todays-deals-slider.md ├── 08-create-best-selling-slider.md ├── 09-create-product-details-page.md ├── 10-create-browsing-history.md ├── 11-implement-add-to-cart.md ├── 12-create-cart-page.md ├── 13-create-cart-sidebar.md ├── 14-signin-user.md ├── 15-register-user.md ├── 16-signin-with-google.md ├── 17-create-checkout-page.md ├── 18-place-order.md ├── 19-pay-order-by-paypal.md ├── 20-pay-order-by-stripe.md ├── 21-rate-review-products.md ├── 22-create-order-history-page.md ├── 23-update-user-name.md ├── 24-create-category-sidebar.md ├── 25-create-search-page.md ├── 26-add-theme-color.md ├── 27-create-admin-dashboard.md ├── 28-admin-products.md ├── 29-create-update-products.md ├── 30-admin-orders.md ├── 31-mark-orders-as-paid-delivered.md ├── 32-admin-users.md ├── 33-edit-user.md ├── 34-admin-web-pages.md ├── 35-create-update-web-pages.md ├── 36-make-website-multilingual.md └── 37-create-settings-page.md ├── lib ├── actions │ ├── order.actions.ts │ ├── product.actions.ts │ ├── review.actions.ts │ ├── setting.actions.ts │ ├── user.actions.ts │ └── web-page.actions.ts ├── constants.ts ├── data.ts ├── db │ ├── client.ts │ ├── index.ts │ ├── models │ │ ├── order.model.ts │ │ ├── product.model.ts │ │ ├── review.model.ts │ │ ├── setting.model.ts │ │ ├── user.model.ts │ │ └── web-page.model.ts │ └── seed.ts ├── paypal.ts ├── uploadthing.ts ├── utils.ts └── validator.ts ├── messages ├── ar.json ├── en-US.json └── fr.json ├── middleware.ts ├── next.config.ts ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── public ├── icons │ └── logo.svg └── images │ ├── app.png │ ├── banner1.jpg │ ├── banner2.jpg │ ├── banner3.jpg │ ├── jeans.jpg │ ├── p11-1.jpg │ ├── p11-2.jpg │ ├── p12-1.jpg │ ├── p12-2.jpg │ ├── p12-3.jpg │ ├── p12-4.jpg │ ├── p13-1.jpg │ ├── p13-2.jpg │ ├── p14-1.jpg │ ├── p14-2.jpg │ ├── p15-1.jpg │ ├── p15-2.jpg │ ├── p16-1.jpg │ ├── p16-2.jpg │ ├── p21-1.jpg │ ├── p21-2.jpg │ ├── p22-1.jpg │ ├── p22-2.jpg │ ├── p23-1.jpg │ ├── p23-2.jpg │ ├── p24-1.jpg │ ├── p24-2.jpg │ ├── p25-1.jpg │ ├── p25-2.jpg │ ├── p26-1.jpg │ ├── p26-2.jpg │ ├── p31-1.jpg │ ├── p31-2.jpg │ ├── p32-1.jpg │ ├── p32-2.jpg │ ├── p33-1.jpg │ ├── p33-2.jpg │ ├── p34-1.jpg │ ├── p34-2.jpg │ ├── p35-1.jpg │ ├── p35-2.jpg │ ├── p36-1.jpg │ ├── p36-2.jpg │ ├── p41-1.jpg │ ├── p41-2.jpg │ ├── p42-1.jpg │ ├── p42-2.jpg │ ├── p43-1.jpg │ ├── p43-2.jpg │ ├── p44-1.jpg │ ├── p44-2.jpg │ ├── p45-1.jpg │ ├── p45-2.jpg │ ├── p46-1.jpg │ ├── p46-2.jpg │ ├── shoes.jpg │ ├── t-shirts.jpg │ └── wrist-watches.jpg ├── tailwind.config.ts ├── tsconfig.json └── types └── index.ts /.example-env: -------------------------------------------------------------------------------- 1 | 2 | NEXT_PUBLIC_APP_NAME=Amazona 3 | NEXT_PUBLIC_APP_DESCRIPTION=An Amazon clone built with Next.js, MongoDB, Shadcn 4 | NEXT_PUBLIC_SERVER_URL=http://localhost:3000 5 | 6 | # Database 7 | MONGODB_URI=mongodb://localhost/nextjs-amazona 8 | 9 | 10 | # $ npx auth secret 11 | AUTH_SECRET= 12 | AUTH_GOOGLE_ID= 13 | AUTH_GOOGLE_SECRET= 14 | 15 | RESEND_API_KEY= 16 | SENDER_EMAIL= 17 | 18 | # UPLOADTHING 19 | UPLOADTHING_TOKEN= 20 | # PAYPAL 21 | PAYPAL_API_URL= 22 | PAYPAL_CLIENT_ID= 23 | PAYPAL_APP_SECRET= 24 | 25 | # STRIPE 26 | NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY= 27 | STRIPE_SECRET_KEY= 28 | STRIPE_WEBHOOK_SECRET= -------------------------------------------------------------------------------- /.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.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env* 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | -------------------------------------------------------------------------------- /app/[locale]/(auth)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { getSetting } from '@/lib/actions/setting.actions' 2 | import Image from 'next/image' 3 | import Link from 'next/link' 4 | import React from 'react' 5 | 6 | export default async function AuthLayout({ 7 | children, 8 | }: { 9 | children: React.ReactNode 10 | }) { 11 | const { site } = await getSetting() 12 | return ( 13 |
14 |
15 | 16 | logo 27 | 28 |
29 |
{children}
30 | 40 |
41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /app/[locale]/(auth)/sign-in/google-signin-form.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { useFormStatus } from 'react-dom' 3 | 4 | import { Button } from '@/components/ui/button' 5 | import { SignInWithGoogle } from '@/lib/actions/user.actions' 6 | 7 | export function GoogleSignInForm() { 8 | const SignInButton = () => { 9 | const { pending } = useFormStatus() 10 | return ( 11 | 14 | ) 15 | } 16 | return ( 17 |
18 | 19 | 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /app/[locale]/(auth)/sign-in/page.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next' 2 | import Link from 'next/link' 3 | import { redirect } from 'next/navigation' 4 | 5 | import { auth } from '@/auth' 6 | import SeparatorWithOr from '@/components/shared/separator-or' 7 | import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' 8 | 9 | import CredentialsSignInForm from './credentials-signin-form' 10 | import { GoogleSignInForm } from './google-signin-form' 11 | import { Button } from '@/components/ui/button' 12 | import { getSetting } from '@/lib/actions/setting.actions' 13 | 14 | export const metadata: Metadata = { 15 | title: 'Sign In', 16 | } 17 | 18 | export default async function SignInPage(props: { 19 | searchParams: Promise<{ 20 | callbackUrl: string 21 | }> 22 | }) { 23 | const searchParams = await props.searchParams 24 | const { site } = await getSetting() 25 | 26 | const { callbackUrl = '/' } = searchParams 27 | 28 | const session = await auth() 29 | if (session) { 30 | return redirect(callbackUrl) 31 | } 32 | 33 | return ( 34 |
35 | 36 | 37 | Sign In 38 | 39 | 40 |
41 | 42 | 43 |
44 | 45 |
46 |
47 |
48 |
49 | New to {site.name}? 50 | 51 | 52 | 55 | 56 |
57 | ) 58 | } 59 | -------------------------------------------------------------------------------- /app/[locale]/(auth)/sign-up/page.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next' 2 | import { redirect } from 'next/navigation' 3 | 4 | import { auth } from '@/auth' 5 | import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' 6 | 7 | import SignUpForm from './signup-form' 8 | 9 | export const metadata: Metadata = { 10 | title: 'Sign Up', 11 | } 12 | 13 | export default async function SignUpPage(props: { 14 | searchParams: Promise<{ 15 | callbackUrl: string 16 | }> 17 | }) { 18 | const searchParams = await props.searchParams 19 | 20 | const { callbackUrl } = searchParams 21 | 22 | const session = await auth() 23 | if (session) { 24 | return redirect(callbackUrl || '/') 25 | } 26 | 27 | return ( 28 |
29 | 30 | 31 | Create account 32 | 33 | 34 | 35 | 36 | 37 |
38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /app/[locale]/(home)/layout.tsx: -------------------------------------------------------------------------------- 1 | import Header from '@/components/shared/header' 2 | import Footer from '@/components/shared/footer' 3 | 4 | export default async function HomeLayout({ 5 | children, 6 | }: { 7 | children: React.ReactNode 8 | }) { 9 | return ( 10 |
11 |
12 |
{children}
13 |
14 |
15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /app/[locale]/(home)/page.tsx: -------------------------------------------------------------------------------- 1 | import BrowsingHistoryList from '@/components/shared/browsing-history-list' 2 | import { HomeCard } from '@/components/shared/home/home-card' 3 | import { HomeCarousel } from '@/components/shared/home/home-carousel' 4 | import ProductSlider from '@/components/shared/product/product-slider' 5 | import { Card, CardContent } from '@/components/ui/card' 6 | 7 | import { 8 | getProductsForCard, 9 | getProductsByTag, 10 | getAllCategories, 11 | } from '@/lib/actions/product.actions' 12 | import { getSetting } from '@/lib/actions/setting.actions' 13 | import { toSlug } from '@/lib/utils' 14 | import { getTranslations } from 'next-intl/server' 15 | 16 | export default async function HomePage() { 17 | const t = await getTranslations('Home') 18 | const { carousels } = await getSetting() 19 | const todaysDeals = await getProductsByTag({ tag: 'todays-deal' }) 20 | const bestSellingProducts = await getProductsByTag({ tag: 'best-seller' }) 21 | 22 | const categories = (await getAllCategories()).slice(0, 4) 23 | const newArrivals = await getProductsForCard({ 24 | tag: 'new-arrival', 25 | }) 26 | const featureds = await getProductsForCard({ 27 | tag: 'featured', 28 | }) 29 | const bestSellers = await getProductsForCard({ 30 | tag: 'best-seller', 31 | }) 32 | const cards = [ 33 | { 34 | title: t('Categories to explore'), 35 | link: { 36 | text: t('See More'), 37 | href: '/search', 38 | }, 39 | items: categories.map((category) => ({ 40 | name: category, 41 | image: `/images/${toSlug(category)}.jpg`, 42 | href: `/search?category=${category}`, 43 | })), 44 | }, 45 | { 46 | title: t('Explore New Arrivals'), 47 | items: newArrivals, 48 | link: { 49 | text: t('View All'), 50 | href: '/search?tag=new-arrival', 51 | }, 52 | }, 53 | { 54 | title: t('Discover Best Sellers'), 55 | items: bestSellers, 56 | link: { 57 | text: t('View All'), 58 | href: '/search?tag=new-arrival', 59 | }, 60 | }, 61 | { 62 | title: t('Featured Products'), 63 | items: featureds, 64 | link: { 65 | text: t('Shop Now'), 66 | href: '/search?tag=new-arrival', 67 | }, 68 | }, 69 | ] 70 | 71 | return ( 72 | <> 73 | 74 |
75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 88 | 89 | 90 |
91 | 92 |
93 | 94 |
95 | 96 | ) 97 | } 98 | -------------------------------------------------------------------------------- /app/[locale]/(root)/account/layout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default async function AccountLayout({ 4 | children, 5 | }: { 6 | children: React.ReactNode 7 | }) { 8 | return ( 9 |
10 |
{children}
11 |
12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /app/[locale]/(root)/account/manage/name/page.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next' 2 | import { SessionProvider } from 'next-auth/react' 3 | 4 | import { auth } from '@/auth' 5 | 6 | import { ProfileForm } from './profile-form' 7 | import Link from 'next/link' 8 | import { Card, CardContent } from '@/components/ui/card' 9 | import { getSetting } from '@/lib/actions/setting.actions' 10 | 11 | const PAGE_TITLE = 'Change Your Name' 12 | export const metadata: Metadata = { 13 | title: PAGE_TITLE, 14 | } 15 | 16 | export default async function ProfilePage() { 17 | const session = await auth() 18 | const { site } = await getSetting() 19 | return ( 20 |
21 | 22 |
23 | Your Account 24 | 25 | Login & Security 26 | 27 | {PAGE_TITLE} 28 |
29 |

{PAGE_TITLE}

30 | 31 | 32 |

33 | If you want to change the name associated with your {site.name} 34 | 's account, you may do so below. Be sure to click the Save 35 | Changes button when you are done. 36 |

37 | 38 |
39 |
40 |
41 |
42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /app/[locale]/(root)/account/manage/name/profile-form.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { zodResolver } from '@hookform/resolvers/zod' 4 | import { useSession } from 'next-auth/react' 5 | import { useForm } from 'react-hook-form' 6 | import { z } from 'zod' 7 | import { useRouter } from 'next/navigation' 8 | 9 | import { Button } from '@/components/ui/button' 10 | import { 11 | Form, 12 | FormControl, 13 | FormField, 14 | FormItem, 15 | FormLabel, 16 | FormMessage, 17 | } from '@/components/ui/form' 18 | import { Input } from '@/components/ui/input' 19 | import { useToast } from '@/hooks/use-toast' 20 | import { updateUserName } from '@/lib/actions/user.actions' 21 | import { UserNameSchema } from '@/lib/validator' 22 | 23 | export const ProfileForm = () => { 24 | const router = useRouter() 25 | const { data: session, update } = useSession() 26 | const form = useForm>({ 27 | resolver: zodResolver(UserNameSchema), 28 | defaultValues: { 29 | name: session?.user?.name ?? '', 30 | }, 31 | }) 32 | const { toast } = useToast() 33 | 34 | async function onSubmit(values: z.infer) { 35 | const res = await updateUserName(values) 36 | if (!res.success) 37 | return toast({ 38 | variant: 'destructive', 39 | description: res.message, 40 | }) 41 | 42 | const { data, message } = res 43 | const newSession = { 44 | ...session, 45 | user: { 46 | ...session?.user, 47 | name: data.name, 48 | }, 49 | } 50 | await update(newSession) 51 | toast({ 52 | description: message, 53 | }) 54 | router.push('/account/manage') 55 | } 56 | return ( 57 |
58 | 62 |
63 | ( 67 | 68 | New name 69 | 70 | 75 | 76 | 77 | 78 | )} 79 | /> 80 |
81 | 82 | 90 |
91 | 92 | ) 93 | } 94 | -------------------------------------------------------------------------------- /app/[locale]/(root)/account/manage/page.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next' 2 | import { SessionProvider } from 'next-auth/react' 3 | 4 | import { auth } from '@/auth' 5 | 6 | import Link from 'next/link' 7 | import { Card, CardContent } from '@/components/ui/card' 8 | import { Separator } from '@/components/ui/separator' 9 | import { Button } from '@/components/ui/button' 10 | 11 | const PAGE_TITLE = 'Login & Security' 12 | export const metadata: Metadata = { 13 | title: PAGE_TITLE, 14 | } 15 | export default async function ProfilePage() { 16 | const session = await auth() 17 | return ( 18 |
19 | 20 |
21 | Your Account 22 | 23 | {PAGE_TITLE} 24 |
25 |

{PAGE_TITLE}

26 | 27 | 28 |
29 |

Name

30 |

{session?.user.name}

31 |
32 |
33 | 34 | 37 | 38 |
39 |
40 | 41 | 42 |
43 |

Email

44 |

{session?.user.email}

45 |

will be implemented in the next version

46 |
47 |
48 | 49 | 56 | 57 |
58 |
59 | 60 | 61 |
62 |

Password

63 |

************

64 |

will be implemented in the next version

65 |
66 |
67 | 68 | 75 | 76 |
77 |
78 |
79 |
80 |
81 | ) 82 | } 83 | -------------------------------------------------------------------------------- /app/[locale]/(root)/account/orders/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import { notFound } from 'next/navigation' 2 | import React from 'react' 3 | 4 | import { auth } from '@/auth' 5 | import { getOrderById } from '@/lib/actions/order.actions' 6 | import OrderDetailsForm from '@/components/shared/order/order-details-form' 7 | import Link from 'next/link' 8 | import { formatId } from '@/lib/utils' 9 | 10 | export async function generateMetadata(props: { 11 | params: Promise<{ id: string }> 12 | }) { 13 | const params = await props.params 14 | 15 | return { 16 | title: `Order ${formatId(params.id)}`, 17 | } 18 | } 19 | 20 | export default async function OrderDetailsPage(props: { 21 | params: Promise<{ 22 | id: string 23 | }> 24 | }) { 25 | const params = await props.params 26 | 27 | const { id } = params 28 | 29 | const order = await getOrderById(id) 30 | if (!order) notFound() 31 | 32 | const session = await auth() 33 | 34 | return ( 35 | <> 36 |
37 | Your Account 38 | 39 | Your Orders 40 | 41 | Order {formatId(order._id)} 42 |
43 |

Order {formatId(order._id)}

44 | 48 | 49 | ) 50 | } 51 | -------------------------------------------------------------------------------- /app/[locale]/(root)/account/orders/page.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next' 2 | import Link from 'next/link' 3 | 4 | import Pagination from '@/components/shared/pagination' 5 | import { 6 | Table, 7 | TableBody, 8 | TableCell, 9 | TableHead, 10 | TableHeader, 11 | TableRow, 12 | } from '@/components/ui/table' 13 | import { getMyOrders } from '@/lib/actions/order.actions' 14 | import { IOrder } from '@/lib/db/models/order.model' 15 | import { formatDateTime, formatId } from '@/lib/utils' 16 | import BrowsingHistoryList from '@/components/shared/browsing-history-list' 17 | import ProductPrice from '@/components/shared/product/product-price' 18 | 19 | const PAGE_TITLE = 'Your Orders' 20 | export const metadata: Metadata = { 21 | title: PAGE_TITLE, 22 | } 23 | export default async function OrdersPage(props: { 24 | searchParams: Promise<{ page: string }> 25 | }) { 26 | const searchParams = await props.searchParams 27 | const page = Number(searchParams.page) || 1 28 | const orders = await getMyOrders({ 29 | page, 30 | }) 31 | return ( 32 |
33 |
34 | Your Account 35 | 36 | {PAGE_TITLE} 37 |
38 |

{PAGE_TITLE}

39 |
40 | 41 | 42 | 43 | Id 44 | Date 45 | Total 46 | Paid 47 | Delivered 48 | Actions 49 | 50 | 51 | 52 | {orders.data.length === 0 && ( 53 | 54 | 55 | You have no orders. 56 | 57 | 58 | )} 59 | {orders.data.map((order: IOrder) => ( 60 | 61 | 62 | 63 | {formatId(order._id)} 64 | 65 | 66 | 67 | {formatDateTime(order.createdAt!).dateTime} 68 | 69 | 70 | 71 | 72 | 73 | {order.isPaid && order.paidAt 74 | ? formatDateTime(order.paidAt).dateTime 75 | : 'No'} 76 | 77 | 78 | {order.isDelivered && order.deliveredAt 79 | ? formatDateTime(order.deliveredAt).dateTime 80 | : 'No'} 81 | 82 | 83 | 84 | Details 85 | 86 | 87 | 88 | ))} 89 | 90 |
91 | {orders.totalPages > 1 && ( 92 | 93 | )} 94 |
95 | 96 |
97 | ) 98 | } 99 | -------------------------------------------------------------------------------- /app/[locale]/(root)/account/page.tsx: -------------------------------------------------------------------------------- 1 | import BrowsingHistoryList from '@/components/shared/browsing-history-list' 2 | import { Card, CardContent } from '@/components/ui/card' 3 | import { Home, PackageCheckIcon, User } from 'lucide-react' 4 | import { Metadata } from 'next' 5 | import Link from 'next/link' 6 | import React from 'react' 7 | 8 | const PAGE_TITLE = 'Your Account' 9 | export const metadata: Metadata = { 10 | title: PAGE_TITLE, 11 | } 12 | export default function AccountPage() { 13 | return ( 14 |
15 |

{PAGE_TITLE}

16 |
17 | 18 | 19 | 20 |
21 | 22 |
23 |
24 |

Orders

25 |

26 | Track, return, cancel an order, download invoice or buy again 27 |

28 |
29 |
30 | 31 |
32 | 33 | 34 | 35 | 36 |
37 | 38 |
39 |
40 |

Login & security

41 |

42 | Manage password, email and mobile number 43 |

44 |
45 |
46 | 47 |
48 | 49 | 50 | 51 | 52 |
53 | 54 |
55 |
56 |

Addresses

57 |

58 | Edit, remove or set default address 59 |

60 |
61 |
62 | 63 |
64 |
65 | 66 |
67 | ) 68 | } 69 | -------------------------------------------------------------------------------- /app/[locale]/(root)/cart/[itemId]/page.tsx: -------------------------------------------------------------------------------- 1 | import CartAddItem from './cart-add-item' 2 | 3 | export default async function CartAddItemPage(props: { 4 | params: Promise<{ itemId: string }> 5 | }) { 6 | const { itemId } = await props.params 7 | 8 | return 9 | } 10 | -------------------------------------------------------------------------------- /app/[locale]/(root)/layout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import Header from '@/components/shared/header' 4 | import Footer from '@/components/shared/footer' 5 | 6 | export default async function RootLayout({ 7 | children, 8 | }: { 9 | children: React.ReactNode 10 | }) { 11 | return ( 12 |
13 |
14 |
{children}
15 |
16 |
17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /app/[locale]/(root)/page/[slug]/page.tsx: -------------------------------------------------------------------------------- 1 | import ReactMarkdown from 'react-markdown' 2 | import { notFound } from 'next/navigation' 3 | import { getWebPageBySlug } from '@/lib/actions/web-page.actions' 4 | 5 | export async function generateMetadata(props: { 6 | params: Promise<{ slug: string }> 7 | }) { 8 | const params = await props.params 9 | 10 | const { slug } = params 11 | 12 | const webPage = await getWebPageBySlug(slug) 13 | if (!webPage) { 14 | return { title: 'Web page not found' } 15 | } 16 | return { 17 | title: webPage.title, 18 | } 19 | } 20 | 21 | export default async function ProductDetailsPage(props: { 22 | params: Promise<{ slug: string }> 23 | searchParams: Promise<{ page: string; color: string; size: string }> 24 | }) { 25 | const params = await props.params 26 | const { slug } = params 27 | const webPage = await getWebPageBySlug(slug) 28 | 29 | if (!webPage) notFound() 30 | 31 | return ( 32 |
33 |

{webPage.title}

34 |
35 | {webPage.content} 36 |
37 |
38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /app/[locale]/admin/admin-nav.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import Link from 'next/link' 3 | import { usePathname } from 'next/navigation' 4 | import React from 'react' 5 | 6 | import { cn } from '@/lib/utils' 7 | import { useTranslations } from 'next-intl' 8 | 9 | const links = [ 10 | { 11 | title: 'Overview', 12 | href: '/admin/overview', 13 | }, 14 | { 15 | title: 'Products', 16 | href: '/admin/products', 17 | }, 18 | { 19 | title: 'Orders', 20 | href: '/admin/orders', 21 | }, 22 | { 23 | title: 'Users', 24 | href: '/admin/users', 25 | }, 26 | { 27 | title: 'Pages', 28 | href: '/admin/web-pages', 29 | }, 30 | { 31 | title: 'Settings', 32 | href: '/admin/settings', 33 | }, 34 | ] 35 | export function AdminNav({ 36 | className, 37 | ...props 38 | }: React.HTMLAttributes) { 39 | const pathname = usePathname() 40 | const t = useTranslations('Admin') 41 | return ( 42 | 62 | ) 63 | } 64 | -------------------------------------------------------------------------------- /app/[locale]/admin/layout.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image' 2 | import Link from 'next/link' 3 | import React from 'react' 4 | import Menu from '@/components/shared/header/menu' 5 | import { AdminNav } from './admin-nav' 6 | import { getSetting } from '@/lib/actions/setting.actions' 7 | 8 | export default async function AdminLayout({ 9 | children, 10 | }: { 11 | children: React.ReactNode 12 | }) { 13 | const { site } = await getSetting() 14 | return ( 15 | <> 16 |
17 |
18 |
19 | 20 | {`${site.name} 26 | 27 | 28 |
29 | 30 |
31 |
32 |
33 | 34 |
35 |
36 |
{children}
37 |
38 | 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /app/[locale]/admin/orders/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import { notFound } from 'next/navigation' 2 | import React from 'react' 3 | 4 | import { auth } from '@/auth' 5 | import { getOrderById } from '@/lib/actions/order.actions' 6 | import OrderDetailsForm from '@/components/shared/order/order-details-form' 7 | import Link from 'next/link' 8 | 9 | export const metadata = { 10 | title: 'Admin Order Details', 11 | } 12 | 13 | const AdminOrderDetailsPage = async (props: { 14 | params: Promise<{ 15 | id: string 16 | }> 17 | }) => { 18 | const params = await props.params 19 | 20 | const { id } = params 21 | 22 | const order = await getOrderById(id) 23 | if (!order) notFound() 24 | 25 | const session = await auth() 26 | 27 | return ( 28 |
29 |
30 | Orders 31 | {order._id} 32 |
33 | 37 |
38 | ) 39 | } 40 | 41 | export default AdminOrderDetailsPage 42 | -------------------------------------------------------------------------------- /app/[locale]/admin/orders/page.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next' 2 | import Link from 'next/link' 3 | 4 | import { auth } from '@/auth' 5 | import DeleteDialog from '@/components/shared/delete-dialog' 6 | import Pagination from '@/components/shared/pagination' 7 | import { Button } from '@/components/ui/button' 8 | import { 9 | Table, 10 | TableBody, 11 | TableCell, 12 | TableHead, 13 | TableHeader, 14 | TableRow, 15 | } from '@/components/ui/table' 16 | import { deleteOrder, getAllOrders } from '@/lib/actions/order.actions' 17 | import { formatDateTime, formatId } from '@/lib/utils' 18 | import { IOrderList } from '@/types' 19 | import ProductPrice from '@/components/shared/product/product-price' 20 | 21 | export const metadata: Metadata = { 22 | title: 'Admin Orders', 23 | } 24 | export default async function OrdersPage(props: { 25 | searchParams: Promise<{ page: string }> 26 | }) { 27 | const searchParams = await props.searchParams 28 | 29 | const { page = '1' } = searchParams 30 | 31 | const session = await auth() 32 | if (session?.user.role !== 'Admin') 33 | throw new Error('Admin permission required') 34 | 35 | const orders = await getAllOrders({ 36 | page: Number(page), 37 | }) 38 | return ( 39 |
40 |

Orders

41 |
42 | 43 | 44 | 45 | Id 46 | Date 47 | Buyer 48 | Total 49 | Paid 50 | Delivered 51 | Actions 52 | 53 | 54 | 55 | {orders.data.map((order: IOrderList) => ( 56 | 57 | {formatId(order._id)} 58 | 59 | {formatDateTime(order.createdAt!).dateTime} 60 | 61 | 62 | {order.user ? order.user.name : 'Deleted User'} 63 | 64 | 65 | {' '} 66 | 67 | 68 | 69 | {order.isPaid && order.paidAt 70 | ? formatDateTime(order.paidAt).dateTime 71 | : 'No'} 72 | 73 | 74 | {order.isDelivered && order.deliveredAt 75 | ? formatDateTime(order.deliveredAt).dateTime 76 | : 'No'} 77 | 78 | 79 | 82 | 83 | 84 | 85 | ))} 86 | 87 |
88 | {orders.totalPages > 1 && ( 89 | 90 | )} 91 |
92 |
93 | ) 94 | } 95 | -------------------------------------------------------------------------------- /app/[locale]/admin/overview/date-range-picker.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as React from 'react' 4 | import { CalendarIcon } from 'lucide-react' 5 | import { DateRange } from 'react-day-picker' 6 | 7 | import { cn, formatDateTime } from '@/lib/utils' 8 | import { 9 | Popover, 10 | PopoverContent, 11 | PopoverTrigger, 12 | } from '@/components/ui/popover' 13 | import { Button } from '@/components/ui/button' 14 | import { Calendar } from '@/components/ui/calendar' 15 | import { PopoverClose } from '@radix-ui/react-popover' 16 | 17 | export function CalendarDateRangePicker({ 18 | defaultDate, 19 | setDate, 20 | className, 21 | }: { 22 | defaultDate?: DateRange 23 | setDate: React.Dispatch> 24 | className?: string 25 | }) { 26 | const [calendarDate, setCalendarDate] = React.useState( 27 | defaultDate 28 | ) 29 | 30 | return ( 31 |
32 | 33 | 34 | 56 | 57 | setCalendarDate(defaultDate)} 59 | className='w-auto p-0' 60 | align='end' 61 | > 62 | 69 |
70 | 71 | 72 | 73 | 74 | 75 | 76 |
77 |
78 |
79 |
80 | ) 81 | } 82 | -------------------------------------------------------------------------------- /app/[locale]/admin/overview/page.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next' 2 | 3 | import OverviewReport from './overview-report' 4 | import { auth } from '@/auth' 5 | export const metadata: Metadata = { 6 | title: 'Admin Dashboard', 7 | } 8 | const DashboardPage = async () => { 9 | const session = await auth() 10 | if (session?.user.role !== 'Admin') 11 | throw new Error('Admin permission required') 12 | 13 | return 14 | } 15 | 16 | export default DashboardPage 17 | -------------------------------------------------------------------------------- /app/[locale]/admin/overview/sales-area-chart.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | 'use client' 3 | 4 | import ProductPrice from '@/components/shared/product/product-price' 5 | import { Card, CardContent } from '@/components/ui/card' 6 | import useColorStore from '@/hooks/use-color-store' 7 | import { formatDateTime } from '@/lib/utils' 8 | import { useTheme } from 'next-themes' 9 | import React from 'react' 10 | import { 11 | Area, 12 | AreaChart, 13 | CartesianGrid, 14 | ResponsiveContainer, 15 | Tooltip, 16 | TooltipProps, 17 | XAxis, 18 | YAxis, 19 | } from 'recharts' 20 | 21 | interface CustomTooltipProps extends TooltipProps { 22 | active?: boolean 23 | payload?: { value: number }[] 24 | label?: string 25 | } 26 | 27 | const CustomTooltip: React.FC = ({ 28 | active, 29 | payload, 30 | label, 31 | }) => { 32 | if (active && payload && payload.length) { 33 | return ( 34 | 35 | 36 |

{label && formatDateTime(new Date(label)).dateOnly}

37 |

38 | 39 |

40 |
41 |
42 | ) 43 | } 44 | return null 45 | } 46 | 47 | const CustomXAxisTick: React.FC = ({ x, y, payload }) => { 48 | return ( 49 | 50 | {formatDateTime(new Date(payload.value)).dateOnly} 51 | {/* {`${payload.value.split('/')[1]}/${payload.value.split('/')[2]}`} */} 52 | 53 | ) 54 | } 55 | const STROKE_COLORS: { [key: string]: { [key: string]: string } } = { 56 | Red: { light: '#980404', dark: '#ff3333' }, 57 | Green: { light: '#015001', dark: '#06dc06' }, 58 | Gold: { light: '#ac9103', dark: '#f1d541' }, 59 | } 60 | 61 | export default function SalesAreaChart({ data }: { data: any[] }) { 62 | const { theme } = useTheme() 63 | const { cssColors, color } = useColorStore(theme) 64 | 65 | return ( 66 | 67 | 68 | 69 | } interval={3} /> 70 | `$${value}`} /> 71 | } /> 72 | 80 | 81 | 82 | ) 83 | } 84 | -------------------------------------------------------------------------------- /app/[locale]/admin/overview/sales-category-pie-chart.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | 'use client' 3 | 4 | import useColorStore from '@/hooks/use-color-store' 5 | import { useTheme } from 'next-themes' 6 | import React from 'react' 7 | import { PieChart, Pie, ResponsiveContainer, Cell } from 'recharts' 8 | 9 | export default function SalesCategoryPieChart({ data }: { data: any[] }) { 10 | const { theme } = useTheme() 11 | const { cssColors } = useColorStore(theme) 12 | 13 | const RADIAN = Math.PI / 180 14 | const renderCustomizedLabel = ({ 15 | cx, 16 | cy, 17 | midAngle, 18 | innerRadius, 19 | outerRadius, 20 | index, 21 | }: any) => { 22 | const radius = innerRadius + (outerRadius - innerRadius) * 0.5 23 | const x = cx + radius * Math.cos(-midAngle * RADIAN) 24 | const y = cy + radius * Math.sin(-midAngle * RADIAN) 25 | 26 | return ( 27 | <> 28 | cx ? 'start' : 'end'} 32 | dominantBaseline='central' 33 | className='text-xs' 34 | > 35 | {`${data[index]._id} ${data[index].totalSales} sales`} 36 | 37 | 38 | ) 39 | } 40 | 41 | return ( 42 | 43 | 44 | 52 | {data.map((entry, index) => ( 53 | 57 | ))} 58 | 59 | 60 | 61 | ) 62 | } 63 | -------------------------------------------------------------------------------- /app/[locale]/admin/overview/table-chart.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import ProductPrice from '@/components/shared/product/product-price' 4 | import { getMonthName } from '@/lib/utils' 5 | import Image from 'next/image' 6 | import Link from 'next/link' 7 | 8 | type TableChartProps = { 9 | labelType: 'month' | 'product' 10 | data: { 11 | label: string 12 | image?: string 13 | value: number 14 | id?: string 15 | }[] 16 | } 17 | 18 | import React from 'react' 19 | 20 | interface ProgressBarProps { 21 | value: number // Accepts a number between 0 and 100 22 | className?: string 23 | } 24 | 25 | const ProgressBar: React.FC = ({ value }) => { 26 | // Ensure value stays within 0-100 range 27 | const boundedValue = Math.min(100, Math.max(0, value)) 28 | 29 | return ( 30 |
31 |
38 |
39 | ) 40 | } 41 | 42 | export default function TableChart({ 43 | labelType = 'month', 44 | data = [], 45 | }: TableChartProps) { 46 | const max = Math.max(...data.map((item) => item.value)) 47 | const dataWithPercentage = data.map((x) => ({ 48 | ...x, 49 | label: labelType === 'month' ? getMonthName(x.label) : x.label, 50 | percentage: Math.round((x.value / max) * 100), 51 | })) 52 | return ( 53 |
54 | {dataWithPercentage.map(({ label, id, value, image, percentage }) => ( 55 |
59 | {image ? ( 60 | 61 | {label} 68 |

69 | {label} 70 |

71 | 72 | ) : ( 73 |
{label}
74 | )} 75 | 76 | 77 | 78 |
79 | 80 |
81 |
82 | ))} 83 |
84 | ) 85 | } 86 | -------------------------------------------------------------------------------- /app/[locale]/admin/products/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import { notFound } from 'next/navigation' 2 | 3 | import { getProductById } from '@/lib/actions/product.actions' 4 | import Link from 'next/link' 5 | import ProductForm from '../product-form' 6 | import { Metadata } from 'next' 7 | 8 | export const metadata: Metadata = { 9 | title: 'Edit Product', 10 | } 11 | 12 | type UpdateProductProps = { 13 | params: Promise<{ 14 | id: string 15 | }> 16 | } 17 | 18 | const UpdateProduct = async (props: UpdateProductProps) => { 19 | const params = await props.params 20 | 21 | const { id } = params 22 | 23 | const product = await getProductById(id) 24 | if (!product) notFound() 25 | return ( 26 |
27 |
28 | Products 29 | 30 | {product._id} 31 |
32 | 33 |
34 | 35 |
36 |
37 | ) 38 | } 39 | 40 | export default UpdateProduct 41 | -------------------------------------------------------------------------------- /app/[locale]/admin/products/create/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import ProductForm from '../product-form' 3 | import { Metadata } from 'next' 4 | 5 | export const metadata: Metadata = { 6 | title: 'Create Product', 7 | } 8 | 9 | const CreateProductPage = () => { 10 | return ( 11 |
12 |
13 | Products 14 | 15 | Create 16 |
17 | 18 |
19 | 20 |
21 |
22 | ) 23 | } 24 | 25 | export default CreateProductPage 26 | -------------------------------------------------------------------------------- /app/[locale]/admin/products/page.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next' 2 | import ProductList from './product-list' 3 | 4 | export const metadata: Metadata = { 5 | title: 'Admin Products', 6 | } 7 | 8 | export default async function AdminProduct() { 9 | return 10 | } 11 | -------------------------------------------------------------------------------- /app/[locale]/admin/settings/page.tsx: -------------------------------------------------------------------------------- 1 | import { getNoCachedSetting } from '@/lib/actions/setting.actions' 2 | import SettingForm from './setting-form' 3 | import SettingNav from './setting-nav' 4 | 5 | import { Metadata } from 'next' 6 | 7 | export const metadata: Metadata = { 8 | title: 'Setting', 9 | } 10 | const SettingPage = async () => { 11 | return ( 12 |
13 | 14 |
15 |
16 | 17 |
18 |
19 |
20 | ) 21 | } 22 | 23 | export default SettingPage 24 | -------------------------------------------------------------------------------- /app/[locale]/admin/settings/setting-form.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { zodResolver } from '@hookform/resolvers/zod' 4 | import { useForm } from 'react-hook-form' 5 | import { Button } from '@/components/ui/button' 6 | import { Form } from '@/components/ui/form' 7 | import { useToast } from '@/hooks/use-toast' 8 | import { SettingInputSchema } from '@/lib/validator' 9 | import { ClientSetting, ISettingInput } from '@/types' 10 | import { updateSetting } from '@/lib/actions/setting.actions' 11 | import useSetting from '@/hooks/use-setting-store' 12 | import LanguageForm from './language-form' 13 | import CurrencyForm from './currency-form' 14 | import PaymentMethodForm from './payment-method-form' 15 | import DeliveryDateForm from './delivery-date-form' 16 | import SiteInfoForm from './site-info-form' 17 | import CommonForm from './common-form' 18 | import CarouselForm from './carousel-form' 19 | 20 | const SettingForm = ({ setting }: { setting: ISettingInput }) => { 21 | const { setSetting } = useSetting() 22 | 23 | const form = useForm({ 24 | resolver: zodResolver(SettingInputSchema), 25 | defaultValues: setting, 26 | }) 27 | const { 28 | formState: { isSubmitting }, 29 | } = form 30 | 31 | const { toast } = useToast() 32 | async function onSubmit(values: ISettingInput) { 33 | const res = await updateSetting({ ...values }) 34 | if (!res.success) { 35 | toast({ 36 | variant: 'destructive', 37 | description: res.message, 38 | }) 39 | } else { 40 | toast({ 41 | description: res.message, 42 | }) 43 | setSetting(values as ClientSetting) 44 | } 45 | } 46 | 47 | return ( 48 |
49 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 |
67 | 75 |
76 | 77 | 78 | ) 79 | } 80 | 81 | export default SettingForm 82 | -------------------------------------------------------------------------------- /app/[locale]/admin/settings/setting-nav.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { Button } from '@/components/ui/button' 3 | import { 4 | CreditCard, 5 | Currency, 6 | ImageIcon, 7 | Info, 8 | Languages, 9 | Package, 10 | SettingsIcon, 11 | } from 'lucide-react' 12 | 13 | import { useEffect, useState } from 'react' 14 | 15 | const SettingNav = () => { 16 | const [active, setActive] = useState('') 17 | 18 | useEffect(() => { 19 | const sections = document.querySelectorAll('div[id^="setting-"]') 20 | 21 | const observer = new IntersectionObserver( 22 | (entries) => { 23 | entries.forEach((entry) => { 24 | if (entry.isIntersecting) { 25 | setActive(entry.target.id) 26 | } 27 | }) 28 | }, 29 | { threshold: 0.6, rootMargin: '0px 0px -40% 0px' } 30 | ) 31 | sections.forEach((section) => observer.observe(section)) 32 | return () => observer.disconnect() 33 | }, []) 34 | const handleScroll = (id: string) => { 35 | const section = document.getElementById(id) 36 | if (section) { 37 | const top = section.offsetTop - 16 // 20px above the section 38 | window.scrollTo({ top, behavior: 'smooth' }) 39 | } 40 | } 41 | 42 | return ( 43 |
44 |

Setting

45 | 88 |
89 | ) 90 | } 91 | 92 | export default SettingNav 93 | -------------------------------------------------------------------------------- /app/[locale]/admin/users/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import { notFound } from 'next/navigation' 2 | 3 | import { getUserById } from '@/lib/actions/user.actions' 4 | 5 | import UserEditForm from './user-edit-form' 6 | import Link from 'next/link' 7 | import { Metadata } from 'next' 8 | 9 | export const metadata: Metadata = { 10 | title: 'Edit User', 11 | } 12 | 13 | export default async function UserEditPage(props: { 14 | params: Promise<{ 15 | id: string 16 | }> 17 | }) { 18 | const params = await props.params 19 | 20 | const { id } = params 21 | 22 | const user = await getUserById(id) 23 | if (!user) notFound() 24 | return ( 25 |
26 |
27 | Users 28 | 29 | {user._id} 30 |
31 | 32 |
33 | 34 |
35 |
36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /app/[locale]/admin/users/page.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next' 2 | import Link from 'next/link' 3 | 4 | import { auth } from '@/auth' 5 | import DeleteDialog from '@/components/shared/delete-dialog' 6 | import Pagination from '@/components/shared/pagination' 7 | import { Button } from '@/components/ui/button' 8 | import { 9 | Table, 10 | TableBody, 11 | TableCell, 12 | TableHead, 13 | TableHeader, 14 | TableRow, 15 | } from '@/components/ui/table' 16 | import { deleteUser, getAllUsers } from '@/lib/actions/user.actions' 17 | import { IUser } from '@/lib/db/models/user.model' 18 | import { formatId } from '@/lib/utils' 19 | 20 | export const metadata: Metadata = { 21 | title: 'Admin Users', 22 | } 23 | 24 | export default async function AdminUser(props: { 25 | searchParams: Promise<{ page: string }> 26 | }) { 27 | const searchParams = await props.searchParams 28 | const session = await auth() 29 | if (session?.user.role !== 'Admin') 30 | throw new Error('Admin permission required') 31 | const page = Number(searchParams.page) || 1 32 | const users = await getAllUsers({ 33 | page, 34 | }) 35 | return ( 36 |
37 |

Users

38 |
39 | 40 | 41 | 42 | Id 43 | Name 44 | Email 45 | Role 46 | Actions 47 | 48 | 49 | 50 | {users?.data.map((user: IUser) => ( 51 | 52 | {formatId(user._id)} 53 | {user.name} 54 | {user.email} 55 | {user.role} 56 | 57 | 60 | 61 | 62 | 63 | ))} 64 | 65 |
66 | {users?.totalPages > 1 && ( 67 | 68 | )} 69 |
70 |
71 | ) 72 | } 73 | -------------------------------------------------------------------------------- /app/[locale]/admin/web-pages/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import { notFound } from 'next/navigation' 2 | 3 | import { getWebPageById } from '@/lib/actions/web-page.actions' 4 | import Link from 'next/link' 5 | import WebPageForm from '../web-page-form' 6 | 7 | type UpdateWebPageProps = { 8 | params: Promise<{ 9 | id: string 10 | }> 11 | } 12 | 13 | const UpdateWebPage = async (props: UpdateWebPageProps) => { 14 | const params = await props.params 15 | 16 | const { id } = params 17 | 18 | const webPage = await getWebPageById(id) 19 | if (!webPage) notFound() 20 | return ( 21 |
22 |
23 | Web Pages 24 | 25 | {webPage._id} 26 |
27 | 28 |
29 | 30 |
31 |
32 | ) 33 | } 34 | 35 | export default UpdateWebPage 36 | -------------------------------------------------------------------------------- /app/[locale]/admin/web-pages/create/page.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next' 2 | import WebPageForm from '../web-page-form' 3 | 4 | export const metadata: Metadata = { 5 | title: 'Create WebPage', 6 | } 7 | 8 | export default function CreateWebPagePage() { 9 | return ( 10 | <> 11 |

Create WebPage

12 | 13 |
14 | 15 |
16 | 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /app/[locale]/admin/web-pages/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | 3 | import DeleteDialog from '@/components/shared/delete-dialog' 4 | import { Button } from '@/components/ui/button' 5 | import { 6 | Table, 7 | TableBody, 8 | TableCell, 9 | TableHead, 10 | TableHeader, 11 | TableRow, 12 | } from '@/components/ui/table' 13 | import { formatId } from '@/lib/utils' 14 | import { Metadata } from 'next' 15 | import { deleteWebPage, getAllWebPages } from '@/lib/actions/web-page.actions' 16 | import { IWebPage } from '@/lib/db/models/web-page.model' 17 | 18 | export const metadata: Metadata = { 19 | title: 'Admin Web Pages', 20 | } 21 | 22 | export default async function WebPageAdminPage() { 23 | const webPages = await getAllWebPages() 24 | return ( 25 |
26 |
27 |

Web Pages

28 | 31 |
32 |
33 | 34 | 35 | 36 | Id 37 | Name 38 | Slug 39 | IsPublished 40 | Actions 41 | 42 | 43 | 44 | {webPages.map((webPage: IWebPage) => ( 45 | 46 | {formatId(webPage._id)} 47 | {webPage.title} 48 | {webPage.slug} 49 | {webPage.isPublished ? 'Yes' : 'No'} 50 | 51 | 54 | 55 | 56 | 57 | ))} 58 | 59 |
60 |
61 |
62 | ) 63 | } 64 | -------------------------------------------------------------------------------- /app/[locale]/checkout/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import { notFound } from 'next/navigation' 2 | import React from 'react' 3 | 4 | import { auth } from '@/auth' 5 | import { getOrderById } from '@/lib/actions/order.actions' 6 | import PaymentForm from './payment-form' 7 | import Stripe from 'stripe' 8 | 9 | export const metadata = { 10 | title: 'Payment', 11 | } 12 | 13 | const CheckoutPaymentPage = async (props: { 14 | params: Promise<{ 15 | id: string 16 | }> 17 | }) => { 18 | const params = await props.params 19 | 20 | const { id } = params 21 | 22 | const order = await getOrderById(id) 23 | if (!order) notFound() 24 | 25 | const session = await auth() 26 | 27 | let client_secret = null 28 | if (order.paymentMethod === 'Stripe' && !order.isPaid) { 29 | const stripe = new Stripe(process.env.STRIPE_SECRET_KEY as string) 30 | const paymentIntent = await stripe.paymentIntents.create({ 31 | amount: Math.round(order.totalPrice * 100), 32 | currency: 'USD', 33 | metadata: { orderId: order._id }, 34 | }) 35 | client_secret = paymentIntent.client_secret 36 | } 37 | return ( 38 | 44 | ) 45 | } 46 | 47 | export default CheckoutPaymentPage 48 | -------------------------------------------------------------------------------- /app/[locale]/checkout/[id]/stripe-form.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | LinkAuthenticationElement, 3 | PaymentElement, 4 | useElements, 5 | useStripe, 6 | } from '@stripe/react-stripe-js' 7 | import { FormEvent, useState } from 'react' 8 | 9 | import { Button } from '@/components/ui/button' 10 | import ProductPrice from '@/components/shared/product/product-price' 11 | import useSettingStore from '@/hooks/use-setting-store' 12 | 13 | export default function StripeForm({ 14 | priceInCents, 15 | orderId, 16 | }: { 17 | priceInCents: number 18 | orderId: string 19 | }) { 20 | const { 21 | setting: { site }, 22 | } = useSettingStore() 23 | 24 | const stripe = useStripe() 25 | const elements = useElements() 26 | const [isLoading, setIsLoading] = useState(false) 27 | const [errorMessage, setErrorMessage] = useState() 28 | const [email, setEmail] = useState() 29 | 30 | async function handleSubmit(e: FormEvent) { 31 | e.preventDefault() 32 | 33 | if (stripe == null || elements == null || email == null) return 34 | 35 | setIsLoading(true) 36 | stripe 37 | .confirmPayment({ 38 | elements, 39 | confirmParams: { 40 | return_url: `${site.url}/checkout/${orderId}/stripe-payment-success`, 41 | }, 42 | }) 43 | .then(({ error }) => { 44 | if (error.type === 'card_error' || error.type === 'validation_error') { 45 | setErrorMessage(error.message) 46 | } else { 47 | setErrorMessage('An unknown error occurred') 48 | } 49 | }) 50 | .finally(() => setIsLoading(false)) 51 | } 52 | 53 | return ( 54 |
55 |
Stripe Checkout
56 | {errorMessage &&
{errorMessage}
} 57 | 58 |
59 | setEmail(e.value.email)} /> 60 |
61 | 74 | 75 | ) 76 | } 77 | -------------------------------------------------------------------------------- /app/[locale]/checkout/[id]/stripe-payment-success/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import { notFound, redirect } from 'next/navigation' 3 | import Stripe from 'stripe' 4 | 5 | import { Button } from '@/components/ui/button' 6 | import { getOrderById } from '@/lib/actions/order.actions' 7 | 8 | const stripe = new Stripe(process.env.STRIPE_SECRET_KEY as string) 9 | 10 | export default async function SuccessPage(props: { 11 | params: Promise<{ 12 | id: string 13 | }> 14 | searchParams: Promise<{ payment_intent: string }> 15 | }) { 16 | const params = await props.params 17 | 18 | const { id } = params 19 | 20 | const searchParams = await props.searchParams 21 | const order = await getOrderById(id) 22 | if (!order) notFound() 23 | 24 | const paymentIntent = await stripe.paymentIntents.retrieve( 25 | searchParams.payment_intent 26 | ) 27 | if ( 28 | paymentIntent.metadata.orderId == null || 29 | paymentIntent.metadata.orderId !== order._id.toString() 30 | ) 31 | return notFound() 32 | 33 | const isSuccess = paymentIntent.status === 'succeeded' 34 | if (!isSuccess) return redirect(`/checkout/${id}`) 35 | return ( 36 |
37 |
38 |

39 | Thanks for your purchase 40 |

41 |
We are now processing your order.
42 | 45 |
46 |
47 | ) 48 | } 49 | -------------------------------------------------------------------------------- /app/[locale]/checkout/checkout-footer.tsx: -------------------------------------------------------------------------------- 1 | import useSettingStore from '@/hooks/use-setting-store' 2 | import Link from 'next/link' 3 | import React from 'react' 4 | 5 | export default function CheckoutFooter() { 6 | const { 7 | setting: { site }, 8 | } = useSettingStore() 9 | return ( 10 |
11 |

12 | Need help? Check our Help Center or{' '} 13 | Contact Us{' '} 14 |

15 |

16 | For an item ordered from {site.name}: When you click the 'Place 17 | Your Order' button, we will send you an e-mail acknowledging 18 | receipt of your order. Your contract to purchase an item will not be 19 | complete until we send you an e-mail notifying you that the item has 20 | been shipped to you. By placing your order, you agree to {site.name} 21 | 's privacy notice and 22 | conditions of use. 23 |

24 |

25 | Within 30 days of delivery, you may return new, unopened merchandise in 26 | its original condition. Exceptions and restrictions apply.{' '} 27 | 28 | See {site.name}'s Returns Policy. 29 | 30 |

31 |
32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /app/[locale]/checkout/layout.tsx: -------------------------------------------------------------------------------- 1 | import { HelpCircle } from 'lucide-react' 2 | import Image from 'next/image' 3 | import Link from 'next/link' 4 | import React from 'react' 5 | 6 | export default function CheckoutLayout({ 7 | children, 8 | }: { 9 | children: React.ReactNode 10 | }) { 11 | return ( 12 |
13 |
14 |
15 | 16 | logo 26 | 27 |
28 |

Checkout

29 |
30 |
31 | 32 | 33 | 34 |
35 |
36 |
37 | {children} 38 |
39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /app/[locale]/checkout/page.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next' 2 | import CheckoutForm from './checkout-form' 3 | import { auth } from '@/auth' 4 | import { redirect } from 'next/navigation' 5 | 6 | export const metadata: Metadata = { 7 | title: 'Checkout', 8 | } 9 | 10 | export default async function CheckoutPage() { 11 | const session = await auth() 12 | if (!session?.user) { 13 | redirect('/sign-in?callbackUrl=/checkout') 14 | } 15 | return 16 | } 17 | -------------------------------------------------------------------------------- /app/[locale]/error.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import React from 'react' 3 | 4 | import { Button } from '@/components/ui/button' 5 | import { useTranslations } from 'next-intl' 6 | 7 | export default function ErrorPage({ 8 | error, 9 | reset, 10 | }: { 11 | error: Error 12 | reset: () => void 13 | }) { 14 | const t = useTranslations() 15 | return ( 16 |
17 |
18 |

{t('Error.Error')}

19 |

{error.message}

20 | 23 | 30 |
31 |
32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /app/[locale]/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basir/nextjs-amazona/c07e0b2738c8fadc6a6c8699b1aa9fe77d9546c3/app/[locale]/favicon.ico -------------------------------------------------------------------------------- /app/[locale]/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Geist, Geist_Mono } from 'next/font/google' 2 | import '../globals.css' 3 | import ClientProviders from '@/components/shared/client-providers' 4 | import { getDirection } from '@/i18n-config' 5 | import { NextIntlClientProvider } from 'next-intl' 6 | import { getMessages } from 'next-intl/server' 7 | import { routing } from '@/i18n/routing' 8 | import { notFound } from 'next/navigation' 9 | import { getSetting } from '@/lib/actions/setting.actions' 10 | import { cookies } from 'next/headers' 11 | 12 | const geistSans = Geist({ 13 | variable: '--font-geist-sans', 14 | subsets: ['latin'], 15 | }) 16 | 17 | const geistMono = Geist_Mono({ 18 | variable: '--font-geist-mono', 19 | subsets: ['latin'], 20 | }) 21 | 22 | export async function generateMetadata() { 23 | const { 24 | site: { slogan, name, description, url }, 25 | } = await getSetting() 26 | return { 27 | title: { 28 | template: `%s | ${name}`, 29 | default: `${name}. ${slogan}`, 30 | }, 31 | description: description, 32 | metadataBase: new URL(url), 33 | } 34 | } 35 | 36 | export default async function AppLayout({ 37 | params, 38 | children, 39 | }: { 40 | params: { locale: string } 41 | children: React.ReactNode 42 | }) { 43 | const setting = await getSetting() 44 | const currencyCookie = (await cookies()).get('currency') 45 | const currency = currencyCookie ? currencyCookie.value : 'USD' 46 | 47 | const { locale } = await params 48 | // Ensure that the incoming `locale` is valid 49 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 50 | if (!routing.locales.includes(locale as any)) { 51 | notFound() 52 | } 53 | const messages = await getMessages() 54 | 55 | return ( 56 | 61 | 64 | 65 | 66 | {children} 67 | 68 | 69 | 70 | 71 | ) 72 | } 73 | -------------------------------------------------------------------------------- /app/[locale]/loading.tsx: -------------------------------------------------------------------------------- 1 | import { getTranslations } from 'next-intl/server' 2 | 3 | export default async function LoadingPage() { 4 | const t = await getTranslations() 5 | return ( 6 |
7 |
8 | {t('Loading.Loading')} 9 |
10 |
11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /app/[locale]/not-found.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import React from 'react' 3 | 4 | import { Button } from '@/components/ui/button' 5 | 6 | export default function NotFound() { 7 | return ( 8 |
9 |
10 |

Not Found

11 |

Could not find requested resource

12 | 19 |
20 |
21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /app/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | import { handlers } from '@/auth' 2 | 3 | export const { GET, POST } = handlers 4 | -------------------------------------------------------------------------------- /app/api/products/browsing-history/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server' 2 | 3 | import Product from '@/lib/db/models/product.model' 4 | import { connectToDatabase } from '@/lib/db' 5 | 6 | export const GET = async (request: NextRequest) => { 7 | const listType = request.nextUrl.searchParams.get('type') || 'history' 8 | const productIdsParam = request.nextUrl.searchParams.get('ids') 9 | const categoriesParam = request.nextUrl.searchParams.get('categories') 10 | 11 | if (!productIdsParam || !categoriesParam) { 12 | return NextResponse.json([]) 13 | } 14 | 15 | const productIds = productIdsParam.split(',') 16 | const categories = categoriesParam.split(',') 17 | const filter = 18 | listType === 'history' 19 | ? { 20 | _id: { $in: productIds }, 21 | } 22 | : { category: { $in: categories }, _id: { $nin: productIds } } 23 | 24 | await connectToDatabase() 25 | const products = await Product.find(filter) 26 | if (listType === 'history') 27 | return NextResponse.json( 28 | products.sort( 29 | (a, b) => 30 | productIds.indexOf(a._id.toString()) - 31 | productIds.indexOf(b._id.toString()) 32 | ) 33 | ) 34 | return NextResponse.json(products) 35 | } 36 | -------------------------------------------------------------------------------- /app/api/uploadthing/core.ts: -------------------------------------------------------------------------------- 1 | import { createUploadthing, type FileRouter } from 'uploadthing/next' 2 | import { UploadThingError } from 'uploadthing/server' 3 | import { auth } from '@/auth' 4 | 5 | const f = createUploadthing() 6 | 7 | // FileRouter for your app, can contain multiple FileRoutes 8 | export const ourFileRouter = { 9 | // Define as many FileRoutes as you like, each with a unique routeSlug 10 | imageUploader: f({ image: { maxFileSize: '4MB' } }) 11 | // Set permissions and file types for this FileRoute 12 | .middleware(async () => { 13 | // This code runs on your server before upload 14 | const session = await auth() 15 | 16 | // If you throw, the user will not be able to upload 17 | if (!session) throw new UploadThingError('Unauthorized') 18 | 19 | // Whatever is returned here is accessible in onUploadComplete as `metadata` 20 | return { userId: session?.user?.id } 21 | }) 22 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 23 | .onUploadComplete(async ({ metadata, file }) => { 24 | // This code RUNS ON YOUR SERVER after upload 25 | 26 | // !!! Whatever is returned here is sent to the clientside `onClientUploadComplete` callback 27 | return { uploadedBy: metadata.userId } 28 | }), 29 | } satisfies FileRouter 30 | 31 | export type OurFileRouter = typeof ourFileRouter 32 | -------------------------------------------------------------------------------- /app/api/uploadthing/route.ts: -------------------------------------------------------------------------------- 1 | import { createRouteHandler } from 'uploadthing/next' 2 | 3 | import { ourFileRouter } from './core' 4 | 5 | // Export routes for Next App Router 6 | export const { GET, POST } = createRouteHandler({ 7 | router: ourFileRouter, 8 | 9 | // Apply an (optional) custom config: 10 | // config: { ... }, 11 | }) 12 | -------------------------------------------------------------------------------- /app/api/webhooks/stripe/route.tsx: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server' 2 | import Stripe from 'stripe' 3 | 4 | import { sendPurchaseReceipt } from '@/emails' 5 | import Order from '@/lib/db/models/order.model' 6 | 7 | const stripe = new Stripe(process.env.STRIPE_SECRET_KEY as string) 8 | 9 | export async function POST(req: NextRequest) { 10 | const event = await stripe.webhooks.constructEvent( 11 | await req.text(), 12 | req.headers.get('stripe-signature') as string, 13 | process.env.STRIPE_WEBHOOK_SECRET as string 14 | ) 15 | 16 | if (event.type === 'charge.succeeded') { 17 | const charge = event.data.object 18 | const orderId = charge.metadata.orderId 19 | const email = charge.billing_details.email 20 | const pricePaidInCents = charge.amount 21 | const order = await Order.findById(orderId).populate('user', 'email') 22 | if (order == null) { 23 | return new NextResponse('Bad Request', { status: 400 }) 24 | } 25 | 26 | order.isPaid = true 27 | order.paidAt = new Date() 28 | order.paymentResult = { 29 | id: event.id, 30 | status: 'COMPLETED', 31 | email_address: email!, 32 | pricePaid: (pricePaidInCents / 100).toFixed(2), 33 | } 34 | await order.save() 35 | try { 36 | await sendPurchaseReceipt({ order }) 37 | } catch (err) { 38 | console.log('email error', err) 39 | } 40 | return NextResponse.json({ 41 | message: 'updateOrderToPaid was successful', 42 | }) 43 | } 44 | return new NextResponse() 45 | } 46 | -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | font-family: Arial, Helvetica, sans-serif; 7 | } 8 | 9 | @layer base { 10 | :root { 11 | --background: 0 0% 100%; 12 | --foreground: 20 14.3% 4.1%; 13 | --card: 0 0% 100%; 14 | --card-foreground: 20 14.3% 4.1%; 15 | --popover: 0 0% 100%; 16 | --popover-foreground: 20 14.3% 4.1%; 17 | --primary: 47.9 95.8% 53.1%; 18 | --primary-foreground: 26 83.3% 14.1%; 19 | --secondary: 60 4.8% 95.9%; 20 | --secondary-foreground: 24 9.8% 10%; 21 | --muted: 60 4.8% 95.9%; 22 | --muted-foreground: 25 5.3% 44.7%; 23 | --accent: 60 4.8% 95.9%; 24 | --accent-foreground: 24 9.8% 10%; 25 | --destructive: 0 84.2% 60.2%; 26 | --destructive-foreground: 60 9.1% 97.8%; 27 | --border: 20 5.9% 90%; 28 | --input: 20 5.9% 90%; 29 | --ring: 20 14.3% 4.1%; 30 | --radius: 0.5rem; 31 | --chart-1: 12 76% 61%; 32 | --chart-2: 173 58% 39%; 33 | --chart-3: 197 37% 24%; 34 | --chart-4: 43 74% 66%; 35 | --chart-5: 27 87% 67%; 36 | } 37 | 38 | .dark { 39 | --background: 20 14.3% 4.1%; 40 | --foreground: 60 9.1% 97.8%; 41 | --card: 20 14.3% 4.1%; 42 | --card-foreground: 60 9.1% 97.8%; 43 | --popover: 20 14.3% 4.1%; 44 | --popover-foreground: 60 9.1% 97.8%; 45 | --primary: 47.9 95.8% 53.1%; 46 | --primary-foreground: 26 83.3% 14.1%; 47 | --secondary: 12 6.5% 15.1%; 48 | --secondary-foreground: 60 9.1% 97.8%; 49 | --muted: 12 6.5% 15.1%; 50 | --muted-foreground: 24 5.4% 63.9%; 51 | --accent: 12 6.5% 15.1%; 52 | --accent-foreground: 60 9.1% 97.8%; 53 | --destructive: 0 62.8% 30.6%; 54 | --destructive-foreground: 60 9.1% 97.8%; 55 | --border: 12 6.5% 15.1%; 56 | --input: 12 6.5% 15.1%; 57 | --ring: 35.5 91.7% 32.9%; 58 | --chart-1: 220 70% 50%; 59 | --chart-2: 160 60% 45%; 60 | --chart-3: 30 80% 55%; 61 | --chart-4: 280 65% 60%; 62 | --chart-5: 340 75% 55%; 63 | } 64 | } 65 | 66 | @layer base { 67 | * { 68 | @apply border-border; 69 | } 70 | body { 71 | @apply bg-background text-foreground; 72 | } 73 | } 74 | 75 | @layer utilities { 76 | .web-page-content p { 77 | @apply py-2; 78 | } 79 | .highlight-link a, 80 | a.highlight-link { 81 | @apply text-sky-700 hover:text-orange-700 hover:underline; 82 | } 83 | .header-button { 84 | @apply cursor-pointer p-1 border border-transparent hover:border-white rounded-[2px]; 85 | } 86 | .item-button { 87 | @apply p-3 hover:bg-muted hover:no-underline; 88 | } 89 | .h1-bold { 90 | @apply font-bold text-2xl lg:text-3xl; 91 | } 92 | .h2-bold { 93 | @apply font-bold text-lg lg:text-xl; 94 | } 95 | .flex-between { 96 | @apply flex justify-between items-center; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /auth.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextAuthConfig } from 'next-auth' 2 | 3 | // Notice this is only an object, not a full Auth.js instance 4 | export default { 5 | providers: [], 6 | callbacks: { 7 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 8 | authorized({ request, auth }: any) { 9 | const protectedPaths = [ 10 | /\/checkout(\/.*)?/, 11 | /\/account(\/.*)?/, 12 | /\/admin(\/.*)?/, 13 | ] 14 | const { pathname } = request.nextUrl 15 | if (protectedPaths.some((p) => p.test(pathname))) return !!auth 16 | return true 17 | }, 18 | }, 19 | } satisfies NextAuthConfig 20 | -------------------------------------------------------------------------------- /auth.ts: -------------------------------------------------------------------------------- 1 | import { MongoDBAdapter } from '@auth/mongodb-adapter' 2 | import Google from 'next-auth/providers/google' 3 | import bcrypt from 'bcryptjs' 4 | import CredentialsProvider from 'next-auth/providers/credentials' 5 | import { connectToDatabase } from './lib/db' 6 | import client from './lib/db/client' 7 | import User from './lib/db/models/user.model' 8 | 9 | import NextAuth, { type DefaultSession } from 'next-auth' 10 | import authConfig from './auth.config' 11 | 12 | declare module 'next-auth' { 13 | interface Session { 14 | user: { 15 | role: string 16 | } & DefaultSession['user'] 17 | } 18 | } 19 | 20 | export const { handlers, auth, signIn, signOut } = NextAuth({ 21 | ...authConfig, 22 | pages: { 23 | signIn: '/sign-in', 24 | newUser: '/sign-up', 25 | error: '/sign-in', 26 | }, 27 | session: { 28 | strategy: 'jwt', 29 | maxAge: 30 * 24 * 60 * 60, 30 | }, 31 | adapter: MongoDBAdapter(client), 32 | providers: [ 33 | Google({ 34 | allowDangerousEmailAccountLinking: true, 35 | }), 36 | CredentialsProvider({ 37 | credentials: { 38 | email: { 39 | type: 'email', 40 | }, 41 | password: { type: 'password' }, 42 | }, 43 | async authorize(credentials) { 44 | await connectToDatabase() 45 | if (credentials == null) return null 46 | 47 | const user = await User.findOne({ email: credentials.email }) 48 | 49 | if (user && user.password) { 50 | const isMatch = await bcrypt.compare( 51 | credentials.password as string, 52 | user.password 53 | ) 54 | if (isMatch) { 55 | return { 56 | id: user._id, 57 | name: user.name, 58 | email: user.email, 59 | role: user.role, 60 | } 61 | } 62 | } 63 | return null 64 | }, 65 | }), 66 | ], 67 | callbacks: { 68 | jwt: async ({ token, user, trigger, session }) => { 69 | if (user) { 70 | if (!user.name) { 71 | await connectToDatabase() 72 | await User.findByIdAndUpdate(user.id, { 73 | name: user.name || user.email!.split('@')[0], 74 | role: 'user', 75 | }) 76 | } 77 | token.name = user.name || user.email!.split('@')[0] 78 | token.role = (user as { role: string }).role 79 | } 80 | 81 | if (session?.user?.name && trigger === 'update') { 82 | token.name = session.user.name 83 | } 84 | return token 85 | }, 86 | session: async ({ session, user, trigger, token }) => { 87 | session.user.id = token.sub as string 88 | session.user.role = token.role as string 89 | session.user.name = token.name 90 | if (trigger === 'update') { 91 | session.user.name = user.name 92 | } 93 | return session 94 | }, 95 | }, 96 | }) 97 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /components/shared/action-button.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { useTransition } from 'react' 3 | 4 | import { Button } from '@/components/ui/button' 5 | import { useToast } from '@/hooks/use-toast' 6 | import { cn } from '@/lib/utils' 7 | 8 | export default function ActionButton({ 9 | caption, 10 | action, 11 | className = 'w-full', 12 | variant = 'default', 13 | size = 'default', 14 | }: { 15 | caption: string 16 | action: () => Promise<{ success: boolean; message: string }> 17 | className?: string 18 | variant?: 'default' | 'outline' | 'destructive' 19 | size?: 'default' | 'sm' | 'lg' 20 | }) { 21 | const [isPending, startTransition] = useTransition() 22 | const { toast } = useToast() 23 | return ( 24 | 42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /components/shared/app-initializer.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react' 2 | import useSettingStore from '@/hooks/use-setting-store' 3 | import { ClientSetting } from '@/types' 4 | 5 | export default function AppInitializer({ 6 | setting, 7 | children, 8 | }: { 9 | setting: ClientSetting 10 | children: React.ReactNode 11 | }) { 12 | const [rendered, setRendered] = useState(false) 13 | 14 | useEffect(() => { 15 | setRendered(true) 16 | }, [setting]) 17 | if (!rendered) { 18 | useSettingStore.setState({ 19 | setting, 20 | }) 21 | } 22 | 23 | return children 24 | } 25 | -------------------------------------------------------------------------------- /components/shared/browsing-history-list.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import useBrowsingHistory from '@/hooks/use-browsing-history' 3 | import React, { useEffect } from 'react' 4 | import ProductSlider from './product/product-slider' 5 | import { useTranslations } from 'next-intl' 6 | import { Separator } from '../ui/separator' 7 | import { cn } from '@/lib/utils' 8 | 9 | export default function BrowsingHistoryList({ 10 | className, 11 | }: { 12 | className?: string 13 | }) { 14 | const { products } = useBrowsingHistory() 15 | const t = useTranslations('Home') 16 | return ( 17 | products.length !== 0 && ( 18 |
19 | 20 | 24 | 25 | 30 |
31 | ) 32 | ) 33 | } 34 | 35 | function ProductList({ 36 | title, 37 | type = 'history', 38 | hideDetails = false, 39 | excludeId = '', 40 | }: { 41 | title: string 42 | type: 'history' | 'related' 43 | excludeId?: string 44 | hideDetails?: boolean 45 | }) { 46 | const { products } = useBrowsingHistory() 47 | const [data, setData] = React.useState([]) 48 | useEffect(() => { 49 | const fetchProducts = async () => { 50 | const res = await fetch( 51 | `/api/products/browsing-history?type=${type}&excludeId=${excludeId}&categories=${products 52 | .map((product) => product.category) 53 | .join(',')}&ids=${products.map((product) => product.id).join(',')}` 54 | ) 55 | const data = await res.json() 56 | setData(data) 57 | } 58 | fetchProducts() 59 | }, [excludeId, products, type]) 60 | 61 | return ( 62 | data.length > 0 && ( 63 | 64 | ) 65 | ) 66 | } 67 | -------------------------------------------------------------------------------- /components/shared/client-providers.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import React from 'react' 3 | import useCartSidebar from '@/hooks/use-cart-sidebar' 4 | import CartSidebar from './cart-sidebar' 5 | import { ThemeProvider } from './theme-provider' 6 | import { Toaster } from '../ui/toaster' 7 | import AppInitializer from './app-initializer' 8 | import { ClientSetting } from '@/types' 9 | 10 | export default function ClientProviders({ 11 | setting, 12 | children, 13 | }: { 14 | setting: ClientSetting 15 | children: React.ReactNode 16 | }) { 17 | const visible = useCartSidebar() 18 | 19 | return ( 20 | 21 | 25 | {visible ? ( 26 |
27 |
{children}
28 | 29 |
30 | ) : ( 31 |
{children}
32 | )} 33 | 34 |
35 |
36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /components/shared/collapsible-on-mobile.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import React, { useEffect, useState } from 'react' 3 | import { useSearchParams } from 'next/navigation' 4 | 5 | import { 6 | Collapsible, 7 | CollapsibleContent, 8 | CollapsibleTrigger, 9 | } from '../ui/collapsible' 10 | import useDeviceType from '@/hooks/use-device-type' 11 | import { Button } from '../ui/button' 12 | 13 | export default function CollapsibleOnMobile({ 14 | title, 15 | children, 16 | }: { 17 | title: string 18 | children: React.ReactNode 19 | }) { 20 | const searchParams = useSearchParams() 21 | 22 | const deviceType = useDeviceType() 23 | const [open, setOpen] = useState(false) 24 | useEffect(() => { 25 | if (deviceType === 'mobile') setOpen(false) 26 | else if (deviceType === 'desktop') setOpen(true) 27 | }, [deviceType, searchParams]) 28 | if (deviceType === 'unknown') return null 29 | return ( 30 | 31 | 32 | {deviceType === 'mobile' && ( 33 | 40 | )} 41 | 42 | {children} 43 | 44 | ) 45 | } 46 | -------------------------------------------------------------------------------- /components/shared/color-provider.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as React from 'react' 4 | import { ThemeProvider as NextThemesProvider, useTheme } from 'next-themes' 5 | import useColorStore from '@/hooks/use-color-store' 6 | export function ColorProvider({ 7 | children, 8 | ...props 9 | }: React.ComponentProps) { 10 | const { theme } = useTheme() 11 | const { color, updateCssVariables } = useColorStore(theme) 12 | React.useEffect(() => { 13 | updateCssVariables() 14 | // eslint-disable-next-line react-hooks/exhaustive-deps 15 | }, [theme, color]) 16 | 17 | return {children} 18 | } 19 | -------------------------------------------------------------------------------- /components/shared/delete-dialog.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { useState, useTransition } from 'react' 3 | 4 | import { 5 | AlertDialog, 6 | AlertDialogCancel, 7 | AlertDialogContent, 8 | AlertDialogDescription, 9 | AlertDialogFooter, 10 | AlertDialogHeader, 11 | AlertDialogTitle, 12 | AlertDialogTrigger, 13 | } from '@/components/ui/alert-dialog' 14 | import { Button } from '@/components/ui/button' 15 | import { useToast } from '@/hooks/use-toast' 16 | 17 | export default function DeleteDialog({ 18 | id, 19 | action, 20 | callbackAction, 21 | }: { 22 | id: string 23 | action: (id: string) => Promise<{ success: boolean; message: string }> 24 | callbackAction?: () => void 25 | }) { 26 | const [open, setOpen] = useState(false) 27 | const [isPending, startTransition] = useTransition() 28 | const { toast } = useToast() 29 | return ( 30 | 31 | 32 | 35 | 36 | 37 | 38 | Are you absolutely sure? 39 | 40 | This action cannot be undone. 41 | 42 | 43 | 44 | Cancel 45 | 46 | 70 | 71 | 72 | 73 | ) 74 | } 75 | -------------------------------------------------------------------------------- /components/shared/header/cart-button.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { ShoppingCartIcon } from 'lucide-react' 4 | import Link from 'next/link' 5 | import useIsMounted from '@/hooks/use-is-mounted' 6 | import useShowSidebar from '@/hooks/use-cart-sidebar' 7 | import { cn } from '@/lib/utils' 8 | import useCartStore from '@/hooks/use-cart-store' 9 | import { useLocale, useTranslations } from 'next-intl' 10 | import { getDirection } from '@/i18n-config' 11 | 12 | export default function CartButton() { 13 | const isMounted = useIsMounted() 14 | const { 15 | cart: { items }, 16 | } = useCartStore() 17 | const cartItemsCount = items.reduce((a, c) => a + c.quantity, 0) 18 | const showSidebar = useShowSidebar() 19 | const t = useTranslations() 20 | 21 | const locale = useLocale() 22 | return ( 23 | 24 |
25 | 26 | 27 | {isMounted && ( 28 | = 10 && 'text-sm px-0 p-[1px]' 34 | )} 35 | > 36 | {cartItemsCount} 37 | 38 | )} 39 | {t('Header.Cart')} 40 | 41 | {showSidebar && ( 42 |
49 | )} 50 |
51 | 52 | ) 53 | } 54 | -------------------------------------------------------------------------------- /components/shared/header/index.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image' 2 | import Link from 'next/link' 3 | import { getAllCategories } from '@/lib/actions/product.actions' 4 | import Menu from './menu' 5 | import Search from './search' 6 | import data from '@/lib/data' 7 | import Sidebar from './sidebar' 8 | import { getSetting } from '@/lib/actions/setting.actions' 9 | import { getTranslations } from 'next-intl/server' 10 | 11 | export default async function Header() { 12 | const categories = await getAllCategories() 13 | const { site } = await getSetting() 14 | const t = await getTranslations() 15 | return ( 16 |
17 |
18 |
19 |
20 | 24 | {`${site.name} 30 | {site.name} 31 | 32 |
33 | 34 |
35 | 36 |
37 | 38 |
39 |
40 | 41 |
42 |
43 |
44 | 45 |
46 | {data.headerMenus.map((menu) => ( 47 | 52 | {t('Header.' + menu.name)} 53 | 54 | ))} 55 |
56 |
57 |
58 | ) 59 | } 60 | -------------------------------------------------------------------------------- /components/shared/header/language-switcher.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as React from 'react' 4 | import { 5 | DropdownMenu, 6 | DropdownMenuContent, 7 | DropdownMenuLabel, 8 | DropdownMenuRadioGroup, 9 | DropdownMenuRadioItem, 10 | DropdownMenuSeparator, 11 | DropdownMenuTrigger, 12 | } from '@/components/ui/dropdown-menu' 13 | 14 | import { useLocale } from 'next-intl' 15 | import { Link, usePathname } from '@/i18n/routing' 16 | import useSettingStore from '@/hooks/use-setting-store' 17 | import { i18n } from '@/i18n-config' 18 | import { setCurrencyOnServer } from '@/lib/actions/setting.actions' 19 | import { ChevronDownIcon } from 'lucide-react' 20 | 21 | export default function LanguageSwitcher() { 22 | const { locales } = i18n 23 | const locale = useLocale() 24 | const pathname = usePathname() 25 | 26 | const { 27 | setting: { availableCurrencies, currency }, 28 | setCurrency, 29 | } = useSettingStore() 30 | const handleCurrencyChange = async (newCurrency: string) => { 31 | await setCurrencyOnServer(newCurrency) 32 | setCurrency(newCurrency) 33 | } 34 | return ( 35 | 36 | 37 |
38 | 39 | {locales.find((l) => l.code === locale)?.icon} 40 | 41 | {locale.toUpperCase().slice(0, 2)} 42 | 43 |
44 |
45 | 46 | Language 47 | 48 | {locales.map((c) => ( 49 | 50 | 55 | {c.icon} {c.name} 56 | 57 | 58 | ))} 59 | 60 | 61 | 62 | 63 | Currency 64 | 68 | {availableCurrencies.map((c) => ( 69 | 70 | {c.symbol} {c.code} 71 | 72 | ))} 73 | 74 | 75 |
76 | ) 77 | } 78 | -------------------------------------------------------------------------------- /components/shared/header/menu.tsx: -------------------------------------------------------------------------------- 1 | import { EllipsisVertical } from 'lucide-react' 2 | import { 3 | Sheet, 4 | SheetContent, 5 | SheetDescription, 6 | SheetHeader, 7 | SheetTitle, 8 | SheetTrigger, 9 | } from '@/components/ui/sheet' 10 | import CartButton from './cart-button' 11 | import UserButton from './user-button' 12 | import ThemeSwitcher from './theme-switcher' 13 | import LanguageSwitcher from './language-switcher' 14 | import { useTranslations } from 'next-intl' 15 | 16 | const Menu = ({ forAdmin = false }: { forAdmin?: boolean }) => { 17 | const t = useTranslations() 18 | return ( 19 |
20 | 26 | 45 |
46 | ) 47 | } 48 | 49 | export default Menu 50 | -------------------------------------------------------------------------------- /components/shared/header/search.tsx: -------------------------------------------------------------------------------- 1 | import { SearchIcon } from 'lucide-react' 2 | 3 | import { Input } from '@/components/ui/input' 4 | import { getAllCategories } from '@/lib/actions/product.actions' 5 | 6 | import { 7 | Select, 8 | SelectContent, 9 | SelectItem, 10 | SelectTrigger, 11 | SelectValue, 12 | } from '../../ui/select' 13 | import { getSetting } from '@/lib/actions/setting.actions' 14 | import { getTranslations } from 'next-intl/server' 15 | 16 | export default async function Search() { 17 | const { 18 | site: { name }, 19 | } = await getSetting() 20 | const categories = await getAllCategories() 21 | 22 | const t = await getTranslations() 23 | return ( 24 |
25 | 38 | 44 | 50 |
51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /components/shared/header/theme-switcher.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { ChevronDownIcon, Moon, Sun } from 'lucide-react' 4 | import { useTheme } from 'next-themes' 5 | import * as React from 'react' 6 | import { 7 | DropdownMenu, 8 | DropdownMenuContent, 9 | DropdownMenuLabel, 10 | DropdownMenuRadioGroup, 11 | DropdownMenuRadioItem, 12 | DropdownMenuSeparator, 13 | DropdownMenuTrigger, 14 | } from '@/components/ui/dropdown-menu' 15 | 16 | import useColorStore from '@/hooks/use-color-store' 17 | import useIsMounted from '@/hooks/use-is-mounted' 18 | import { useTranslations } from 'next-intl' 19 | 20 | export default function ThemeSwitcher() { 21 | const { theme, setTheme } = useTheme() 22 | const { availableColors, color, setColor } = useColorStore(theme) 23 | const t = useTranslations('Header') 24 | const changeTheme = (value: string) => { 25 | setTheme(value) 26 | } 27 | const isMounted = useIsMounted() 28 | return ( 29 | 30 | 31 | {theme === 'dark' && isMounted ? ( 32 |
33 | {t('Dark')} 34 |
35 | ) : ( 36 |
37 | {t('Light')} 38 |
39 | )} 40 |
41 | 42 | Theme 43 | 44 | 45 | 46 | {t('Dark')} 47 | 48 | 49 | {t('Light')} 50 | 51 | 52 | 53 | {t('Color')} 54 | 55 | setColor(value, true)} 58 | > 59 | {availableColors.map((c) => ( 60 | 61 |
65 | 66 | {t(c.name)} 67 |
68 | ))} 69 |
70 |
71 |
72 | ) 73 | } 74 | -------------------------------------------------------------------------------- /components/shared/home/home-card.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image' 2 | import Link from 'next/link' 3 | import React from 'react' 4 | import { Card, CardContent, CardFooter } from '@/components/ui/card' 5 | 6 | type CardItem = { 7 | title: string 8 | link: { text: string; href: string } 9 | items: { 10 | name: string 11 | items?: string[] 12 | image: string 13 | href: string 14 | }[] 15 | } 16 | 17 | export function HomeCard({ cards }: { cards: CardItem[] }) { 18 | return ( 19 |
20 | {cards.map((card) => ( 21 | 22 | 23 |

{card.title}

24 |
25 | {card.items.map((item) => ( 26 | 31 | {item.name} 38 |

39 | {item.name} 40 |

41 | 42 | ))} 43 |
44 |
45 | {card.link && ( 46 | 47 | 48 | {card.link.text} 49 | 50 | 51 | )} 52 |
53 | ))} 54 |
55 | ) 56 | } 57 | -------------------------------------------------------------------------------- /components/shared/home/home-carousel.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as React from 'react' 4 | import Image from 'next/image' 5 | import Autoplay from 'embla-carousel-autoplay' 6 | import { 7 | Carousel, 8 | CarouselContent, 9 | CarouselItem, 10 | CarouselNext, 11 | CarouselPrevious, 12 | } from '@/components/ui/carousel' 13 | import Link from 'next/link' 14 | import { cn } from '@/lib/utils' 15 | import { Button } from '@/components/ui/button' 16 | import { useTranslations } from 'next-intl' 17 | import { ICarousel } from '@/types' 18 | 19 | export function HomeCarousel({ items }: { items: ICarousel[] }) { 20 | const plugin = React.useRef( 21 | Autoplay({ delay: 3000, stopOnInteraction: true }) 22 | ) 23 | 24 | const t = useTranslations('Home') 25 | 26 | return ( 27 | 34 | 35 | {items.map((item) => ( 36 | 37 | 38 |
39 | {item.title} 46 |
47 |

52 | {t(`${item.title}`)} 53 |

54 | 57 |
58 |
59 | 60 |
61 | ))} 62 |
63 | 64 | 65 |
66 | ) 67 | } 68 | -------------------------------------------------------------------------------- /components/shared/pagination.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useRouter, useSearchParams } from 'next/navigation' 4 | import React from 'react' 5 | 6 | import { formUrlQuery } from '@/lib/utils' 7 | 8 | import { Button } from '../ui/button' 9 | import { ChevronLeft, ChevronRight } from 'lucide-react' 10 | import { useTranslations } from 'next-intl' 11 | 12 | type PaginationProps = { 13 | page: number | string 14 | totalPages: number 15 | urlParamName?: string 16 | } 17 | 18 | const Pagination = ({ page, totalPages, urlParamName }: PaginationProps) => { 19 | const router = useRouter() 20 | const searchParams = useSearchParams() 21 | 22 | const onClick = (btnType: string) => { 23 | const pageValue = btnType === 'next' ? Number(page) + 1 : Number(page) - 1 24 | 25 | const newUrl = formUrlQuery({ 26 | params: searchParams.toString(), 27 | key: urlParamName || 'page', 28 | value: pageValue.toString(), 29 | }) 30 | 31 | router.push(newUrl, { scroll: true }) 32 | } 33 | 34 | const t = useTranslations() 35 | return ( 36 |
37 | 46 | {t('Search.Page')} {page} {t('Search.of')} {totalPages} 47 | 56 |
57 | ) 58 | } 59 | 60 | export default Pagination 61 | -------------------------------------------------------------------------------- /components/shared/product/add-to-browsing-history.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import useBrowsingHistory from '@/hooks/use-browsing-history' 3 | import { useEffect } from 'react' 4 | 5 | export default function AddToBrowsingHistory({ 6 | id, 7 | category, 8 | }: { 9 | id: string 10 | category: string 11 | }) { 12 | const { addItem } = useBrowsingHistory() 13 | useEffect(() => { 14 | addItem({ id, category }) 15 | // eslint-disable-next-line react-hooks/exhaustive-deps 16 | }, []) 17 | return null 18 | } 19 | -------------------------------------------------------------------------------- /components/shared/product/add-to-cart.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | 'use client' 3 | 4 | import { Button } from '@/components/ui/button' 5 | import { 6 | Select, 7 | SelectContent, 8 | SelectItem, 9 | SelectTrigger, 10 | SelectValue, 11 | } from '@/components/ui/select' 12 | import useCartStore from '@/hooks/use-cart-store' 13 | import { useToast } from '@/hooks/use-toast' 14 | import { OrderItem } from '@/types' 15 | import { useTranslations } from 'next-intl' 16 | import { useRouter } from 'next/navigation' 17 | import { useState } from 'react' 18 | 19 | export default function AddToCart({ 20 | item, 21 | minimal = false, 22 | }: { 23 | item: OrderItem 24 | minimal?: boolean 25 | }) { 26 | const router = useRouter() 27 | const { toast } = useToast() 28 | 29 | const { addItem } = useCartStore() 30 | 31 | //PROMPT: add quantity state 32 | const [quantity, setQuantity] = useState(1) 33 | 34 | const t = useTranslations() 35 | 36 | return minimal ? ( 37 | 52 | ), 53 | }) 54 | } catch (error: any) { 55 | toast({ 56 | variant: 'destructive', 57 | description: error.message, 58 | }) 59 | } 60 | }} 61 | > 62 | {t('Product.Add to Cart')} 63 | 64 | ) : ( 65 |
66 | 83 | 84 | 101 | 118 |
119 | ) 120 | } 121 | -------------------------------------------------------------------------------- /components/shared/product/image-hover.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | 'use client' 3 | import Image from 'next/image' 4 | import { useState } from 'react' 5 | 6 | const ImageHover = ({ 7 | src, 8 | hoverSrc, 9 | alt, 10 | }: { 11 | src: string 12 | hoverSrc: string 13 | alt: string 14 | }) => { 15 | const [isHovered, setIsHovered] = useState(false) 16 | let hoverTimeout: any 17 | const handleMouseEnter = () => { 18 | hoverTimeout = setTimeout(() => setIsHovered(true), 1000) // 1 second delay 19 | } 20 | 21 | const handleMouseLeave = () => { 22 | clearTimeout(hoverTimeout) 23 | setIsHovered(false) 24 | } 25 | 26 | return ( 27 |
32 | {alt} 41 | {alt} 50 |
51 | ) 52 | } 53 | 54 | export default ImageHover 55 | -------------------------------------------------------------------------------- /components/shared/product/product-gallery.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useState } from 'react' 4 | import Image from 'next/image' 5 | import Zoom from 'react-medium-image-zoom' 6 | import 'react-medium-image-zoom/dist/styles.css' 7 | export default function ProductGallery({ images }: { images: string[] }) { 8 | const [selectedImage, setSelectedImage] = useState(0) 9 | return ( 10 |
11 |
12 | {images.map((image, index) => ( 13 | 29 | ))} 30 |
31 | 32 |
33 | 34 |
35 | {'product 43 |
44 |
45 |
46 |
47 | ) 48 | } 49 | -------------------------------------------------------------------------------- /components/shared/product/product-slider.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as React from 'react' 4 | import { 5 | Carousel, 6 | CarouselContent, 7 | CarouselItem, 8 | CarouselNext, 9 | CarouselPrevious, 10 | } from '@/components/ui/carousel' 11 | import ProductCard from './product-card' 12 | import { IProduct } from '@/lib/db/models/product.model' 13 | 14 | export default function ProductSlider({ 15 | title, 16 | products, 17 | hideDetails = false, 18 | }: { 19 | title?: string 20 | products: IProduct[] 21 | hideDetails?: boolean 22 | }) { 23 | return ( 24 |
25 |

{title}

26 | 32 | 33 | {products.map((product) => ( 34 | 42 | 48 | 49 | ))} 50 | 51 | 52 | 53 | 54 |
55 | ) 56 | } 57 | -------------------------------------------------------------------------------- /components/shared/product/product-sort-selector.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { 3 | Select, 4 | SelectContent, 5 | SelectItem, 6 | SelectTrigger, 7 | SelectValue, 8 | } from '@/components/ui/select' 9 | import { getFilterUrl } from '@/lib/utils' 10 | import { useRouter } from 'next/navigation' 11 | import React from 'react' 12 | 13 | export default function ProductSortSelector({ 14 | sortOrders, 15 | sort, 16 | params, 17 | }: { 18 | sortOrders: { value: string; name: string }[] 19 | sort: string 20 | params: { 21 | q?: string 22 | category?: string 23 | price?: string 24 | rating?: string 25 | sort?: string 26 | page?: string 27 | } 28 | }) { 29 | const router = useRouter() 30 | return ( 31 | 51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /components/shared/product/rating-summary.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { Progress } from '@/components/ui/progress' 4 | import Rating from './rating' 5 | import { Separator } from '@/components/ui/separator' 6 | import Link from 'next/link' 7 | import { 8 | Popover, 9 | PopoverContent, 10 | PopoverTrigger, 11 | } from '@/components/ui/popover' 12 | import { Button } from '@/components/ui/button' 13 | import { useTranslations } from 'next-intl' 14 | import { ChevronDownIcon } from 'lucide-react' 15 | 16 | type RatingSummaryProps = { 17 | asPopover?: boolean 18 | avgRating: number 19 | numReviews: number 20 | ratingDistribution: { 21 | rating: number 22 | count: number 23 | }[] 24 | } 25 | 26 | export default function RatingSummary({ 27 | asPopover, 28 | avgRating = 0, 29 | numReviews = 0, 30 | ratingDistribution = [], 31 | }: RatingSummaryProps) { 32 | const t = useTranslations() 33 | const RatingDistribution = () => { 34 | const ratingPercentageDistribution = ratingDistribution.map((x) => ({ 35 | ...x, 36 | percentage: Math.round((x.count / numReviews) * 100), 37 | })) 38 | 39 | return ( 40 | <> 41 |
42 | 43 | 44 | {t('Product.avgRating out of 5', { 45 | avgRating: avgRating.toFixed(1), 46 | })} 47 | 48 |
49 |
50 | {t('Product.numReviews ratings', { numReviews })} 51 |
52 | 53 |
54 | {ratingPercentageDistribution 55 | .sort((a, b) => b.rating - a.rating) 56 | .map(({ rating, percentage }) => ( 57 |
61 |
62 | {' '} 63 | {t('Product.rating star', { rating })} 64 |
65 | 66 |
{percentage}%
67 |
68 | ))} 69 |
70 | 71 | ) 72 | } 73 | 74 | return asPopover ? ( 75 |
76 | 77 | 78 | 83 | 84 | 85 |
86 | 87 | 88 | 89 | 90 | {t('Product.See customer reviews')} 91 | 92 |
93 |
94 |
95 |
96 | 97 | {t('Product.numReviews ratings', { numReviews })} 98 | 99 |
100 |
101 | ) : ( 102 | 103 | ) 104 | } 105 | -------------------------------------------------------------------------------- /components/shared/product/rating.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Star } from 'lucide-react' 3 | 4 | export default function Rating({ 5 | rating = 0, 6 | size = 6, 7 | }: { 8 | rating: number 9 | size?: number 10 | }) { 11 | const fullStars = Math.floor(rating) 12 | const partialStar = rating % 1 13 | const emptyStars = 5 - Math.ceil(rating) 14 | 15 | return ( 16 |
20 | {[...Array(fullStars)].map((_, i) => ( 21 | 25 | ))} 26 | {partialStar > 0 && ( 27 |
28 | 29 |
33 | 34 |
35 |
36 | )} 37 | {[...Array(emptyStars)].map((_, i) => ( 38 | 42 | ))} 43 |
44 | ) 45 | } 46 | -------------------------------------------------------------------------------- /components/shared/product/select-variant.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from '@/components/ui/button' 2 | import { IProduct } from '@/lib/db/models/product.model' 3 | import Link from 'next/link' 4 | 5 | export default function SelectVariant({ 6 | product, 7 | size, 8 | color, 9 | }: { 10 | product: IProduct 11 | color: string 12 | size: string 13 | }) { 14 | const selectedColor = color || product.colors[0] 15 | const selectedSize = size || product.sizes[0] 16 | 17 | return ( 18 | <> 19 | {product.colors.length > 0 && ( 20 |
21 |
Color:
22 | {product.colors.map((x: string) => ( 23 | 47 | ))} 48 |
49 | )} 50 | {product.sizes.length > 0 && ( 51 |
52 |
Size:
53 | {product.sizes.map((x: string) => ( 54 | 73 | ))} 74 |
75 | )} 76 | 77 | ) 78 | } 79 | -------------------------------------------------------------------------------- /components/shared/separator-or.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react' 2 | 3 | const SeparatorWithOr = ({ children }: { children?: ReactNode }) => { 4 | return ( 5 |
6 | 7 | {children ?? 'or'} 8 | 9 |
10 | ) 11 | } 12 | 13 | export default SeparatorWithOr 14 | -------------------------------------------------------------------------------- /components/shared/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as React from 'react' 4 | import { ThemeProvider as NextThemesProvider } from 'next-themes' 5 | import { ColorProvider } from './color-provider' 6 | export function ThemeProvider({ 7 | children, 8 | ...props 9 | }: React.ComponentProps) { 10 | return ( 11 | 12 | {children} 13 | 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const badgeVariants = cva( 7 | "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", 13 | secondary: 14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 15 | destructive: 16 | "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", 17 | outline: "text-foreground", 18 | }, 19 | }, 20 | defaultVariants: { 21 | variant: "default", 22 | }, 23 | } 24 | ) 25 | 26 | export interface BadgeProps 27 | extends React.HTMLAttributes, 28 | VariantProps {} 29 | 30 | function Badge({ className, variant, ...props }: BadgeProps) { 31 | return ( 32 |
33 | ) 34 | } 35 | 36 | export { Badge, badgeVariants } 37 | -------------------------------------------------------------------------------- /components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", 16 | outline: 17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", 20 | ghost: "hover:bg-accent hover:text-accent-foreground", 21 | link: "text-primary underline-offset-4 hover:underline", 22 | }, 23 | size: { 24 | default: "h-9 px-4 py-2", 25 | sm: "h-8 rounded-md px-3 text-xs", 26 | lg: "h-10 rounded-md px-8", 27 | icon: "h-9 w-9", 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: "default", 32 | size: "default", 33 | }, 34 | } 35 | ) 36 | 37 | export interface ButtonProps 38 | extends React.ButtonHTMLAttributes, 39 | VariantProps { 40 | asChild?: boolean 41 | } 42 | 43 | const Button = React.forwardRef( 44 | ({ className, variant, size, asChild = false, ...props }, ref) => { 45 | const Comp = asChild ? Slot : "button" 46 | return ( 47 | 52 | ) 53 | } 54 | ) 55 | Button.displayName = "Button" 56 | 57 | export { Button, buttonVariants } 58 | -------------------------------------------------------------------------------- /components/ui/calendar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { ChevronLeft, ChevronRight } from "lucide-react" 5 | import { DayPicker } from "react-day-picker" 6 | 7 | import { cn } from "@/lib/utils" 8 | import { buttonVariants } from "@/components/ui/button" 9 | 10 | export type CalendarProps = React.ComponentProps 11 | 12 | function Calendar({ 13 | className, 14 | classNames, 15 | showOutsideDays = true, 16 | ...props 17 | }: CalendarProps) { 18 | return ( 19 | .day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md" 43 | : "[&:has([aria-selected])]:rounded-md" 44 | ), 45 | day: cn( 46 | buttonVariants({ variant: "ghost" }), 47 | "h-8 w-8 p-0 font-normal aria-selected:opacity-100" 48 | ), 49 | day_range_start: "day-range-start", 50 | day_range_end: "day-range-end", 51 | day_selected: 52 | "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground", 53 | day_today: "bg-accent text-accent-foreground", 54 | day_outside: 55 | "day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground", 56 | day_disabled: "text-muted-foreground opacity-50", 57 | day_range_middle: 58 | "aria-selected:bg-accent aria-selected:text-accent-foreground", 59 | day_hidden: "invisible", 60 | ...classNames, 61 | }} 62 | components={{ 63 | IconLeft: ({ className, ...props }) => ( 64 | 65 | ), 66 | IconRight: ({ className, ...props }) => ( 67 | 68 | ), 69 | }} 70 | {...props} 71 | /> 72 | ) 73 | } 74 | Calendar.displayName = "Calendar" 75 | 76 | export { Calendar } 77 | -------------------------------------------------------------------------------- /components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )) 18 | Card.displayName = "Card" 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )) 30 | CardHeader.displayName = "CardHeader" 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLDivElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |
41 | )) 42 | CardTitle.displayName = "CardTitle" 43 | 44 | const CardDescription = React.forwardRef< 45 | HTMLDivElement, 46 | React.HTMLAttributes 47 | >(({ className, ...props }, ref) => ( 48 |
53 | )) 54 | CardDescription.displayName = "CardDescription" 55 | 56 | const CardContent = React.forwardRef< 57 | HTMLDivElement, 58 | React.HTMLAttributes 59 | >(({ className, ...props }, ref) => ( 60 |
61 | )) 62 | CardContent.displayName = "CardContent" 63 | 64 | const CardFooter = React.forwardRef< 65 | HTMLDivElement, 66 | React.HTMLAttributes 67 | >(({ className, ...props }, ref) => ( 68 |
73 | )) 74 | CardFooter.displayName = "CardFooter" 75 | 76 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 77 | -------------------------------------------------------------------------------- /components/ui/checkbox.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox" 5 | import { Check } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const Checkbox = React.forwardRef< 10 | React.ElementRef, 11 | React.ComponentPropsWithoutRef 12 | >(({ className, ...props }, ref) => ( 13 | 21 | 24 | 25 | 26 | 27 | )) 28 | Checkbox.displayName = CheckboxPrimitive.Root.displayName 29 | 30 | export { Checkbox } 31 | -------------------------------------------------------------------------------- /components/ui/collapsible.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as CollapsiblePrimitive from "@radix-ui/react-collapsible" 4 | 5 | const Collapsible = CollapsiblePrimitive.Root 6 | 7 | const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger 8 | 9 | const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent 10 | 11 | export { Collapsible, CollapsibleTrigger, CollapsibleContent } 12 | -------------------------------------------------------------------------------- /components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Input = React.forwardRef>( 6 | ({ className, type, ...props }, ref) => { 7 | return ( 8 | 17 | ) 18 | } 19 | ) 20 | Input.displayName = "Input" 21 | 22 | export { Input } 23 | -------------------------------------------------------------------------------- /components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | import { cva, type VariantProps } from "class-variance-authority" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const labelVariants = cva( 10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 11 | ) 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef & 16 | VariantProps 17 | >(({ className, ...props }, ref) => ( 18 | 23 | )) 24 | Label.displayName = LabelPrimitive.Root.displayName 25 | 26 | export { Label } 27 | -------------------------------------------------------------------------------- /components/ui/popover.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as PopoverPrimitive from "@radix-ui/react-popover" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Popover = PopoverPrimitive.Root 9 | 10 | const PopoverTrigger = PopoverPrimitive.Trigger 11 | 12 | const PopoverAnchor = PopoverPrimitive.Anchor 13 | 14 | const PopoverContent = React.forwardRef< 15 | React.ElementRef, 16 | React.ComponentPropsWithoutRef 17 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( 18 | 19 | 29 | 30 | )) 31 | PopoverContent.displayName = PopoverPrimitive.Content.displayName 32 | 33 | export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor } 34 | -------------------------------------------------------------------------------- /components/ui/progress.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as ProgressPrimitive from "@radix-ui/react-progress" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Progress = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, value, ...props }, ref) => ( 12 | 20 | 24 | 25 | )) 26 | Progress.displayName = ProgressPrimitive.Root.displayName 27 | 28 | export { Progress } 29 | -------------------------------------------------------------------------------- /components/ui/radio-group.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as RadioGroupPrimitive from "@radix-ui/react-radio-group" 5 | import { Circle } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const RadioGroup = React.forwardRef< 10 | React.ElementRef, 11 | React.ComponentPropsWithoutRef 12 | >(({ className, ...props }, ref) => { 13 | return ( 14 | 19 | ) 20 | }) 21 | RadioGroup.displayName = RadioGroupPrimitive.Root.displayName 22 | 23 | const RadioGroupItem = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, ...props }, ref) => { 27 | return ( 28 | 36 | 37 | 38 | 39 | 40 | ) 41 | }) 42 | RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName 43 | 44 | export { RadioGroup, RadioGroupItem } 45 | -------------------------------------------------------------------------------- /components/ui/scroll-area.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const ScrollArea = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, children, ...props }, ref) => ( 12 | 17 | 18 | {children} 19 | 20 | 21 | 22 | 23 | )) 24 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName 25 | 26 | const ScrollBar = React.forwardRef< 27 | React.ElementRef, 28 | React.ComponentPropsWithoutRef 29 | >(({ className, orientation = "vertical", ...props }, ref) => ( 30 | 43 | 44 | 45 | )) 46 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName 47 | 48 | export { ScrollArea, ScrollBar } 49 | -------------------------------------------------------------------------------- /components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SeparatorPrimitive from "@radix-ui/react-separator" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Separator = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >( 12 | ( 13 | { className, orientation = "horizontal", decorative = true, ...props }, 14 | ref 15 | ) => ( 16 | 27 | ) 28 | ) 29 | Separator.displayName = SeparatorPrimitive.Root.displayName 30 | 31 | export { Separator } 32 | -------------------------------------------------------------------------------- /components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes) { 7 | return ( 8 |
12 | ) 13 | } 14 | 15 | export { Skeleton } 16 | -------------------------------------------------------------------------------- /components/ui/table.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Table = React.forwardRef< 6 | HTMLTableElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
10 | 15 | 16 | )) 17 | Table.displayName = "Table" 18 | 19 | const TableHeader = React.forwardRef< 20 | HTMLTableSectionElement, 21 | React.HTMLAttributes 22 | >(({ className, ...props }, ref) => ( 23 | 24 | )) 25 | TableHeader.displayName = "TableHeader" 26 | 27 | const TableBody = React.forwardRef< 28 | HTMLTableSectionElement, 29 | React.HTMLAttributes 30 | >(({ className, ...props }, ref) => ( 31 | 36 | )) 37 | TableBody.displayName = "TableBody" 38 | 39 | const TableFooter = React.forwardRef< 40 | HTMLTableSectionElement, 41 | React.HTMLAttributes 42 | >(({ className, ...props }, ref) => ( 43 | tr]:last:border-b-0", 47 | className 48 | )} 49 | {...props} 50 | /> 51 | )) 52 | TableFooter.displayName = "TableFooter" 53 | 54 | const TableRow = React.forwardRef< 55 | HTMLTableRowElement, 56 | React.HTMLAttributes 57 | >(({ className, ...props }, ref) => ( 58 | 66 | )) 67 | TableRow.displayName = "TableRow" 68 | 69 | const TableHead = React.forwardRef< 70 | HTMLTableCellElement, 71 | React.ThHTMLAttributes 72 | >(({ className, ...props }, ref) => ( 73 |
[role=checkbox]]:translate-y-[2px]", 77 | className 78 | )} 79 | {...props} 80 | /> 81 | )) 82 | TableHead.displayName = "TableHead" 83 | 84 | const TableCell = React.forwardRef< 85 | HTMLTableCellElement, 86 | React.TdHTMLAttributes 87 | >(({ className, ...props }, ref) => ( 88 | [role=checkbox]]:translate-y-[2px]", 92 | className 93 | )} 94 | {...props} 95 | /> 96 | )) 97 | TableCell.displayName = "TableCell" 98 | 99 | const TableCaption = React.forwardRef< 100 | HTMLTableCaptionElement, 101 | React.HTMLAttributes 102 | >(({ className, ...props }, ref) => ( 103 |
108 | )) 109 | TableCaption.displayName = "TableCaption" 110 | 111 | export { 112 | Table, 113 | TableHeader, 114 | TableBody, 115 | TableFooter, 116 | TableHead, 117 | TableRow, 118 | TableCell, 119 | TableCaption, 120 | } 121 | -------------------------------------------------------------------------------- /components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Textarea = React.forwardRef< 6 | HTMLTextAreaElement, 7 | React.ComponentProps<"textarea"> 8 | >(({ className, ...props }, ref) => { 9 | return ( 10 |