├── public ├── favicon.ico ├── screenshot.png └── img │ ├── logo-black.png │ ├── logo-white.png │ ├── advertisement-one.webp │ ├── hero-carousel-five.webp │ ├── hero-carousel-four.webp │ ├── hero-carousel-one.webp │ ├── hero-carousel-six.webp │ ├── hero-carousel-three.webp │ └── hero-carousel-two.webp ├── postcss.config.cjs ├── prettier.config.cjs ├── src ├── styles │ ├── globals.css │ ├── customtable.module.css │ └── searchbar.module.css ├── server │ ├── stripe │ │ ├── client.ts │ │ └── stripe-webhook-handlers.ts │ ├── cloudinary │ │ └── cloudinary.ts │ ├── trpc │ │ ├── router │ │ │ ├── admin │ │ │ │ ├── index.ts │ │ │ │ ├── orders.ts │ │ │ │ ├── users.ts │ │ │ │ └── products.ts │ │ │ ├── _app.ts │ │ │ ├── products.ts │ │ │ ├── users.ts │ │ │ ├── stripe.ts │ │ │ └── orders.ts │ │ ├── context.ts │ │ └── trpc.ts │ ├── db │ │ └── client.ts │ └── common │ │ └── get-server-auth-session.ts ├── components │ ├── layouts │ │ ├── CompactLayout.tsx │ │ ├── Footer.tsx │ │ ├── DefaultLayout.tsx │ │ ├── Meta.tsx │ │ └── Navbar.tsx │ ├── screens │ │ ├── LoadingScreen.tsx │ │ ├── DeactivatedScreen.tsx │ │ ├── RestrictedScreen.tsx │ │ └── ErrorScreen.tsx │ ├── ui │ │ ├── ToastWrapper.tsx │ │ ├── Button.tsx │ │ └── FileInput.tsx │ ├── CategoryList.tsx │ ├── Searchbar.tsx │ ├── Hero.tsx │ ├── ConfirmationModal.tsx │ ├── ProductList.tsx │ └── Cart.tsx ├── types │ ├── globals.ts │ └── next-auth.d.ts ├── pages │ ├── _document.tsx │ ├── api │ │ ├── trpc │ │ │ └── [trpc].ts │ │ ├── restricted.ts │ │ ├── auth │ │ │ └── [...nextauth].ts │ │ ├── og.tsx │ │ └── stripe-webhook.ts │ ├── app │ │ ├── checkout │ │ │ └── index.tsx │ │ ├── products │ │ │ ├── index.tsx │ │ │ └── [productId].tsx │ │ ├── categories │ │ │ ├── index.tsx │ │ │ └── [category].tsx │ │ ├── index.tsx │ │ ├── orders │ │ │ ├── [orderId].tsx │ │ │ └── index.tsx │ │ └── account │ │ │ ├── index.tsx │ │ │ ├── prime.tsx │ │ │ └── update.tsx │ ├── 404.tsx │ ├── _app.tsx │ ├── index.tsx │ └── dashboard │ │ ├── index.tsx │ │ ├── orders │ │ ├── index.tsx │ │ └── [orderId].tsx │ │ ├── users │ │ ├── index.tsx │ │ └── [userId].tsx │ │ └── products │ │ ├── index.tsx │ │ └── add.tsx ├── utils │ ├── format.ts │ ├── render.tsx │ └── trpc.ts ├── env │ ├── server.mjs │ ├── client.mjs │ └── schema.mjs └── stores │ └── cart.ts ├── .eslintrc.json ├── next.config.mjs ├── tsconfig.json ├── .gitignore ├── .env.example ├── tailwind.config.cjs ├── README.md ├── package.json └── prisma └── schema.prisma /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sadmann7/amzn-web/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sadmann7/amzn-web/HEAD/public/screenshot.png -------------------------------------------------------------------------------- /public/img/logo-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sadmann7/amzn-web/HEAD/public/img/logo-black.png -------------------------------------------------------------------------------- /public/img/logo-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sadmann7/amzn-web/HEAD/public/img/logo-white.png -------------------------------------------------------------------------------- /public/img/advertisement-one.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sadmann7/amzn-web/HEAD/public/img/advertisement-one.webp -------------------------------------------------------------------------------- /public/img/hero-carousel-five.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sadmann7/amzn-web/HEAD/public/img/hero-carousel-five.webp -------------------------------------------------------------------------------- /public/img/hero-carousel-four.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sadmann7/amzn-web/HEAD/public/img/hero-carousel-four.webp -------------------------------------------------------------------------------- /public/img/hero-carousel-one.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sadmann7/amzn-web/HEAD/public/img/hero-carousel-one.webp -------------------------------------------------------------------------------- /public/img/hero-carousel-six.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sadmann7/amzn-web/HEAD/public/img/hero-carousel-six.webp -------------------------------------------------------------------------------- /public/img/hero-carousel-three.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sadmann7/amzn-web/HEAD/public/img/hero-carousel-three.webp -------------------------------------------------------------------------------- /public/img/hero-carousel-two.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sadmann7/amzn-web/HEAD/public/img/hero-carousel-two.webp -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /prettier.config.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import("prettier").Config} */ 2 | module.exports = { 3 | plugins: [require.resolve("prettier-plugin-tailwindcss")], 4 | }; 5 | -------------------------------------------------------------------------------- /src/styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | html { 6 | scroll-behavior: smooth; 7 | } 8 | 9 | body { 10 | @apply bg-bg-white; 11 | } 12 | -------------------------------------------------------------------------------- /src/server/stripe/client.ts: -------------------------------------------------------------------------------- 1 | import Stripe from "stripe"; 2 | import { env } from "../../env/server.mjs"; 3 | 4 | export const stripe = new Stripe(env.STRIPE_SECRET_KEY, { 5 | apiVersion: "2022-11-15", 6 | }); 7 | -------------------------------------------------------------------------------- /src/components/layouts/CompactLayout.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from "react"; 2 | 3 | const CompactLayout = ({ children }: { children: ReactNode }) => { 4 | return <>{children}; 5 | }; 6 | 7 | export default CompactLayout; 8 | -------------------------------------------------------------------------------- /src/types/globals.ts: -------------------------------------------------------------------------------- 1 | import type { Order, OrderItem, Product } from "@prisma/client"; 2 | 3 | export type OrderItemWithProduct = OrderItem & { product: Product }; 4 | 5 | export type OrderWithItems = Order & { items: OrderItemWithProduct[] }; 6 | -------------------------------------------------------------------------------- /src/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Head, Html, Main, NextScript } from "next/document"; 2 | 3 | const Document = () => { 4 | return ( 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | ); 13 | }; 14 | export default Document; 15 | -------------------------------------------------------------------------------- /src/server/cloudinary/cloudinary.ts: -------------------------------------------------------------------------------- 1 | import { env } from "@/env/server.mjs"; 2 | import { v2 as cloudinary } from "cloudinary"; 3 | 4 | cloudinary.config({ 5 | cloud_name: env.CLOUDINARY_CLOUD_NAME, 6 | api_key: env.CLOUDINARY_API_KEY, 7 | api_secret: env.CLOUDINARY_API_SECRET, 8 | secure: true, 9 | }); 10 | 11 | export { cloudinary }; 12 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { 4 | "project": "./tsconfig.json" 5 | }, 6 | "plugins": ["@typescript-eslint"], 7 | "extends": ["next/core-web-vitals", "plugin:@typescript-eslint/recommended"], 8 | "rules": { 9 | "@typescript-eslint/consistent-type-imports": "warn" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/types/next-auth.d.ts: -------------------------------------------------------------------------------- 1 | import type { USER_ROLE } from "@prisma/client"; 2 | import type { DefaultUser } from "next-auth"; 3 | 4 | declare module "next-auth" { 5 | interface User extends DefaultUser { 6 | id: string; 7 | role: USER_ROLE; 8 | active: boolean; 9 | phone: string; 10 | } 11 | interface Session { 12 | user?: User; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/server/trpc/router/admin/index.ts: -------------------------------------------------------------------------------- 1 | import { router } from "../../trpc"; 2 | import { ordersAdminRouter } from "./orders"; 3 | import { productsAdminRouter } from "./products"; 4 | import { usersAdminRouter } from "./users"; 5 | 6 | export const adminRouter = router({ 7 | users: usersAdminRouter, 8 | products: productsAdminRouter, 9 | orders: ordersAdminRouter, 10 | }); 11 | -------------------------------------------------------------------------------- /src/components/screens/LoadingScreen.tsx: -------------------------------------------------------------------------------- 1 | const LoadingScreen = () => { 2 | return ( 3 |
8 |
12 |
13 | ); 14 | }; 15 | 16 | export default LoadingScreen; 17 | -------------------------------------------------------------------------------- /src/server/db/client.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | 3 | import { env } from "../../env/server.mjs"; 4 | 5 | declare global { 6 | // eslint-disable-next-line no-var 7 | var prisma: PrismaClient | undefined; 8 | } 9 | 10 | export const prisma = 11 | global.prisma || 12 | new PrismaClient({ 13 | log: 14 | env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"], 15 | }); 16 | 17 | if (env.NODE_ENV !== "production") { 18 | global.prisma = prisma; 19 | } 20 | -------------------------------------------------------------------------------- /src/server/trpc/router/_app.ts: -------------------------------------------------------------------------------- 1 | import { router } from "../trpc"; 2 | import { adminRouter } from "./admin"; 3 | import { ordersRouter } from "./orders"; 4 | import { productsRouter } from "./products"; 5 | import { stripeRouter } from "./stripe"; 6 | import { usersRouter } from "./users"; 7 | 8 | export const appRouter = router({ 9 | users: usersRouter, 10 | products: productsRouter, 11 | orders: ordersRouter, 12 | stripe: stripeRouter, 13 | admin: adminRouter, 14 | }); 15 | 16 | // export type definition of API 17 | export type AppRouter = typeof appRouter; 18 | -------------------------------------------------------------------------------- /src/components/ui/ToastWrapper.tsx: -------------------------------------------------------------------------------- 1 | import { Toaster } from "react-hot-toast"; 2 | 3 | const ToastWrapper = () => { 4 | return ( 5 | 21 | ); 22 | }; 23 | 24 | export default ToastWrapper; 25 | -------------------------------------------------------------------------------- /src/pages/api/trpc/[trpc].ts: -------------------------------------------------------------------------------- 1 | import { createNextApiHandler } from "@trpc/server/adapters/next"; 2 | 3 | import { env } from "../../../env/server.mjs"; 4 | import { createContext } from "../../../server/trpc/context"; 5 | import { appRouter } from "../../../server/trpc/router/_app"; 6 | 7 | // export API handler 8 | export default createNextApiHandler({ 9 | router: appRouter, 10 | createContext, 11 | onError: 12 | env.NODE_ENV === "development" 13 | ? ({ path, error }) => { 14 | console.error(`❌ tRPC failed on ${path}: ${error}`); 15 | } 16 | : undefined, 17 | }); 18 | -------------------------------------------------------------------------------- /src/components/layouts/Footer.tsx: -------------------------------------------------------------------------------- 1 | import dayjs from "dayjs"; 2 | 3 | const Footer = () => { 4 | return ( 5 |
6 |
window.scrollTo(0, 0)} 9 | > 10 | Back to top 11 |
12 |
13 | Copyright © {dayjs().format("YYYY")} Amzn Store 14 |
15 |
16 | ); 17 | }; 18 | 19 | export default Footer; 20 | -------------------------------------------------------------------------------- /src/utils/format.ts: -------------------------------------------------------------------------------- 1 | export const formatCurrency = ( 2 | value: number, 3 | currency: "USD" | "EUR" | "GBP" | "BDT" 4 | ) => { 5 | return new Intl.NumberFormat("en-US", { 6 | style: "currency", 7 | currency, 8 | }).format(value); 9 | }; 10 | 11 | export const formatEnum = (str: string) => { 12 | const words = str.split("_"); 13 | const formattedWords = words.map((word) => { 14 | return word.charAt(0) + word.slice(1).toLowerCase(); 15 | }); 16 | return formattedWords.join(" "); 17 | }; 18 | 19 | export const truncateText = (str: string, n: number) => { 20 | return str.length > n ? str.substring(0, n - 1) + "..." : str; 21 | }; 22 | -------------------------------------------------------------------------------- /src/server/common/get-server-auth-session.ts: -------------------------------------------------------------------------------- 1 | import { type GetServerSidePropsContext } from "next"; 2 | import { unstable_getServerSession } from "next-auth"; 3 | 4 | import { authOptions } from "../../pages/api/auth/[...nextauth]"; 5 | 6 | /** 7 | * Wrapper for unstable_getServerSession https://next-auth.js.org/configuration/nextjs 8 | * See example usage in trpc createContext or the restricted API route 9 | */ 10 | export const getServerAuthSession = async (ctx: { 11 | req: GetServerSidePropsContext["req"]; 12 | res: GetServerSidePropsContext["res"]; 13 | }) => { 14 | return await unstable_getServerSession(ctx.req, ctx.res, authOptions); 15 | }; 16 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /** 3 | * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. 4 | * This is especially useful for Docker builds. 5 | */ 6 | !process.env.SKIP_ENV_VALIDATION && (await import("./src/env/server.mjs")); 7 | 8 | /** @type {import("next").NextConfig} */ 9 | const config = { 10 | reactStrictMode: true, 11 | swcMinify: true, 12 | i18n: { 13 | locales: ["en"], 14 | defaultLocale: "en", 15 | }, 16 | images: { 17 | // image optimization is disabled because of exceeding the vercel hobby tier limit 18 | unoptimized: true, 19 | domains: ["fakestoreapi.com", "res.cloudinary.com"], 20 | }, 21 | }; 22 | export default config; 23 | -------------------------------------------------------------------------------- /src/pages/api/restricted.ts: -------------------------------------------------------------------------------- 1 | import { type NextApiRequest, type NextApiResponse } from "next"; 2 | 3 | import { getServerAuthSession } from "../../server/common/get-server-auth-session"; 4 | 5 | const restricted = async (req: NextApiRequest, res: NextApiResponse) => { 6 | const session = await getServerAuthSession({ req, res }); 7 | 8 | if (session) { 9 | res.send({ 10 | content: 11 | "This is protected content. You can access this content because you are signed in.", 12 | }); 13 | } else { 14 | res.send({ 15 | error: 16 | "You must be signed in to view the protected content on this page.", 17 | }); 18 | } 19 | }; 20 | 21 | export default restricted; 22 | -------------------------------------------------------------------------------- /src/components/ui/Button.tsx: -------------------------------------------------------------------------------- 1 | import type { ButtonHTMLAttributes, DetailedHTMLProps, ReactNode } from "react"; 2 | 3 | type ButtonProps = { 4 | children: ReactNode; 5 | } & DetailedHTMLProps< 6 | ButtonHTMLAttributes, 7 | HTMLButtonElement 8 | >; 9 | 10 | const Button = ({ children, className, ...btnProps }: ButtonProps) => { 11 | return ( 12 | 18 | ); 19 | }; 20 | 21 | export default Button; 22 | -------------------------------------------------------------------------------- /src/styles/customtable.module.css: -------------------------------------------------------------------------------- 1 | .tableWrapper { 2 | @apply overflow-x-auto overflow-y-hidden pb-1; 3 | } 4 | 5 | .table, 6 | .table th, 7 | .table td { 8 | @apply border-collapse border border-solid border-lowkey; 9 | } 10 | 11 | .table { 12 | @apply w-full min-w-[640px]; 13 | } 14 | 15 | .tableHeader { 16 | @apply px-4 pt-2 pb-3.5 text-left text-xs font-bold tracking-wide text-black md:text-sm; 17 | } 18 | .paginationbar { 19 | @apply mt-5 flex w-full flex-wrap items-center gap-2 text-sm md:text-base; 20 | } 21 | 22 | .paginationBtn { 23 | @apply grid aspect-square w-8 place-items-center border border-neutral-500 enabled:transition-opacity enabled:hover:opacity-90 enabled:active:opacity-100 disabled:cursor-not-allowed; 24 | } 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "noUncheckedIndexedAccess": true, 18 | "baseUrl": "./", 19 | "paths": { 20 | "@/*": ["src/*"] 21 | } 22 | }, 23 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.cjs", "**/*.mjs"], 24 | "exclude": ["node_modules"] 25 | } 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # database 12 | /prisma/db.sqlite 13 | /prisma/db.sqlite-journal 14 | 15 | # next.js 16 | /.next/ 17 | /out/ 18 | next-env.d.ts 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 | # local env files 34 | # do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables 35 | .env 36 | .env*.local 37 | 38 | # vercel 39 | .vercel 40 | 41 | # typescript 42 | *.tsbuildinfo 43 | -------------------------------------------------------------------------------- /src/components/screens/DeactivatedScreen.tsx: -------------------------------------------------------------------------------- 1 | // external imports 2 | import { ExclamationCircleIcon } from "@heroicons/react/24/outline"; 3 | 4 | const DeactivatedScreen = () => { 5 | return ( 6 |
7 | 8 |

9 | Your account has been deactivated 10 |

11 |

12 | Please contact the administrator to reactivate your account. If you are 13 | the administrator, please check the logs for more information. 14 |

15 |
16 | ); 17 | }; 18 | 19 | export default DeactivatedScreen; 20 | -------------------------------------------------------------------------------- /src/utils/render.tsx: -------------------------------------------------------------------------------- 1 | // external imports 2 | import { StarIcon } from "@heroicons/react/24/solid"; 3 | 4 | export const renderStars = (rate: number) => { 5 | const stars = []; 6 | for (let i = 1; i <= 5; i++) { 7 | if (i <= rate) { 8 | stars.push( 9 | 13 | ); 14 | } else if (i === Math.ceil(rate) && !Number.isInteger(rate)) { 15 | stars.push( 16 | 20 | ); 21 | } else { 22 | stars.push( 23 | 27 | ); 28 | } 29 | } 30 | return stars; 31 | }; 32 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # When adding additional env variables, the schema in /env/schema.mjs should be updated accordingly 2 | 3 | # Prisma 4 | DATABASE_URL=file:./db.sqlite 5 | 6 | # Next Auth 7 | # You can generate the secret via 'openssl rand -base64 32' on Linux 8 | # More info: https://next-auth.js.org/configuration/options#secret 9 | NEXTAUTH_SECRET= 10 | NEXTAUTH_URL=http://localhost:3000 11 | 12 | # Next Auth Google Provider 13 | GOOGLE_CLIENT_ID= 14 | GOOGLE_CLIENT_SECRET= 15 | 16 | # Cloudinary 17 | CLOUDINARY_CLOUD_NAME= 18 | CLOUDINARY_API_KEY= 19 | CLOUDINARY_API_SECRET= 20 | 21 | # Stripe 22 | #Stripe API keys 23 | STRIPE_PUBLISHABLE_KEY= 24 | STRIPE_SECRET_KEY= 25 | 26 | # Stripe Webhook Secret found at https://dashboard.stripe.com/test/webhooks/create?endpoint_location=local 27 | STRIPE_WEBHOOK_SECRET= 28 | 29 | # Stripe Price ID for the product you created 30 | STRIPE_PRICE_ID= 31 | -------------------------------------------------------------------------------- /src/pages/app/checkout/index.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPageWithLayout } from "@/pages/_app"; 2 | import { useCartStore } from "@/stores/cart"; 3 | import Head from "next/head"; 4 | 5 | // external imports 6 | import Cart from "@/components/Cart"; 7 | import DefaultLayout from "@/components/layouts/DefaultLayout"; 8 | 9 | const Checkout: NextPageWithLayout = () => { 10 | // cart store 11 | const cartStore = useCartStore((state) => ({ 12 | products: state.products, 13 | })); 14 | 15 | return ( 16 | <> 17 | 18 | Checkout | Amzn Store 19 | 20 |
21 | 22 |
23 | 24 | ); 25 | }; 26 | 27 | export default Checkout; 28 | 29 | Checkout.getLayout = (page) => {page}; 30 | -------------------------------------------------------------------------------- /src/env/server.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /** 3 | * This file is included in `/next.config.mjs` which ensures the app isn't built with invalid env vars. 4 | * It has to be a `.mjs`-file to be imported there. 5 | */ 6 | import { serverSchema } from "./schema.mjs"; 7 | import { env as clientEnv, formatErrors } from "./client.mjs"; 8 | 9 | const _serverEnv = serverSchema.safeParse(process.env); 10 | 11 | if (!_serverEnv.success) { 12 | console.error( 13 | "❌ Invalid environment variables:\n", 14 | ...formatErrors(_serverEnv.error.format()), 15 | ); 16 | throw new Error("Invalid environment variables"); 17 | } 18 | 19 | for (let key of Object.keys(_serverEnv.data)) { 20 | if (key.startsWith("NEXT_PUBLIC_")) { 21 | console.warn("❌ You are exposing a server-side env-variable:", key); 22 | 23 | throw new Error("You are exposing a server-side env-variable"); 24 | } 25 | } 26 | 27 | export const env = { ..._serverEnv.data, ...clientEnv }; 28 | -------------------------------------------------------------------------------- /src/components/screens/RestrictedScreen.tsx: -------------------------------------------------------------------------------- 1 | import Router from "next/router"; 2 | 3 | // external imports 4 | import Button from "@/components/ui/Button"; 5 | import { NoSymbolIcon } from "@heroicons/react/20/solid"; 6 | 7 | const RestrictedScreen = () => { 8 | return ( 9 |
10 | 11 |

12 | Restricted 13 |

14 |

15 | You are not authorized to view this page. 16 |

17 | 24 |
25 | ); 26 | }; 27 | 28 | export default RestrictedScreen; 29 | -------------------------------------------------------------------------------- /tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ["./src/**/*.{js,ts,jsx,tsx}"], 4 | theme: { 5 | extend: { 6 | colors: { 7 | bg: { 8 | white: "hsl(0, 0%, 100%)", 9 | gray: "hsl(220, 14%, 96%)", 10 | }, 11 | primary: "hsl(27, 96%, 61%)", 12 | secondary: "hsl(220, 9%, 46%)", 13 | success: "hsl(142, 71%, 45%)", 14 | danger: "hsl(0, 84%, 60%)", 15 | layout: "hsl(221, 39%, 11%)", 16 | "layout-light": "hsl(217, 19%, 27%)", 17 | lowkey: "hsl(220, 9%, 46%)", 18 | title: "hsl(0, 0%, 15%)", 19 | text: "hsl(0, 0%, 25%)", 20 | link: "hsl(193, 82%, 31%)", 21 | }, 22 | screens: { 23 | xxs: "320px", 24 | xs: "480px", 25 | }, 26 | }, 27 | }, 28 | plugins: [ 29 | require("@tailwindcss/forms"), 30 | require("@tailwindcss/line-clamp"), 31 | require("@headlessui/tailwindcss"), 32 | ], 33 | }; 34 | -------------------------------------------------------------------------------- /src/pages/404.tsx: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | import type { NextPageWithLayout } from "./_app"; 3 | 4 | // external imports 5 | import DefaultLayout from "@/components/layouts/DefaultLayout"; 6 | 7 | const Four0Four: NextPageWithLayout = () => { 8 | return ( 9 | <> 10 | 11 | 404: This page could not be found 12 | 13 |
14 |
15 |

404

16 | | 17 |
18 |
19 | This page could not be found 20 |
21 |
22 | 23 | ); 24 | }; 25 | 26 | export default Four0Four; 27 | 28 | Four0Four.getLayout = (page) => {page}; 29 | -------------------------------------------------------------------------------- /src/styles/searchbar.module.css: -------------------------------------------------------------------------------- 1 | .inputWrapper { 2 | @apply w-full cursor-default overflow-hidden rounded-md bg-white text-left shadow-md ring-2 ring-lowkey/80 transition ui-open:ring-primary; 3 | @apply focus:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-opacity-75 focus-visible:ring-offset-2 focus-visible:ring-offset-primary; 4 | } 5 | .input { 6 | @apply w-full border-none py-2.5 text-sm font-medium leading-5 text-title placeholder:text-lowkey/80 focus:ring-0; 7 | } 8 | .inputButton { 9 | @apply absolute right-0 h-full rounded-sm bg-orange-300 px-3 transition-colors hover:bg-primary; 10 | } 11 | .options { 12 | @apply absolute max-h-60 w-full overflow-auto bg-white py-1 text-sm shadow-lg ring-1 ring-lowkey/80 focus:outline-none; 13 | } 14 | .optionNull { 15 | @apply relative block cursor-default select-none truncate bg-red-200 py-2 px-3 font-medium text-title; 16 | } 17 | .option { 18 | @apply relative block cursor-pointer select-none truncate py-2 px-3 font-medium text-title ui-active:bg-neutral-200; 19 | } 20 | -------------------------------------------------------------------------------- /src/components/CategoryList.tsx: -------------------------------------------------------------------------------- 1 | import { formatEnum } from "@/utils/format"; 2 | import type { PRODUCT_CATEGORY } from "@prisma/client"; 3 | import Link from "next/link"; 4 | 5 | const CategoryList = ({ categories }: { categories: PRODUCT_CATEGORY[] }) => { 6 | return ( 7 |
11 |

Category list

12 |
13 | {categories.map((category) => ( 14 | 15 |
16 | {formatEnum(category)} 17 |
18 | 19 | ))} 20 |
21 |
22 | ); 23 | }; 24 | 25 | export default CategoryList; 26 | -------------------------------------------------------------------------------- /src/env/client.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { clientEnv, clientSchema } from "./schema.mjs"; 3 | 4 | const _clientEnv = clientSchema.safeParse(clientEnv); 5 | 6 | export const formatErrors = ( 7 | /** @type {import('zod').ZodFormattedError,string>} */ 8 | errors, 9 | ) => 10 | Object.entries(errors) 11 | .map(([name, value]) => { 12 | if (value && "_errors" in value) 13 | return `${name}: ${value._errors.join(", ")}\n`; 14 | }) 15 | .filter(Boolean); 16 | 17 | if (!_clientEnv.success) { 18 | console.error( 19 | "❌ Invalid environment variables:\n", 20 | ...formatErrors(_clientEnv.error.format()), 21 | ); 22 | throw new Error("Invalid environment variables"); 23 | } 24 | 25 | for (let key of Object.keys(_clientEnv.data)) { 26 | if (!key.startsWith("NEXT_PUBLIC_")) { 27 | console.warn( 28 | `❌ Invalid public environment variable name: ${key}. It must begin with 'NEXT_PUBLIC_'`, 29 | ); 30 | 31 | throw new Error("Invalid public environment variable name"); 32 | } 33 | } 34 | 35 | export const env = _clientEnv.data; 36 | -------------------------------------------------------------------------------- /src/components/layouts/DefaultLayout.tsx: -------------------------------------------------------------------------------- 1 | import DeactivatedScreen from "@/components/screens/DeactivatedScreen"; 2 | import ErrorScreen from "@/components/screens/ErrorScreen"; 3 | import { trpc } from "@/utils/trpc"; 4 | import { useSession } from "next-auth/react"; 5 | import { type ReactNode } from "react"; 6 | 7 | // external imports 8 | import Footer from "./Footer"; 9 | import Navbar from "./Navbar"; 10 | import LoadingScreen from "../screens/LoadingScreen"; 11 | 12 | const DefaultLayout = ({ children }: { children: ReactNode }) => { 13 | const { data: session, status } = useSession(); 14 | 15 | // get products query 16 | const productsQuery = trpc.products.get.useQuery(undefined, { 17 | staleTime: 1000 * 60 * 60 * 24, 18 | }); 19 | 20 | if (productsQuery.isLoading) { 21 | return ; 22 | } 23 | 24 | if (productsQuery.isError) { 25 | return ; 26 | } 27 | 28 | if (status === "authenticated" && !session?.user?.active) { 29 | return ; 30 | } 31 | 32 | return ( 33 | <> 34 | 35 | {children} 36 |