├── .gitattributes ├── .gitignore ├── README.md ├── TODO.txt ├── ecommerce-admin ├── .eslintrc.json ├── .gitignore ├── actions │ ├── get-graph-revenue.ts │ ├── get-recent-orders.ts │ ├── get-sales-count.ts │ ├── get-stock-count.ts │ └── get-total-revenue.ts ├── app │ ├── (auth) │ │ ├── (routes) │ │ │ ├── sign-in │ │ │ │ └── [[...sign-in]] │ │ │ │ │ └── page.tsx │ │ │ └── sign-up │ │ │ │ └── [[...sign-up]] │ │ │ │ └── page.tsx │ │ └── layout.tsx │ ├── (dashboard) │ │ └── [storeId] │ │ │ ├── (routes) │ │ │ ├── billboards │ │ │ │ ├── [billboardId] │ │ │ │ │ ├── components │ │ │ │ │ │ └── billboard-form.tsx │ │ │ │ │ └── page.tsx │ │ │ │ ├── components │ │ │ │ │ ├── cell-action.tsx │ │ │ │ │ ├── client.tsx │ │ │ │ │ └── columns.tsx │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ │ ├── categories │ │ │ │ ├── [categoryId] │ │ │ │ │ ├── components │ │ │ │ │ │ └── category-form.tsx │ │ │ │ │ └── page.tsx │ │ │ │ ├── components │ │ │ │ │ ├── cell-action.tsx │ │ │ │ │ ├── client.tsx │ │ │ │ │ └── columns.tsx │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ │ ├── colors │ │ │ │ ├── [colorId] │ │ │ │ │ ├── components │ │ │ │ │ │ └── color-form.tsx │ │ │ │ │ └── page.tsx │ │ │ │ ├── components │ │ │ │ │ ├── cell-action.tsx │ │ │ │ │ ├── client.tsx │ │ │ │ │ └── columns.tsx │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ │ ├── orders │ │ │ │ ├── [orderId] │ │ │ │ │ ├── components │ │ │ │ │ │ └── order-form.tsx │ │ │ │ │ └── page.tsx │ │ │ │ ├── components │ │ │ │ │ ├── cell-action.tsx │ │ │ │ │ ├── client.tsx │ │ │ │ │ └── columns.tsx │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ │ ├── page.tsx │ │ │ ├── products │ │ │ │ ├── [productId] │ │ │ │ │ ├── components │ │ │ │ │ │ └── product-form.tsx │ │ │ │ │ └── page.tsx │ │ │ │ ├── components │ │ │ │ │ ├── cell-action.tsx │ │ │ │ │ ├── client.tsx │ │ │ │ │ └── columns.tsx │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ │ ├── settings │ │ │ │ ├── components │ │ │ │ │ └── settings-form.tsx │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ │ └── sizes │ │ │ │ ├── [sizeId] │ │ │ │ ├── components │ │ │ │ │ └── size-form.tsx │ │ │ │ └── page.tsx │ │ │ │ ├── components │ │ │ │ ├── cell-action.tsx │ │ │ │ ├── client.tsx │ │ │ │ └── columns.tsx │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ │ └── layout.tsx │ ├── (root) │ │ ├── (routes) │ │ │ └── page.tsx │ │ └── layout.tsx │ ├── api │ │ ├── [storeId] │ │ │ ├── billboards │ │ │ │ ├── [billboardId] │ │ │ │ │ └── route.ts │ │ │ │ └── route.ts │ │ │ ├── categories │ │ │ │ ├── [categoryId] │ │ │ │ │ └── route.ts │ │ │ │ └── route.ts │ │ │ ├── checkout │ │ │ │ └── route.ts │ │ │ ├── colors │ │ │ │ ├── [colorId] │ │ │ │ │ └── route.ts │ │ │ │ └── route.ts │ │ │ ├── orders │ │ │ │ ├── [orderId] │ │ │ │ │ └── route.ts │ │ │ │ └── route.ts │ │ │ ├── products │ │ │ │ ├── [productId] │ │ │ │ │ └── route.ts │ │ │ │ └── route.ts │ │ │ └── sizes │ │ │ │ ├── [sizeId] │ │ │ │ └── route.ts │ │ │ │ └── route.ts │ │ ├── stores │ │ │ ├── [storeId] │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ └── webhook │ │ │ └── route.ts │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ ├── loading.tsx │ └── types │ │ ├── reset.d.ts │ │ └── types.ts ├── components.json ├── components │ ├── main-nav.tsx │ ├── modals │ │ ├── alert-modal.tsx │ │ ├── store-modal.tsx │ │ └── use-category-modal.tsx │ ├── navbar.tsx │ ├── overview.tsx │ ├── recent-orders.tsx │ ├── store-switcher.tsx │ ├── theme-toggle.tsx │ └── ui │ │ ├── alert.tsx │ │ ├── api-alert.tsx │ │ ├── api-list.tsx │ │ ├── badge.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── checkbox.tsx │ │ ├── command.tsx │ │ ├── data-table.tsx │ │ ├── dialog.tsx │ │ ├── dropdown-menu.tsx │ │ ├── form.tsx │ │ ├── heading.tsx │ │ ├── image-upload.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── loader.tsx │ │ ├── modal.tsx │ │ ├── popover.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── sheet.tsx │ │ ├── switch.tsx │ │ ├── table.tsx │ │ └── textarea.tsx ├── hooks │ ├── use-origin.tsx │ └── use-store-model.tsx ├── lib │ ├── mounted-check.ts │ ├── prismadb.ts │ ├── stripe.ts │ └── utils.ts ├── middleware.ts ├── next.config.js ├── package.json ├── postcss.config.js ├── prisma │ └── schema.prisma ├── providers │ ├── modal-provider.tsx │ ├── theme-provider.tsx │ └── toast-provider.tsx ├── public │ ├── next.svg │ └── vercel.svg ├── tailwind.config.js └── tsconfig.json ├── ecommerce-store ├── .eslintrc.json ├── .gitignore ├── actions │ ├── get-billboard.tsx │ ├── get-categories.tsx │ ├── get-category.tsx │ ├── get-colors.tsx │ ├── get-product.tsx │ ├── get-products.tsx │ └── get-sizes.tsx ├── app │ ├── (routes) │ │ ├── cart │ │ │ ├── components │ │ │ │ ├── cart-item.tsx │ │ │ │ └── summary.tsx │ │ │ └── page.tsx │ │ ├── category │ │ │ └── [categoryId] │ │ │ │ ├── components │ │ │ │ ├── filter.tsx │ │ │ │ └── mobile-filters.tsx │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ ├── page.tsx │ │ ├── product │ │ │ └── [productId] │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ └── search │ │ │ ├── loading.tsx │ │ │ └── page.tsx │ ├── favicon.ico │ ├── globals.css │ └── layout.tsx ├── components.json ├── components │ ├── billboard.tsx │ ├── dialog │ │ ├── cart-dialog.tsx │ │ ├── cart-item-dialog.tsx │ │ └── summary-dialog.tsx │ ├── footer-bar.tsx │ ├── footer.tsx │ ├── gallery │ │ ├── gallery-tab.tsx │ │ └── index.tsx │ ├── info.tsx │ ├── main-nav.tsx │ ├── navbar-actions.tsx │ ├── navbar.tsx │ ├── preview-modal.tsx │ ├── product-list.tsx │ ├── size-table.tsx │ └── ui │ │ ├── accordion.tsx │ │ ├── button.tsx │ │ ├── color-effect.tsx │ │ ├── container.tsx │ │ ├── currency.tsx │ │ ├── icon-button.tsx │ │ ├── loader.tsx │ │ ├── modal.tsx │ │ ├── no-results.tsx │ │ ├── product-card.tsx │ │ ├── search-input.tsx │ │ ├── select.tsx │ │ ├── skeleton.tsx │ │ └── table.tsx ├── hooks │ ├── use-cart.ts │ └── use-preview-modal.ts ├── lib │ ├── mounted-check.ts │ └── utils.ts ├── next.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── providers │ ├── modal-provider.tsx │ └── toast-provider.tsx ├── public │ ├── logo-black.png │ ├── logo.png │ ├── next.svg │ ├── search-icon.svg │ └── vercel.svg ├── tailwind.config.js ├── tsconfig.json └── types.ts └── package-lock.json /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Serverless directories 108 | .serverless/ 109 | 110 | # FuseBox cache 111 | .fusebox/ 112 | 113 | # DynamoDB Local files 114 | .dynamodb/ 115 | 116 | # TernJS port file 117 | .tern-port 118 | 119 | # Stores VSCode versions used for testing VSCode extensions 120 | .vscode-test 121 | 122 | # yarn v2 123 | .yarn/cache 124 | .yarn/unplugged 125 | .yarn/build-state.yml 126 | .yarn/install-state.gz 127 | .pnp.* 128 | -------------------------------------------------------------------------------- /TODO.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Repith/ecommerce-project/1c07ae8ffe6bb1f8c641a0410e08fdfeb375182a/TODO.txt -------------------------------------------------------------------------------- /ecommerce-admin/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /ecommerce-admin/.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 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | .env 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /ecommerce-admin/actions/get-graph-revenue.ts: -------------------------------------------------------------------------------- 1 | import prismadb from "@/lib/prismadb"; 2 | 3 | interface GraphData { 4 | name?: string; 5 | total?: number; 6 | } 7 | 8 | export const getGraphRevenue = async ( 9 | storeId: string 10 | ): Promise => { 11 | const paidOrders = await prismadb.order.findMany({ 12 | where: { 13 | storeId, 14 | isPaid: true, 15 | }, 16 | include: { 17 | orderItems: { 18 | include: { 19 | product: true, 20 | }, 21 | }, 22 | }, 23 | }); 24 | 25 | const monthlyRevenue: { [key: number]: number } = {}; 26 | 27 | // Grouping the orders by month and summing the revenue 28 | for (const order of paidOrders) { 29 | const month = order.createdAt.getMonth(); // 0 for Jan, 1 for Feb, ... 30 | let revenueForOrder = 0; 31 | 32 | for (const item of order.orderItems) { 33 | revenueForOrder += item.product.price.toNumber() || 0; // Add a fallback value in case it's undefined 34 | } 35 | 36 | // Adding the revenue for this order to the respective month 37 | monthlyRevenue[month] = 38 | (monthlyRevenue[month] || 0) + revenueForOrder; 39 | } 40 | 41 | // Converting the grouped data into the format expected by the graph 42 | const graphData: GraphData[] = [ 43 | { name: "Jan", total: 0 }, 44 | { name: "Feb", total: 0 }, 45 | { name: "Mar", total: 0 }, 46 | { name: "Apr", total: 0 }, 47 | { name: "May", total: 0 }, 48 | { name: "Jun", total: 0 }, 49 | { name: "Jul", total: 0 }, 50 | { name: "Aug", total: 0 }, 51 | { name: "Sep", total: 0 }, 52 | { name: "Oct", total: 0 }, 53 | { name: "Nov", total: 0 }, 54 | { name: "Dec", total: 0 }, 55 | ]; 56 | 57 | // Filling in the revenue data 58 | for (const month in monthlyRevenue) { 59 | const monthIndex = parseInt(month, 10); 60 | 61 | if (!isNaN(monthIndex)) { 62 | const graphDataEntry = graphData[monthIndex]; 63 | 64 | if (graphDataEntry) { 65 | graphDataEntry.total = 66 | monthlyRevenue[monthIndex] ?? 0; 67 | } 68 | } 69 | } 70 | 71 | return graphData; 72 | }; 73 | -------------------------------------------------------------------------------- /ecommerce-admin/actions/get-recent-orders.ts: -------------------------------------------------------------------------------- 1 | import prismadb from "@/lib/prismadb"; 2 | 3 | export const getRecentOrders = async (storeId: string) => { 4 | const recentOrders = await prismadb.order.findMany({ 5 | where: { 6 | storeId, 7 | isPaid: true, 8 | }, 9 | include: { 10 | orderItems: { 11 | include: { 12 | product: true, 13 | }, 14 | }, 15 | }, 16 | orderBy: { 17 | createdAt: "desc", 18 | }, 19 | take: 5, 20 | }); 21 | return recentOrders; 22 | }; 23 | -------------------------------------------------------------------------------- /ecommerce-admin/actions/get-sales-count.ts: -------------------------------------------------------------------------------- 1 | import prismadb from "@/lib/prismadb"; 2 | 3 | export const getSalesCount = async (storeId: string) => { 4 | const salesCount = await prismadb.order.count({ 5 | where: { 6 | storeId, 7 | isPaid: true, 8 | }, 9 | }); 10 | 11 | return salesCount; 12 | }; 13 | -------------------------------------------------------------------------------- /ecommerce-admin/actions/get-stock-count.ts: -------------------------------------------------------------------------------- 1 | import prismadb from "@/lib/prismadb"; 2 | 3 | export const getStockCount = async (storeId: string) => { 4 | const stockCount = await prismadb.product.count({ 5 | where: { 6 | storeId, 7 | isArchived: false, 8 | }, 9 | }); 10 | 11 | return stockCount; 12 | }; 13 | -------------------------------------------------------------------------------- /ecommerce-admin/actions/get-total-revenue.ts: -------------------------------------------------------------------------------- 1 | import prismadb from "@/lib/prismadb"; 2 | 3 | export const getTotalRevenue = async (storeId: string) => { 4 | const paidOrders = await prismadb.order.findMany({ 5 | where: { 6 | storeId, 7 | isPaid: true, 8 | }, 9 | include: { 10 | orderItems: { 11 | include: { 12 | product: true, 13 | }, 14 | }, 15 | }, 16 | }); 17 | 18 | const totalRevenue = paidOrders.reduce((total, order) => { 19 | const orderTotal = order.orderItems.reduce((orderSum, item) => { 20 | return orderSum + item.product.price.toNumber(); 21 | }, 0); 22 | return total + orderTotal; 23 | }, 0); 24 | 25 | return totalRevenue; 26 | }; 27 | -------------------------------------------------------------------------------- /ecommerce-admin/app/(auth)/(routes)/sign-in/[[...sign-in]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignIn } from "@clerk/nextjs"; 2 | 3 | export default function Page() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /ecommerce-admin/app/(auth)/(routes)/sign-up/[[...sign-up]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignUp } from "@clerk/nextjs"; 2 | 3 | export default function Page() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /ecommerce-admin/app/(auth)/layout.tsx: -------------------------------------------------------------------------------- 1 | export default function AuthLayout({ 2 | children, 3 | }: { 4 | children: React.ReactNode; 5 | }) { 6 | return ( 7 |
{children}
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /ecommerce-admin/app/(dashboard)/[storeId]/(routes)/billboards/[billboardId]/page.tsx: -------------------------------------------------------------------------------- 1 | import prismadb from "@/lib/prismadb"; 2 | import { BillboardForm } from "./components/billboard-form"; 3 | 4 | const BillboardPage = async ({ 5 | params, 6 | }: { 7 | params: { billboardId: string }; 8 | }) => { 9 | const billboard = await prismadb.billboard.findUnique({ 10 | where: { 11 | id: params.billboardId, 12 | }, 13 | }); 14 | 15 | return ( 16 |
17 |
18 | 19 |
20 |
21 | ); 22 | }; 23 | 24 | export default BillboardPage; 25 | -------------------------------------------------------------------------------- /ecommerce-admin/app/(dashboard)/[storeId]/(routes)/billboards/components/cell-action.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import axios from "axios"; 4 | import { Copy, Edit, MoreHorizontal, Trash } from "lucide-react"; 5 | import { useParams, useRouter } from "next/navigation"; 6 | import { useState } from "react"; 7 | import { toast } from "react-hot-toast"; 8 | 9 | import { 10 | DropdownMenu, 11 | DropdownMenuContent, 12 | DropdownMenuItem, 13 | DropdownMenuLabel, 14 | DropdownMenuTrigger, 15 | } from "@/components/ui/dropdown-menu"; 16 | import { BillboardColumn } from "./columns"; 17 | import { AlertModal } from "@/components/modals/alert-modal"; 18 | import { Button } from "@/components/ui/button"; 19 | 20 | interface CellActionProps { 21 | data: BillboardColumn; 22 | } 23 | 24 | export const CellAction: React.FC = ({ data }) => { 25 | const router = useRouter(); 26 | const params = useParams(); 27 | 28 | const [loading, setLoading] = useState(false); 29 | const [open, setOpen] = useState(false); 30 | 31 | const onCopy = (id: string) => { 32 | navigator.clipboard.writeText(id); 33 | toast.success("Billboard ID copied to clipboard."); 34 | }; 35 | 36 | const onDelete = async () => { 37 | try { 38 | setLoading(true); 39 | await axios.delete(`/api/${params.storeId}/billboards/${data.id}`); 40 | router.refresh(); 41 | toast.success("Billboard deleted."); 42 | } catch (error) { 43 | toast.error( 44 | "Make sure you removed all categories using this billboard first." 45 | ); 46 | } finally { 47 | setLoading(false); 48 | setOpen(false); 49 | } 50 | }; 51 | 52 | return ( 53 | <> 54 | setOpen(false)} 57 | onConfirm={onDelete} 58 | loading={loading} 59 | /> 60 | 61 | 62 | 66 | 67 | 68 | Actions 69 | 71 | router.push(`/${params.storeId}/billboards/${data.id}`) 72 | } 73 | > 74 | 75 | Update 76 | 77 | onCopy(data.id)}> 78 | 79 | Copy ID 80 | 81 | setOpen(true)}> 82 | 83 | Delete 84 | 85 | 86 | 87 | 88 | ); 89 | }; 90 | -------------------------------------------------------------------------------- /ecommerce-admin/app/(dashboard)/[storeId]/(routes)/billboards/components/client.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Plus } from "lucide-react"; 4 | import { useParams, useRouter } from "next/navigation"; 5 | 6 | import { Button } from "@/components/ui/button"; 7 | import { Heading } from "@/components/ui/heading"; 8 | import { Separator } from "@/components/ui/separator"; 9 | import { DataTable } from "@/components/ui/data-table"; 10 | import { ApiList } from "@/components/ui/api-list"; 11 | 12 | import { BillboardColumn, columns } from "./columns"; 13 | interface BillboardsClientProps { 14 | data: BillboardColumn[]; 15 | } 16 | 17 | export const BillboardsClient: React.FC = ({ data }) => { 18 | const router = useRouter(); 19 | const params = useParams(); 20 | 21 | return ( 22 | <> 23 |
24 | 28 | 34 |
35 | 36 | 37 | 38 | 39 | 40 | 41 | ); 42 | }; 43 | -------------------------------------------------------------------------------- /ecommerce-admin/app/(dashboard)/[storeId]/(routes)/billboards/components/columns.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ColumnDef } from "@tanstack/react-table"; 4 | 5 | import { CellAction } from "./cell-action"; 6 | 7 | export type BillboardColumn = { 8 | id: string; 9 | label: string; 10 | createdAt: string; 11 | }; 12 | 13 | export const columns: ColumnDef[] = [ 14 | { 15 | accessorKey: "label", 16 | header: "Label", 17 | }, 18 | { 19 | accessorKey: "createdAt", 20 | header: "Date", 21 | }, 22 | { 23 | id: "actions", 24 | cell: ({ row }) => , 25 | }, 26 | ]; 27 | -------------------------------------------------------------------------------- /ecommerce-admin/app/(dashboard)/[storeId]/(routes)/billboards/loading.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Loader } from "@/components/ui/loader"; 4 | 5 | const Loading = () => { 6 | return ( 7 |
8 | 9 |
10 | ); 11 | }; 12 | 13 | export default Loading; 14 | -------------------------------------------------------------------------------- /ecommerce-admin/app/(dashboard)/[storeId]/(routes)/billboards/page.tsx: -------------------------------------------------------------------------------- 1 | import { format } from "date-fns"; 2 | 3 | import prismadb from "@/lib/prismadb"; 4 | 5 | import { BillboardsClient } from "./components/client"; 6 | import { BillboardColumn } from "./components/columns"; 7 | 8 | const BillboardsPage = async ({ 9 | params, 10 | }: { 11 | params: { storeId: string }; 12 | }) => { 13 | const billboards = await prismadb.billboard.findMany({ 14 | where: { 15 | storeId: params.storeId, 16 | }, 17 | orderBy: { 18 | createdAt: "desc", 19 | }, 20 | }); 21 | 22 | const formattedBillboards: BillboardColumn[] = 23 | billboards.map((item) => ({ 24 | id: item.id, 25 | label: item.label, 26 | createdAt: format(item.createdAt, "MMMM do, yyyy"), 27 | })); 28 | 29 | return ( 30 |
31 |
32 | 33 |
34 |
35 | ); 36 | }; 37 | 38 | export default BillboardsPage; 39 | -------------------------------------------------------------------------------- /ecommerce-admin/app/(dashboard)/[storeId]/(routes)/categories/[categoryId]/page.tsx: -------------------------------------------------------------------------------- 1 | import prismadb from "@/lib/prismadb"; 2 | 3 | import { CategoryForm } from "./components/category-form"; 4 | 5 | const CategoryPage = async ({ 6 | params, 7 | }: { 8 | params: { categoryId: string; storeId: string }; 9 | }) => { 10 | const category = await prismadb.category.findUnique({ 11 | where: { 12 | id: params.categoryId, 13 | }, 14 | }); 15 | 16 | const billboards = await prismadb.billboard.findMany({ 17 | where: { 18 | storeId: params.storeId, 19 | }, 20 | }); 21 | 22 | return ( 23 |
24 |
25 | 29 |
30 |
31 | ); 32 | }; 33 | 34 | export default CategoryPage; 35 | -------------------------------------------------------------------------------- /ecommerce-admin/app/(dashboard)/[storeId]/(routes)/categories/components/cell-action.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import axios from "axios"; 4 | import { Copy, Edit, MoreHorizontal, Trash } from "lucide-react"; 5 | import { useParams, useRouter } from "next/navigation"; 6 | import { useState } from "react"; 7 | import { toast } from "react-hot-toast"; 8 | 9 | import { 10 | DropdownMenu, 11 | DropdownMenuContent, 12 | DropdownMenuItem, 13 | DropdownMenuLabel, 14 | DropdownMenuTrigger, 15 | } from "@/components/ui/dropdown-menu"; 16 | import { CategoryColumn } from "./columns"; 17 | import { AlertModal } from "@/components/modals/alert-modal"; 18 | import { Button } from "@/components/ui/button"; 19 | 20 | interface CellActionProps { 21 | data: CategoryColumn; 22 | } 23 | 24 | export const CellAction: React.FC = ({ data }) => { 25 | const router = useRouter(); 26 | const params = useParams(); 27 | 28 | const [loading, setLoading] = useState(false); 29 | const [open, setOpen] = useState(false); 30 | 31 | const onDelete = async () => { 32 | try { 33 | setLoading(true); 34 | await axios.delete(`/api/${params.storeId}/categories/${data.id}`); 35 | toast.success("Category deleted."); 36 | router.refresh(); 37 | } catch (error) { 38 | toast.error( 39 | "Make sure you removed all products using this category first." 40 | ); 41 | } finally { 42 | setOpen(false); 43 | setLoading(false); 44 | } 45 | }; 46 | 47 | const onCopy = (id: string) => { 48 | navigator.clipboard.writeText(id); 49 | toast.success("Category ID copied to clipboard."); 50 | }; 51 | 52 | return ( 53 | <> 54 | setOpen(false)} 57 | onConfirm={onDelete} 58 | loading={loading} 59 | /> 60 | 61 | 62 | 66 | 67 | 68 | Actions 69 | 71 | router.push(`/${params.storeId}/categories/${data.id}`) 72 | } 73 | > 74 | 75 | Update 76 | 77 | onCopy(data.id)}> 78 | 79 | Copy ID 80 | 81 | setOpen(true)}> 82 | 83 | Delete 84 | 85 | 86 | 87 | 88 | ); 89 | }; 90 | -------------------------------------------------------------------------------- /ecommerce-admin/app/(dashboard)/[storeId]/(routes)/categories/components/client.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Plus } from "lucide-react"; 4 | import { useParams, useRouter } from "next/navigation"; 5 | 6 | import { Button } from "@/components/ui/button"; 7 | import { Heading } from "@/components/ui/heading"; 8 | import { Separator } from "@/components/ui/separator"; 9 | import { DataTable } from "@/components/ui/data-table"; 10 | import { ApiList } from "@/components/ui/api-list"; 11 | 12 | import { CategoryColumn, columns } from "./columns"; 13 | interface CategoryClientProps { 14 | data: CategoryColumn[]; 15 | } 16 | 17 | export const CategoriesClient: React.FC = ({ data }) => { 18 | const router = useRouter(); 19 | const params = useParams(); 20 | 21 | return ( 22 | <> 23 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | ); 42 | }; 43 | -------------------------------------------------------------------------------- /ecommerce-admin/app/(dashboard)/[storeId]/(routes)/categories/components/columns.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ColumnDef } from "@tanstack/react-table"; 4 | 5 | import { CellAction } from "./cell-action"; 6 | 7 | export type CategoryColumn = { 8 | id: string; 9 | name: string; 10 | billboardLabel: string; 11 | createdAt: string; 12 | }; 13 | 14 | export const columns: ColumnDef[] = [ 15 | { 16 | accessorKey: "name", 17 | header: "Name", 18 | }, 19 | { 20 | accessorKey: "billboardLabel", 21 | header: "Billboard Label", 22 | cell: ({ row }) => row.original.billboardLabel, 23 | }, 24 | { 25 | accessorKey: "createdAt", 26 | header: "Date", 27 | }, 28 | { 29 | id: "actions", 30 | cell: ({ row }) => , 31 | }, 32 | ]; 33 | -------------------------------------------------------------------------------- /ecommerce-admin/app/(dashboard)/[storeId]/(routes)/categories/loading.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Loader } from "@/components/ui/loader"; 4 | 5 | const Loading = () => { 6 | return ( 7 |
8 | 9 |
10 | ); 11 | }; 12 | 13 | export default Loading; 14 | -------------------------------------------------------------------------------- /ecommerce-admin/app/(dashboard)/[storeId]/(routes)/categories/page.tsx: -------------------------------------------------------------------------------- 1 | import { format } from "date-fns"; 2 | 3 | import prismadb from "@/lib/prismadb"; 4 | 5 | import { CategoriesClient } from "./components/client"; 6 | import { CategoryColumn } from "./components/columns"; 7 | 8 | const CategoriesPage = async ({ 9 | params, 10 | }: { 11 | params: { storeId: string }; 12 | }) => { 13 | const categories = await prismadb.category.findMany({ 14 | where: { 15 | storeId: params.storeId, 16 | }, 17 | include: { 18 | billboard: true, 19 | }, 20 | orderBy: { 21 | createdAt: "desc", 22 | }, 23 | }); 24 | 25 | const formattedCategories: CategoryColumn[] = 26 | categories.map((item) => ({ 27 | id: item.id, 28 | name: item.name, 29 | billboardLabel: item.billboard.label, 30 | createdAt: format(item.createdAt, "MMMM do, yyyy"), 31 | })); 32 | 33 | return ( 34 |
35 |
36 | 37 |
38 |
39 | ); 40 | }; 41 | 42 | export default CategoriesPage; 43 | -------------------------------------------------------------------------------- /ecommerce-admin/app/(dashboard)/[storeId]/(routes)/colors/[colorId]/page.tsx: -------------------------------------------------------------------------------- 1 | import prismadb from "@/lib/prismadb"; 2 | import { ColorForm } from "./components/color-form"; 3 | 4 | const ColorPage = async ({ 5 | params, 6 | }: { 7 | params: { colorId: string }; 8 | }) => { 9 | const color = await prismadb.color.findUnique({ 10 | where: { 11 | id: params.colorId, 12 | }, 13 | }); 14 | 15 | return ( 16 |
17 |
18 | 19 |
20 |
21 | ); 22 | }; 23 | 24 | export default ColorPage; 25 | -------------------------------------------------------------------------------- /ecommerce-admin/app/(dashboard)/[storeId]/(routes)/colors/components/cell-action.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import axios from "axios"; 4 | import { Copy, Edit, MoreHorizontal, Trash } from "lucide-react"; 5 | import { useParams, useRouter } from "next/navigation"; 6 | import { useState } from "react"; 7 | import { toast } from "react-hot-toast"; 8 | 9 | import { 10 | DropdownMenu, 11 | DropdownMenuContent, 12 | DropdownMenuItem, 13 | DropdownMenuLabel, 14 | DropdownMenuTrigger, 15 | } from "@/components/ui/dropdown-menu"; 16 | import { ColorColumn } from "./columns"; 17 | import { AlertModal } from "@/components/modals/alert-modal"; 18 | import { Button } from "@/components/ui/button"; 19 | 20 | interface CellActionProps { 21 | data: ColorColumn; 22 | } 23 | 24 | export const CellAction: React.FC = ({ data }) => { 25 | const router = useRouter(); 26 | const params = useParams(); 27 | 28 | const [loading, setLoading] = useState(false); 29 | const [open, setOpen] = useState(false); 30 | 31 | const onCopy = (id: string) => { 32 | navigator.clipboard.writeText(id); 33 | toast.success("Color ID copied to clipboard."); 34 | }; 35 | 36 | const onDelete = async () => { 37 | try { 38 | setLoading(true); 39 | await axios.delete(`/api/${params.storeId}/colors/${data.id}`); 40 | router.refresh(); 41 | toast.success("Color deleted."); 42 | } catch (error) { 43 | toast.error("Make sure you removed all products using this color first."); 44 | } finally { 45 | setLoading(false); 46 | setOpen(false); 47 | } 48 | }; 49 | 50 | return ( 51 | <> 52 | setOpen(false)} 55 | onConfirm={onDelete} 56 | loading={loading} 57 | /> 58 | 59 | 60 | 64 | 65 | 66 | Actions 67 | router.push(`/${params.storeId}/colors/${data.id}`)} 69 | > 70 | 71 | Update 72 | 73 | onCopy(data.id)}> 74 | 75 | Copy ID 76 | 77 | setOpen(true)}> 78 | 79 | Delete 80 | 81 | 82 | 83 | 84 | ); 85 | }; 86 | -------------------------------------------------------------------------------- /ecommerce-admin/app/(dashboard)/[storeId]/(routes)/colors/components/client.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Plus } from "lucide-react"; 4 | import { useParams, useRouter } from "next/navigation"; 5 | 6 | import { Button } from "@/components/ui/button"; 7 | import { Heading } from "@/components/ui/heading"; 8 | import { Separator } from "@/components/ui/separator"; 9 | import { DataTable } from "@/components/ui/data-table"; 10 | import { ApiList } from "@/components/ui/api-list"; 11 | 12 | import { ColorColumn, columns } from "./columns"; 13 | interface ColorsClientProps { 14 | data: ColorColumn[]; 15 | } 16 | 17 | export const ColorsClient: React.FC = ({ data }) => { 18 | const router = useRouter(); 19 | const params = useParams(); 20 | 21 | return ( 22 | <> 23 |
24 | 28 | 32 |
33 | 34 | 35 | 36 | 37 | 38 | 39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /ecommerce-admin/app/(dashboard)/[storeId]/(routes)/colors/components/columns.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ColumnDef } from "@tanstack/react-table"; 4 | 5 | import { CellAction } from "./cell-action"; 6 | 7 | export type ColorColumn = { 8 | id: string; 9 | name: string; 10 | value: string; 11 | createdAt: string; 12 | }; 13 | 14 | export const columns: ColumnDef[] = [ 15 | { 16 | accessorKey: "name", 17 | header: "Name", 18 | }, 19 | { 20 | accessorKey: "value", 21 | header: "Value", 22 | cell: ({ row }) => ( 23 |
24 |
28 | 29 | {row.original.value} 30 |
31 | ), 32 | }, 33 | { 34 | accessorKey: "createdAt", 35 | header: "Date", 36 | }, 37 | { 38 | id: "actions", 39 | cell: ({ row }) => , 40 | }, 41 | ]; 42 | -------------------------------------------------------------------------------- /ecommerce-admin/app/(dashboard)/[storeId]/(routes)/colors/loading.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Loader } from "@/components/ui/loader"; 4 | 5 | const Loading = () => { 6 | return ( 7 |
8 | 9 |
10 | ); 11 | }; 12 | 13 | export default Loading; 14 | -------------------------------------------------------------------------------- /ecommerce-admin/app/(dashboard)/[storeId]/(routes)/colors/page.tsx: -------------------------------------------------------------------------------- 1 | import { format } from "date-fns"; 2 | 3 | import prismadb from "@/lib/prismadb"; 4 | 5 | import { ColorsClient } from "./components/client"; 6 | import { ColorColumn } from "./components/columns"; 7 | 8 | const ColorsPage = async ({ 9 | params, 10 | }: { 11 | params: { storeId: string }; 12 | }) => { 13 | const colors = await prismadb.color.findMany({ 14 | where: { 15 | storeId: params.storeId, 16 | }, 17 | orderBy: { 18 | createdAt: "desc", 19 | }, 20 | }); 21 | 22 | const formattedColors: ColorColumn[] = colors.map( 23 | (item) => ({ 24 | id: item.id, 25 | name: item.name, 26 | value: item.value, 27 | createdAt: format(item.createdAt, "MMMM do, yyyy"), 28 | }) 29 | ); 30 | 31 | return ( 32 |
33 |
34 | 35 |
36 |
37 | ); 38 | }; 39 | 40 | export default ColorsPage; 41 | -------------------------------------------------------------------------------- /ecommerce-admin/app/(dashboard)/[storeId]/(routes)/orders/[orderId]/page.tsx: -------------------------------------------------------------------------------- 1 | import prismadb from "@/lib/prismadb"; 2 | import { OrderForm } from "./components/order-form"; 3 | import { OrderColumn } from "../components/columns"; 4 | import { formatter } from "@/lib/utils"; 5 | import { format } from "date-fns"; 6 | 7 | const OrderPage = async ({ 8 | params, 9 | }: { 10 | params: { orderId: string }; 11 | }) => { 12 | const order = await prismadb.order.findUnique({ 13 | where: { 14 | id: params.orderId, 15 | }, 16 | include: { 17 | orderItems: { 18 | include: { 19 | product: true, 20 | variant: true, 21 | }, 22 | }, 23 | }, 24 | }); 25 | 26 | if (!order) { 27 | return
Order not found
; 28 | } 29 | 30 | const formattedOrder: OrderColumn = { 31 | id: order.id, 32 | phone: order.phone, 33 | address: order.address, 34 | products: JSON.stringify( 35 | order.orderItems.map( 36 | (orderItem) => orderItem.product.name 37 | ) 38 | ), 39 | variants: JSON.stringify( 40 | order.orderItems.map( 41 | (orderItem) => 42 | `${orderItem.variant?.colorId} - ${orderItem.variant?.sizeId}` 43 | ) 44 | ), 45 | quantity: JSON.stringify( 46 | order.orderItems.map( 47 | (orderItem) => `${orderItem.quantity}` 48 | ) 49 | ), 50 | totalPrice: formatter.format( 51 | order.orderItems.reduce((total, item) => { 52 | return total + Number(item.product.price); 53 | }, 0) 54 | ), 55 | isPaid: order.isPaid, 56 | isSent: order.isSent, 57 | createdAt: format(order.createdAt, "MMMM do, yyyy"), 58 | }; 59 | 60 | return ( 61 |
62 |
63 | 64 |
65 |
66 | ); 67 | }; 68 | 69 | export default OrderPage; 70 | -------------------------------------------------------------------------------- /ecommerce-admin/app/(dashboard)/[storeId]/(routes)/orders/components/client.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { DataTable } from "@/components/ui/data-table"; 4 | import { Heading } from "@/components/ui/heading"; 5 | import { Separator } from "@/components/ui/separator"; 6 | 7 | import { columns, OrderColumn } from "./columns"; 8 | 9 | interface OrderClientProps { 10 | data: OrderColumn[]; 11 | } 12 | 13 | export const OrderClient: React.FC = ({ data }) => { 14 | return ( 15 | <> 16 | 20 | 21 | 22 | 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /ecommerce-admin/app/(dashboard)/[storeId]/(routes)/orders/components/columns.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ColumnDef } from "@tanstack/react-table"; 4 | import { CellAction } from "./cell-action"; 5 | import { ArrowUpDown, Check, X } from "lucide-react"; 6 | import { Button } from "@/components/ui/button"; 7 | import { Badge } from "@/components/ui/badge"; 8 | 9 | export type OrderColumn = { 10 | id: string; 11 | phone: string; 12 | address: string; 13 | isPaid: boolean; 14 | isSent: boolean; 15 | totalPrice: string; 16 | products: string; 17 | createdAt: string; 18 | variants: string; 19 | quantity: string; 20 | }; 21 | 22 | export const columns: ColumnDef[] = [ 23 | { 24 | accessorKey: "products", 25 | header: "Products", 26 | cell: ({ row }) => { 27 | const cellValue = JSON.parse(row.getValue("products")) as string[]; 28 | return ( 29 | <> 30 | {cellValue.map((product, index) => ( 31 |
32 | {index + 1}. {product} 33 |
34 | ))} 35 | 36 | ); 37 | }, 38 | }, 39 | { 40 | accessorKey: "variants", 41 | header: "Variants", 42 | cell: ({ row }) => { 43 | const cellValue = JSON.parse(row.getValue("variants")) as string[]; 44 | return ( 45 | <> 46 | {cellValue.map((variant, index) => ( 47 |
{variant}
48 | ))} 49 | 50 | ); 51 | }, 52 | }, 53 | { 54 | accessorKey: "quantity", 55 | header: "Quantity", 56 | cell: ({ row }) => { 57 | const cellValue = JSON.parse(row.getValue("quantity")) as string[]; 58 | return ( 59 | <> 60 | {cellValue.map((quantity, index) => ( 61 |
{quantity}
62 | ))} 63 | 64 | ); 65 | }, 66 | }, 67 | { 68 | accessorKey: "phone", 69 | header: "Phone", 70 | }, 71 | { 72 | accessorKey: "address", 73 | header: "Address", 74 | }, 75 | { 76 | accessorKey: "totalPrice", 77 | header: "Total price", 78 | }, 79 | { 80 | accessorKey: "isPaid", 81 | header: "Paid", 82 | cell: ({ row }) => { 83 | return row.getValue("isPaid") ? : ; 84 | }, 85 | }, 86 | { 87 | accessorKey: "isSent", 88 | header: ({ column }) => { 89 | return ( 90 | 97 | ); 98 | }, 99 | 100 | cell: ({ row }) => { 101 | return row.getValue("isPaid") ? ( 102 | row.getValue("isSent") ? ( 103 | Sent 104 | ) : ( 105 | Pending 106 | ) 107 | ) : ( 108 | Processing 109 | ); 110 | }, 111 | }, 112 | { 113 | id: "actions", 114 | cell: ({ row }) => , 115 | }, 116 | ]; 117 | -------------------------------------------------------------------------------- /ecommerce-admin/app/(dashboard)/[storeId]/(routes)/orders/loading.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Loader } from "@/components/ui/loader"; 4 | 5 | const Loading = () => { 6 | return ( 7 |
8 | 9 |
10 | ); 11 | }; 12 | 13 | export default Loading; 14 | -------------------------------------------------------------------------------- /ecommerce-admin/app/(dashboard)/[storeId]/(routes)/orders/page.tsx: -------------------------------------------------------------------------------- 1 | import { format } from "date-fns"; 2 | 3 | import prismadb from "@/lib/prismadb"; 4 | import { formatter } from "@/lib/utils"; 5 | 6 | import { OrderColumn } from "./components/columns"; 7 | import { OrderClient } from "./components/client"; 8 | 9 | const OrdersPage = async ({ 10 | params, 11 | }: { 12 | params: { storeId: string }; 13 | }) => { 14 | const orders = await prismadb.order.findMany({ 15 | where: { 16 | storeId: params.storeId, 17 | }, 18 | include: { 19 | orderItems: { 20 | include: { 21 | product: true, 22 | variant: true, 23 | }, 24 | }, 25 | }, 26 | orderBy: { 27 | createdAt: "desc", 28 | }, 29 | }); 30 | 31 | const formattedOrders: OrderColumn[] = orders.map( 32 | (item) => ({ 33 | id: item.id, 34 | phone: item.phone, 35 | address: item.address, 36 | products: JSON.stringify( 37 | item.orderItems.map( 38 | (orderItem) => orderItem.product.name 39 | ) 40 | ), 41 | variants: JSON.stringify( 42 | item.orderItems.map( 43 | (orderItem) => 44 | `${orderItem.variant?.colorId} - ${orderItem.variant?.sizeId}` 45 | ) 46 | ), 47 | quantity: JSON.stringify( 48 | item.orderItems.map( 49 | (orderItem) => `${orderItem.quantity}` 50 | ) 51 | ), 52 | totalPrice: formatter.format( 53 | item.orderItems.reduce((total, item) => { 54 | return total + Number(item.product.price); 55 | }, 0) 56 | ), 57 | isPaid: item.isPaid, 58 | isSent: item.isSent, 59 | createdAt: format(item.createdAt, "MMMM do, yyyy"), 60 | }) 61 | ); 62 | 63 | return ( 64 |
65 |
66 | 67 |
68 |
69 | ); 70 | }; 71 | 72 | export default OrdersPage; 73 | -------------------------------------------------------------------------------- /ecommerce-admin/app/(dashboard)/[storeId]/(routes)/products/[productId]/page.tsx: -------------------------------------------------------------------------------- 1 | import prismadb from "@/lib/prismadb"; 2 | import { ProductForm } from "./components/product-form"; 3 | 4 | const ProductPage = async ({ 5 | params, 6 | }: { 7 | params: { productId: string; storeId: string }; 8 | }) => { 9 | const product = await prismadb.product.findUnique({ 10 | where: { 11 | id: params.productId, 12 | }, 13 | include: { 14 | images: true, 15 | variants: true, 16 | }, 17 | }); 18 | 19 | const categories = await prismadb.category.findMany({ 20 | where: { 21 | storeId: params.storeId, 22 | }, 23 | }); 24 | 25 | const sizes = await prismadb.size.findMany({ 26 | where: { 27 | storeId: params.storeId, 28 | }, 29 | }); 30 | 31 | const colors = await prismadb.color.findMany({ 32 | where: { 33 | storeId: params.storeId, 34 | }, 35 | }); 36 | 37 | return ( 38 |
39 |
40 | 46 |
47 |
48 | ); 49 | }; 50 | 51 | export default ProductPage; 52 | -------------------------------------------------------------------------------- /ecommerce-admin/app/(dashboard)/[storeId]/(routes)/products/components/cell-action.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import axios from "axios"; 4 | import { Copy, Edit, MoreHorizontal, Trash } from "lucide-react"; 5 | import { useParams, useRouter } from "next/navigation"; 6 | import { useState } from "react"; 7 | import { toast } from "react-hot-toast"; 8 | 9 | import { 10 | DropdownMenu, 11 | DropdownMenuContent, 12 | DropdownMenuItem, 13 | DropdownMenuLabel, 14 | DropdownMenuTrigger, 15 | } from "@/components/ui/dropdown-menu"; 16 | import { ProductColumn } from "./columns"; 17 | import { AlertModal } from "@/components/modals/alert-modal"; 18 | import { Button } from "@/components/ui/button"; 19 | 20 | interface CellActionProps { 21 | data: ProductColumn; 22 | } 23 | 24 | export const CellAction: React.FC = ({ data }) => { 25 | const router = useRouter(); 26 | const params = useParams(); 27 | 28 | const [loading, setLoading] = useState(false); 29 | const [open, setOpen] = useState(false); 30 | 31 | const onCopy = (id: string) => { 32 | navigator.clipboard.writeText(id); 33 | toast.success("Product ID copied to clipboard."); 34 | }; 35 | 36 | const onDelete = async () => { 37 | try { 38 | setLoading(true); 39 | await axios.delete(`/api/${params.storeId}/products/${data.id}`); 40 | router.refresh(); 41 | toast.success("Product deleted."); 42 | } catch (error) { 43 | toast.error("Something went wrong."); 44 | } finally { 45 | setLoading(false); 46 | setOpen(false); 47 | } 48 | }; 49 | 50 | return ( 51 | <> 52 | setOpen(false)} 55 | onConfirm={onDelete} 56 | loading={loading} 57 | /> 58 | 59 | 60 | 64 | 65 | 66 | Actions 67 | 69 | router.push(`/${params.storeId}/products/${data.id}`) 70 | } 71 | > 72 | 73 | Update 74 | 75 | onCopy(data.id)}> 76 | 77 | Copy ID 78 | 79 | setOpen(true)}> 80 | 81 | Delete 82 | 83 | 84 | 85 | 86 | ); 87 | }; 88 | -------------------------------------------------------------------------------- /ecommerce-admin/app/(dashboard)/[storeId]/(routes)/products/components/client.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Plus } from "lucide-react"; 4 | import { useParams, useRouter } from "next/navigation"; 5 | 6 | import { Button } from "@/components/ui/button"; 7 | import { Heading } from "@/components/ui/heading"; 8 | import { Separator } from "@/components/ui/separator"; 9 | import { DataTable } from "@/components/ui/data-table"; 10 | import { ApiList } from "@/components/ui/api-list"; 11 | 12 | import { ProductColumn, columns } from "./columns"; 13 | interface ProductClientProps { 14 | data: ProductColumn[]; 15 | } 16 | 17 | export const ProductsClient: React.FC = ({ data }) => { 18 | const router = useRouter(); 19 | const params = useParams(); 20 | 21 | return ( 22 | <> 23 |
24 | 28 | 32 |
33 | 34 | 35 | 36 | 37 | 38 | 39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /ecommerce-admin/app/(dashboard)/[storeId]/(routes)/products/components/columns.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ColumnDef } from "@tanstack/react-table"; 4 | 5 | import { CellAction } from "./cell-action"; 6 | import Link from "next/link"; 7 | 8 | export type ProductColumn = { 9 | id: string; 10 | name: string; 11 | price: string; 12 | category: string; 13 | variants: number; 14 | inStock: number; 15 | isFeatured: boolean; 16 | isArchived: boolean; 17 | createdAt: string; 18 | }; 19 | 20 | export const columns: ColumnDef[] = [ 21 | { 22 | accessorKey: "name", 23 | header: () =>
Name
, 24 | }, 25 | { 26 | accessorKey: "isArchived", 27 | header: "Archived", 28 | }, 29 | { 30 | accessorKey: "isFeatured", 31 | header: "Featured", 32 | }, 33 | { 34 | accessorKey: "price", 35 | header: "Price", 36 | }, 37 | { 38 | accessorKey: "category", 39 | header: "Category", 40 | }, 41 | 42 | { 43 | accessorKey: "variants", 44 | header: "Variants", 45 | }, 46 | { 47 | accessorKey: "inStock", 48 | header: "In Stock", 49 | }, 50 | { 51 | accessorKey: "createdAt", 52 | header: "Date", 53 | }, 54 | { 55 | id: "actions", 56 | cell: ({ row }) => , 57 | }, 58 | ]; 59 | -------------------------------------------------------------------------------- /ecommerce-admin/app/(dashboard)/[storeId]/(routes)/products/loading.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Loader } from "@/components/ui/loader"; 4 | 5 | const Loading = () => { 6 | return ( 7 |
8 | 9 |
10 | ); 11 | }; 12 | 13 | export default Loading; 14 | -------------------------------------------------------------------------------- /ecommerce-admin/app/(dashboard)/[storeId]/(routes)/products/page.tsx: -------------------------------------------------------------------------------- 1 | import { format } from "date-fns"; 2 | 3 | import prismadb from "@/lib/prismadb"; 4 | import { formatter } from "@/lib/utils"; 5 | 6 | import { ProductsClient } from "./components/client"; 7 | import { ProductColumn } from "./components/columns"; 8 | 9 | const ProductsPage = async ({ 10 | params, 11 | }: { 12 | params: { storeId: string }; 13 | }) => { 14 | const products = await prismadb.product.findMany({ 15 | where: { 16 | storeId: params.storeId, 17 | }, 18 | include: { 19 | category: true, 20 | variants: true, 21 | }, 22 | orderBy: { 23 | createdAt: "desc", 24 | }, 25 | }); 26 | 27 | const formattedProducts: ProductColumn[] = products.map( 28 | (item) => { 29 | return { 30 | id: item.id, 31 | name: item.name, 32 | isFeatured: item.isFeatured, 33 | isArchived: item.isArchived, 34 | price: formatter.format(item.price.toNumber()), 35 | category: item.category.name, 36 | variants: item.variants.length, 37 | inStock: item.variants.reduce( 38 | (total, variant) => total + variant.inStock, 39 | 0 40 | ), 41 | description: item.description, 42 | createdAt: format(item.createdAt, "MMMM do, yyyy"), 43 | }; 44 | } 45 | ); 46 | 47 | return ( 48 |
49 |
50 | 51 |
52 |
53 | ); 54 | }; 55 | 56 | export default ProductsPage; 57 | -------------------------------------------------------------------------------- /ecommerce-admin/app/(dashboard)/[storeId]/(routes)/settings/loading.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Loader } from "@/components/ui/loader"; 4 | 5 | const Loading = () => { 6 | return ( 7 |
8 | 9 |
10 | ); 11 | }; 12 | 13 | export default Loading; 14 | -------------------------------------------------------------------------------- /ecommerce-admin/app/(dashboard)/[storeId]/(routes)/settings/page.tsx: -------------------------------------------------------------------------------- 1 | import { auth } from "@clerk/nextjs"; 2 | import { redirect } from "next/navigation"; 3 | 4 | import prismadb from "@/lib/prismadb"; 5 | import { SettingsForm } from "./components/settings-form"; 6 | 7 | interface SettingsPageProps { 8 | params: { 9 | storeId: string; 10 | }; 11 | } 12 | 13 | const SettingsPage: React.FC = async ({ 14 | params, 15 | }) => { 16 | const { userId } = auth(); 17 | 18 | if (!userId) { 19 | redirect("/sign-in"); 20 | } 21 | 22 | const store = await prismadb.store.findFirst({ 23 | where: { 24 | id: params.storeId, 25 | userId, 26 | }, 27 | }); 28 | 29 | if (!store) { 30 | redirect("/"); 31 | } 32 | 33 | return ( 34 |
35 |
36 | 37 |
38 |
39 | ); 40 | }; 41 | 42 | export default SettingsPage; 43 | -------------------------------------------------------------------------------- /ecommerce-admin/app/(dashboard)/[storeId]/(routes)/sizes/[sizeId]/page.tsx: -------------------------------------------------------------------------------- 1 | import prismadb from "@/lib/prismadb"; 2 | import { SizeForm } from "./components/size-form"; 3 | 4 | const SizePage = async ({ 5 | params, 6 | }: { 7 | params: { sizeId: string }; 8 | }) => { 9 | const size = await prismadb.size.findUnique({ 10 | where: { 11 | id: params.sizeId, 12 | }, 13 | }); 14 | 15 | return ( 16 |
17 |
18 | 19 |
20 |
21 | ); 22 | }; 23 | 24 | export default SizePage; 25 | -------------------------------------------------------------------------------- /ecommerce-admin/app/(dashboard)/[storeId]/(routes)/sizes/components/cell-action.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import axios from "axios"; 4 | import { Copy, Edit, MoreHorizontal, Trash } from "lucide-react"; 5 | import { useParams, useRouter } from "next/navigation"; 6 | import { useState } from "react"; 7 | import { toast } from "react-hot-toast"; 8 | 9 | import { 10 | DropdownMenu, 11 | DropdownMenuContent, 12 | DropdownMenuItem, 13 | DropdownMenuLabel, 14 | DropdownMenuTrigger, 15 | } from "@/components/ui/dropdown-menu"; 16 | import { SizeColumn } from "./columns"; 17 | import { AlertModal } from "@/components/modals/alert-modal"; 18 | import { Button } from "@/components/ui/button"; 19 | 20 | interface CellActionProps { 21 | data: SizeColumn; 22 | } 23 | 24 | export const CellAction: React.FC = ({ data }) => { 25 | const router = useRouter(); 26 | const params = useParams(); 27 | 28 | const [loading, setLoading] = useState(false); 29 | const [open, setOpen] = useState(false); 30 | 31 | const onCopy = (id: string) => { 32 | navigator.clipboard.writeText(id); 33 | toast.success("Size ID copied to clipboard."); 34 | }; 35 | 36 | const onDelete = async () => { 37 | try { 38 | setLoading(true); 39 | await axios.delete(`/api/${params.storeId}/sizes/${data.id}`); 40 | router.refresh(); 41 | toast.success("Size deleted."); 42 | } catch (error) { 43 | toast.error("Make sure you removed all products using this size first."); 44 | } finally { 45 | setLoading(false); 46 | setOpen(false); 47 | } 48 | }; 49 | 50 | return ( 51 | <> 52 | setOpen(false)} 55 | onConfirm={onDelete} 56 | loading={loading} 57 | /> 58 | 59 | 60 | 64 | 65 | 66 | Actions 67 | router.push(`/${params.storeId}/sizes/${data.id}`)} 69 | > 70 | 71 | Update 72 | 73 | onCopy(data.id)}> 74 | 75 | Copy ID 76 | 77 | setOpen(true)}> 78 | 79 | Delete 80 | 81 | 82 | 83 | 84 | ); 85 | }; 86 | -------------------------------------------------------------------------------- /ecommerce-admin/app/(dashboard)/[storeId]/(routes)/sizes/components/client.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Plus } from "lucide-react"; 4 | import { useParams, useRouter } from "next/navigation"; 5 | 6 | import { Button } from "@/components/ui/button"; 7 | import { Heading } from "@/components/ui/heading"; 8 | import { Separator } from "@/components/ui/separator"; 9 | import { DataTable } from "@/components/ui/data-table"; 10 | import { ApiList } from "@/components/ui/api-list"; 11 | 12 | import { SizeColumn, columns } from "./columns"; 13 | interface SizesClientProps { 14 | data: SizeColumn[]; 15 | } 16 | 17 | export const SizesClient: React.FC = ({ data }) => { 18 | const router = useRouter(); 19 | const params = useParams(); 20 | 21 | return ( 22 | <> 23 |
24 | 28 | 32 |
33 | 34 | 35 | 36 | 37 | 38 | 39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /ecommerce-admin/app/(dashboard)/[storeId]/(routes)/sizes/components/columns.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ColumnDef } from "@tanstack/react-table"; 4 | 5 | import { CellAction } from "./cell-action"; 6 | 7 | export type SizeColumn = { 8 | id: string; 9 | name: string; 10 | value: string; 11 | createdAt: string; 12 | }; 13 | 14 | export const columns: ColumnDef[] = [ 15 | { 16 | accessorKey: "name", 17 | header: "Name", 18 | }, 19 | { 20 | accessorKey: "value", 21 | header: "Value", 22 | }, 23 | { 24 | accessorKey: "createdAt", 25 | header: "Date", 26 | }, 27 | { 28 | id: "actions", 29 | cell: ({ row }) => , 30 | }, 31 | ]; 32 | -------------------------------------------------------------------------------- /ecommerce-admin/app/(dashboard)/[storeId]/(routes)/sizes/loading.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Loader } from "@/components/ui/loader"; 4 | 5 | const Loading = () => { 6 | return ( 7 |
8 | 9 |
10 | ); 11 | }; 12 | 13 | export default Loading; 14 | -------------------------------------------------------------------------------- /ecommerce-admin/app/(dashboard)/[storeId]/(routes)/sizes/page.tsx: -------------------------------------------------------------------------------- 1 | import { format } from "date-fns"; 2 | 3 | import prismadb from "@/lib/prismadb"; 4 | 5 | import { SizesClient } from "./components/client"; 6 | import { SizeColumn } from "./components/columns"; 7 | 8 | const SizesPage = async ({ 9 | params, 10 | }: { 11 | params: { storeId: string }; 12 | }) => { 13 | const sizes = await prismadb.size.findMany({ 14 | where: { 15 | storeId: params.storeId, 16 | }, 17 | orderBy: { 18 | createdAt: "desc", 19 | }, 20 | }); 21 | 22 | const formattedSizes: SizeColumn[] = sizes.map( 23 | (item) => ({ 24 | id: item.id, 25 | name: item.name, 26 | value: item.value, 27 | createdAt: format(item.createdAt, "MMMM do, yyyy"), 28 | }) 29 | ); 30 | 31 | return ( 32 |
33 |
34 | 35 |
36 |
37 | ); 38 | }; 39 | 40 | export default SizesPage; 41 | -------------------------------------------------------------------------------- /ecommerce-admin/app/(dashboard)/[storeId]/layout.tsx: -------------------------------------------------------------------------------- 1 | import { auth } from "@clerk/nextjs"; 2 | import { redirect } from "next/navigation"; 3 | 4 | import prismadb from "@/lib/prismadb"; 5 | import Navbar from "@/components/navbar"; 6 | 7 | export default async function DashboardLayout({ 8 | children, 9 | params, 10 | }: { 11 | children: React.ReactNode; 12 | params: { storeId: string }; 13 | }) { 14 | const { userId } = auth(); 15 | 16 | if (!userId) { 17 | redirect("/sign-in"); 18 | } 19 | 20 | const store = await prismadb.store.findFirst({ 21 | where: { 22 | id: params.storeId, 23 | userId, 24 | }, 25 | }); 26 | 27 | if (!store) { 28 | redirect("/"); 29 | } 30 | 31 | const stores = await prismadb.store.findMany({ 32 | where: { 33 | userId, 34 | }, 35 | }); 36 | 37 | return ( 38 | <> 39 |
40 | 41 | {children} 42 |
43 | 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /ecommerce-admin/app/(root)/(routes)/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect } from "react"; 4 | 5 | import { useStoreModal } from "@/hooks/use-store-model"; 6 | 7 | const SetupPage = () => { 8 | const onOpen = useStoreModal((state) => state.onOpen); 9 | const isOpen = useStoreModal((state) => state.isOpen); 10 | 11 | useEffect(() => { 12 | if (!isOpen) { 13 | onOpen(); 14 | } 15 | }, [isOpen, onOpen]); 16 | 17 | return null; 18 | }; 19 | 20 | export default SetupPage; 21 | -------------------------------------------------------------------------------- /ecommerce-admin/app/(root)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { auth } from "@clerk/nextjs"; 2 | import { redirect } from "next/navigation"; 3 | 4 | import prismadb from "@/lib/prismadb"; 5 | 6 | export default async function SetupLayout({ 7 | children, 8 | }: { 9 | children: React.ReactNode; 10 | }) { 11 | const { userId } = auth(); 12 | 13 | if (!userId) { 14 | redirect("/sign-in"); 15 | } 16 | 17 | const store = await prismadb.store.findFirst({ 18 | where: { 19 | userId, 20 | }, 21 | }); 22 | 23 | if (store) { 24 | redirect(`/${store.id}`); 25 | } 26 | 27 | return <>{children}; 28 | } 29 | -------------------------------------------------------------------------------- /ecommerce-admin/app/api/[storeId]/billboards/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import { auth } from "@clerk/nextjs"; 3 | 4 | import prismadb from "@/lib/prismadb"; 5 | 6 | export async function POST( 7 | req: Request, 8 | { params }: { params: { storeId: string } } 9 | ) { 10 | try { 11 | const { userId } = auth(); 12 | const body = (await req.json()) as { 13 | label: string; 14 | imageUrl: string; 15 | }; 16 | 17 | const { label, imageUrl } = body; 18 | 19 | if (!userId) { 20 | return new NextResponse("Unauthenticated", { 21 | status: 401, 22 | }); 23 | } 24 | 25 | if (!label) { 26 | return new NextResponse("Label is required", { 27 | status: 400, 28 | }); 29 | } 30 | 31 | if (!imageUrl) { 32 | return new NextResponse("Image URL is required", { 33 | status: 400, 34 | }); 35 | } 36 | 37 | if (!params.storeId) { 38 | return new NextResponse("Store ID is required", { 39 | status: 400, 40 | }); 41 | } 42 | 43 | const storeByUserId = await prismadb.store.findFirst({ 44 | where: { 45 | id: params.storeId, 46 | userId, 47 | }, 48 | }); 49 | 50 | if (!storeByUserId) { 51 | return new NextResponse("Unauthorized", { 52 | status: 405, 53 | }); 54 | } 55 | 56 | const billboard = await prismadb.billboard.create({ 57 | data: { 58 | label, 59 | imageUrl, 60 | storeId: params.storeId, 61 | }, 62 | }); 63 | 64 | return NextResponse.json(billboard); 65 | } catch (error) { 66 | console.log("[BILLBOARDS_POST]", error); 67 | return new NextResponse("Internal error", { 68 | status: 500, 69 | }); 70 | } 71 | } 72 | 73 | export async function GET( 74 | req: Request, 75 | { params }: { params: { storeId: string } } 76 | ) { 77 | try { 78 | if (!params.storeId) { 79 | return new NextResponse("Store ID is required", { 80 | status: 400, 81 | }); 82 | } 83 | 84 | const billboards = await prismadb.billboard.findMany({ 85 | where: { 86 | storeId: params.storeId, 87 | }, 88 | }); 89 | 90 | return NextResponse.json(billboards); 91 | } catch (error) { 92 | console.log("[BILLBOARDS_GET]", error); 93 | return new NextResponse("Internal error", { 94 | status: 500, 95 | }); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /ecommerce-admin/app/api/[storeId]/categories/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import { auth } from "@clerk/nextjs"; 3 | 4 | import prismadb from "@/lib/prismadb"; 5 | 6 | const corsHeaders = { 7 | "Access-Control-Allow-Origin": "*", 8 | "Access-Control-Allow-Methods": 9 | "GET, POST, PUT, DELETE, OPTIONS", 10 | "Access-Control-Allow-Headers": 11 | "Content-Type, Authorization", 12 | proxy: "baseUrlForTheAPI", 13 | }; 14 | 15 | export async function OPTIONS() { 16 | return NextResponse.json({}, { headers: corsHeaders }); 17 | } 18 | 19 | export async function POST( 20 | req: Request, 21 | { params }: { params: { storeId: string } } 22 | ) { 23 | try { 24 | const { userId } = auth(); 25 | const body = (await req.json()) as { 26 | name: string; 27 | billboardId: string; 28 | }; 29 | 30 | const { name, billboardId } = body; 31 | 32 | if (!userId) { 33 | return new NextResponse("Unauthenticated", { 34 | status: 401, 35 | }); 36 | } 37 | 38 | if (!name) { 39 | return new NextResponse("Name is required", { 40 | status: 400, 41 | }); 42 | } 43 | 44 | if (!billboardId) { 45 | return new NextResponse("Billboard ID is required", { 46 | status: 400, 47 | }); 48 | } 49 | 50 | if (!params.storeId) { 51 | return new NextResponse("Store ID is required", { 52 | status: 400, 53 | }); 54 | } 55 | 56 | const storeByUserId = await prismadb.store.findFirst({ 57 | where: { 58 | id: params.storeId, 59 | userId, 60 | }, 61 | }); 62 | 63 | if (!storeByUserId) { 64 | return new NextResponse("Unauthorized", { 65 | status: 405, 66 | }); 67 | } 68 | 69 | const category = await prismadb.category.create({ 70 | data: { 71 | name, 72 | billboardId, 73 | storeId: params.storeId, 74 | }, 75 | }); 76 | 77 | return NextResponse.json(category); 78 | } catch (error) { 79 | console.log("[CATEGORIES_POST]", error); 80 | return new NextResponse("Internal error", { 81 | status: 500, 82 | }); 83 | } 84 | } 85 | 86 | export async function GET( 87 | req: Request, 88 | { params }: { params: { storeId: string } } 89 | ) { 90 | try { 91 | if (!params.storeId) { 92 | return new NextResponse("Store ID is required", { 93 | status: 400, 94 | }); 95 | } 96 | 97 | const categories = await prismadb.category.findMany({ 98 | where: { 99 | storeId: params.storeId, 100 | }, 101 | }); 102 | 103 | return NextResponse.json(categories, { 104 | headers: corsHeaders, 105 | }); 106 | } catch (error) { 107 | console.log("[CATEGORIES_POST]", error); 108 | return new NextResponse("Internal error", { 109 | status: 500, 110 | }); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /ecommerce-admin/app/api/[storeId]/colors/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import { auth } from "@clerk/nextjs"; 3 | 4 | import prismadb from "@/lib/prismadb"; 5 | 6 | export async function POST( 7 | req: Request, 8 | { params }: { params: { storeId: string } } 9 | ) { 10 | try { 11 | const { userId } = auth(); 12 | const body = (await req.json()) as { 13 | name: string; 14 | value: string; 15 | }; 16 | 17 | const { name, value } = body; 18 | 19 | if (!userId) { 20 | return new NextResponse("Unauthenticated", { 21 | status: 401, 22 | }); 23 | } 24 | 25 | if (!name) { 26 | return new NextResponse("Name is required", { 27 | status: 400, 28 | }); 29 | } 30 | 31 | if (!value) { 32 | return new NextResponse("Value is required", { 33 | status: 400, 34 | }); 35 | } 36 | 37 | if (!params.storeId) { 38 | return new NextResponse("Store ID is required", { 39 | status: 400, 40 | }); 41 | } 42 | 43 | const storeByUserId = await prismadb.store.findFirst({ 44 | where: { 45 | id: params.storeId, 46 | userId, 47 | }, 48 | }); 49 | 50 | if (!storeByUserId) { 51 | return new NextResponse("Unauthorized", { 52 | status: 405, 53 | }); 54 | } 55 | 56 | const colors = await prismadb.color.create({ 57 | data: { 58 | name, 59 | value, 60 | storeId: params.storeId, 61 | }, 62 | }); 63 | 64 | return NextResponse.json(colors); 65 | } catch (error) { 66 | console.log("[COLORS_POST]", error); 67 | return new NextResponse("Internal error", { 68 | status: 500, 69 | }); 70 | } 71 | } 72 | 73 | export async function GET( 74 | req: Request, 75 | { params }: { params: { storeId: string } } 76 | ) { 77 | try { 78 | if (!params.storeId) { 79 | return new NextResponse("Store id is required", { 80 | status: 400, 81 | }); 82 | } 83 | 84 | const colors = await prismadb.color.findMany({ 85 | where: { 86 | storeId: params.storeId, 87 | }, 88 | }); 89 | 90 | return NextResponse.json(colors); 91 | } catch (error) { 92 | console.log("[COLORS_GET]", error); 93 | return new NextResponse("Internal error", { 94 | status: 500, 95 | }); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /ecommerce-admin/app/api/[storeId]/orders/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import { auth } from "@clerk/nextjs"; 3 | 4 | import prismadb from "@/lib/prismadb"; 5 | 6 | export async function POST( 7 | req: Request, 8 | { params }: { params: { storeId: string } } 9 | ) { 10 | try { 11 | const { userId } = auth(); 12 | 13 | const body = await req.json(); 14 | 15 | const { isPaid, isSent, phone, address, orderItems } = 16 | body; 17 | 18 | if (!userId) { 19 | return new NextResponse("Unauthenticated", { 20 | status: 403, 21 | }); 22 | } 23 | 24 | if (!isPaid) { 25 | return new NextResponse("Payment check is required", { 26 | status: 400, 27 | }); 28 | } 29 | 30 | if (!isPaid) { 31 | return new NextResponse( 32 | "Mark if an order is sent or not", 33 | { 34 | status: 400, 35 | } 36 | ); 37 | } 38 | 39 | if (!phone) { 40 | return new NextResponse("Phone number is required", { 41 | status: 400, 42 | }); 43 | } 44 | 45 | if (!address) { 46 | return new NextResponse("Adress is required", { 47 | status: 400, 48 | }); 49 | } 50 | 51 | if (!orderItems) { 52 | return new NextResponse( 53 | "Need at least 1 order of a product", 54 | { 55 | status: 400, 56 | } 57 | ); 58 | } 59 | 60 | if (!params.storeId) { 61 | return new NextResponse("Store id is required", { 62 | status: 400, 63 | }); 64 | } 65 | 66 | const storeByUserId = await prismadb.store.findFirst({ 67 | where: { 68 | id: params.storeId, 69 | userId, 70 | }, 71 | }); 72 | 73 | if (!storeByUserId) { 74 | return new NextResponse("Unauthorized", { 75 | status: 405, 76 | }); 77 | } 78 | 79 | const order = await prismadb.order.create({ 80 | data: { 81 | isPaid, 82 | isSent, 83 | phone, 84 | address, 85 | storeId: params.storeId, 86 | orderItems: { 87 | createMany: { 88 | data: [ 89 | ...orderItems.map( 90 | (orderItem: { 91 | quantity: number; 92 | variant: string; 93 | product: string; 94 | }) => orderItem 95 | ), 96 | ], 97 | }, 98 | }, 99 | }, 100 | }); 101 | 102 | return NextResponse.json(order); 103 | } catch (error) { 104 | console.log("[ORDER_POST]", error); 105 | return new NextResponse("Internal error", { 106 | status: 500, 107 | }); 108 | } 109 | } 110 | 111 | export async function GET( 112 | req: Request, 113 | { params }: { params: { storeId: string } } 114 | ) { 115 | try { 116 | if (!params.storeId) { 117 | return new NextResponse("Store ID is required", { 118 | status: 400, 119 | }); 120 | } 121 | 122 | const orders = await prismadb.order.findMany({ 123 | where: { 124 | storeId: params.storeId, 125 | }, 126 | include: { 127 | orderItems: true, 128 | }, 129 | }); 130 | 131 | return NextResponse.json(orders); 132 | } catch (error) { 133 | console.log("[ORDERS_GET]", error); 134 | return new NextResponse("Internal error", { 135 | status: 500, 136 | }); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /ecommerce-admin/app/api/[storeId]/sizes/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import { auth } from "@clerk/nextjs"; 3 | 4 | import prismadb from "@/lib/prismadb"; 5 | 6 | export async function POST( 7 | req: Request, 8 | { params }: { params: { storeId: string } } 9 | ) { 10 | try { 11 | const { userId } = auth(); 12 | const body = (await req.json()) as { 13 | name: string; 14 | value: string; 15 | }; 16 | 17 | const { name, value } = body; 18 | 19 | if (!userId) { 20 | return new NextResponse("Unauthenticated", { 21 | status: 401, 22 | }); 23 | } 24 | 25 | if (!name) { 26 | return new NextResponse("Name is required", { 27 | status: 400, 28 | }); 29 | } 30 | 31 | if (!value) { 32 | return new NextResponse("Value is required", { 33 | status: 400, 34 | }); 35 | } 36 | 37 | if (!params.storeId) { 38 | return new NextResponse("Store ID is required", { 39 | status: 400, 40 | }); 41 | } 42 | 43 | const storeByUserId = await prismadb.store.findFirst({ 44 | where: { 45 | id: params.storeId, 46 | userId, 47 | }, 48 | }); 49 | 50 | if (!storeByUserId) { 51 | return new NextResponse("Unauthorized", { 52 | status: 405, 53 | }); 54 | } 55 | 56 | const sizes = await prismadb.size.create({ 57 | data: { 58 | name, 59 | value, 60 | storeId: params.storeId, 61 | }, 62 | }); 63 | 64 | return NextResponse.json(sizes); 65 | } catch (error) { 66 | console.log("[SIZES_POST]", error); 67 | return new NextResponse("Internal error", { 68 | status: 500, 69 | }); 70 | } 71 | } 72 | 73 | export async function GET( 74 | req: Request, 75 | { params }: { params: { storeId: string } } 76 | ) { 77 | try { 78 | if (!params.storeId) { 79 | return new NextResponse("Store ID is required", { 80 | status: 400, 81 | }); 82 | } 83 | 84 | const sizes = await prismadb.size.findMany({ 85 | where: { 86 | storeId: params.storeId, 87 | }, 88 | }); 89 | 90 | return NextResponse.json(sizes); 91 | } catch (error) { 92 | console.log("[SIZES_GET]", error); 93 | return new NextResponse("Internal error", { 94 | status: 500, 95 | }); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /ecommerce-admin/app/api/stores/[storeId]/route.ts: -------------------------------------------------------------------------------- 1 | import { auth } from "@clerk/nextjs"; 2 | import { NextResponse } from "next/server"; 3 | 4 | import prismadb from "@/lib/prismadb"; 5 | 6 | export async function PATCH( 7 | req: Request, 8 | { params }: { params: { storeId: string } } 9 | ) { 10 | try { 11 | const { userId } = auth(); 12 | const body = await req.json(); 13 | 14 | const { name } = body; 15 | 16 | if (!userId) { 17 | return new NextResponse("Unauthenticated", { status: 401 }); 18 | } 19 | 20 | if (!name) { 21 | return new NextResponse("Name is required", { status: 400 }); 22 | } 23 | 24 | if (!params.storeId) { 25 | return new NextResponse("Store id is required", { status: 400 }); 26 | } 27 | 28 | const store = await prismadb.store.updateMany({ 29 | where: { 30 | id: params.storeId, 31 | userId, 32 | }, 33 | data: { 34 | name, 35 | }, 36 | }); 37 | 38 | return NextResponse.json(store); 39 | } catch (error) { 40 | console.log("[STORE_PATCH", error); 41 | return new NextResponse("Internal error", { status: 500 }); 42 | } 43 | } 44 | 45 | export async function DELETE( 46 | req: Request, 47 | { params }: { params: { storeId: string } } 48 | ) { 49 | try { 50 | const { userId } = auth(); 51 | 52 | if (!userId) { 53 | return new NextResponse("Unauthenticated", { status: 401 }); 54 | } 55 | 56 | if (!params.storeId) { 57 | return new NextResponse("Store id is required", { status: 400 }); 58 | } 59 | 60 | const store = await prismadb.store.deleteMany({ 61 | where: { 62 | id: params.storeId, 63 | userId, 64 | }, 65 | }); 66 | 67 | return NextResponse.json(store); 68 | } catch (error) { 69 | console.log("[STORE_DELETE", error); 70 | return new NextResponse("Internal error", { status: 500 }); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /ecommerce-admin/app/api/stores/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import { auth } from "@clerk/nextjs"; 3 | 4 | import prismadb from "@/lib/prismadb"; 5 | 6 | export async function POST(req: Request) { 7 | try { 8 | const { userId } = auth(); 9 | const body = await req.json(); 10 | 11 | const { name } = body; 12 | 13 | if (!userId) { 14 | return new NextResponse("Unauthorized", { status: 401 }); 15 | } 16 | 17 | if (!name) { 18 | return new NextResponse("Name is required", { status: 400 }); 19 | } 20 | 21 | const store = await prismadb.store.create({ 22 | data: { 23 | name, 24 | userId, 25 | }, 26 | }); 27 | return NextResponse.json(store); 28 | } catch (err) { 29 | console.log("[STORES_POST]", err); 30 | return new NextResponse("Internal error", { status: 500 }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /ecommerce-admin/app/api/webhook/route.ts: -------------------------------------------------------------------------------- 1 | import Stripe from "stripe"; 2 | import { headers } from "next/headers"; 3 | import { NextResponse } from "next/server"; 4 | 5 | import { stripe } from "@/lib/stripe"; 6 | import prismadb from "@/lib/prismadb"; 7 | 8 | export async function POST(req: Request) { 9 | const body = await req.text(); 10 | const signature = headers().get( 11 | "Stripe-Signature" 12 | ) as string; 13 | 14 | let event: Stripe.Event; 15 | 16 | try { 17 | event = stripe.webhooks.constructEvent( 18 | body, 19 | signature, 20 | process.env.STRIPE_WEBHOOK_SECRET! 21 | ); 22 | } catch (error: any) { 23 | return new NextResponse( 24 | `Webhook Error: ${error.message}`, 25 | { status: 400 } 26 | ); 27 | } 28 | 29 | const session = event.data 30 | .object as Stripe.Checkout.Session; 31 | const address = session?.customer_details?.address; 32 | 33 | const addressComponents = [ 34 | address?.line1, 35 | address?.line2, 36 | address?.city, 37 | address?.state, 38 | address?.postal_code, 39 | address?.country, 40 | ]; 41 | 42 | const addressString = addressComponents 43 | .filter((c) => c !== null) 44 | .join(", "); 45 | 46 | if (event.type === "checkout.session.completed") { 47 | try { 48 | const order = await prismadb.order.update({ 49 | where: { 50 | id: session?.metadata?.orderId, 51 | }, 52 | data: { 53 | isPaid: true, 54 | address: addressString, 55 | phone: session?.customer_details?.phone || "", 56 | }, 57 | include: { 58 | orderItems: true, 59 | }, 60 | }); 61 | 62 | const variantUpdates = order.orderItems.map( 63 | async (orderItem) => { 64 | const variant = await prismadb.variant.findUnique( 65 | { 66 | where: { id: orderItem.variantId }, 67 | } 68 | ); 69 | 70 | if (!variant) { 71 | throw new Error( 72 | `Variant with id ${orderItem.variantId} not found.` 73 | ); 74 | } 75 | 76 | if (variant.inStock < orderItem.quantity) { 77 | throw new Error( 78 | `Not enough items in stock for variant id ${orderItem.variantId}.` 79 | ); 80 | } 81 | 82 | const updatedVariant = 83 | await prismadb.variant.update({ 84 | where: { id: variant.id }, 85 | data: { 86 | inStock: 87 | variant.inStock - orderItem.quantity, 88 | }, 89 | }); 90 | 91 | return updatedVariant; 92 | } 93 | ); 94 | 95 | await Promise.all(variantUpdates); 96 | } catch (error: any) { 97 | console.error("Error in checkout session:", error); 98 | } 99 | } 100 | 101 | return new NextResponse(null, { status: 200 }); 102 | } 103 | -------------------------------------------------------------------------------- /ecommerce-admin/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Repith/ecommerce-project/1c07ae8ffe6bb1f8c641a0410e08fdfeb375182a/ecommerce-admin/app/favicon.ico -------------------------------------------------------------------------------- /ecommerce-admin/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | html, 6 | body, 7 | :root { 8 | height: 100%; 9 | } 10 | 11 | 12 | @layer base { 13 | :root { 14 | --background: 0 0% 100%; 15 | --foreground: 240 10% 3.9%; 16 | --card: 0 0% 100%; 17 | --card-foreground: 240 10% 3.9%; 18 | --popover: 0 0% 100%; 19 | --popover-foreground: 240 10% 3.9%; 20 | --primary: 346.8 77.2% 49.8%; 21 | --primary-foreground: 355.7 100% 97.3%; 22 | --secondary: 240 4.8% 95.9%; 23 | --secondary-foreground: 240 5.9% 10%; 24 | --muted: 240 4.8% 95.9%; 25 | --muted-foreground: 240 3.8% 46.1%; 26 | --accent: 240 4.8% 95.9%; 27 | --accent-foreground: 240 5.9% 10%; 28 | --destructive: 0 84.2% 60.2%; 29 | --destructive-foreground: 0 0% 98%; 30 | --border: 240 5.9% 90%; 31 | --input: 240 5.9% 90%; 32 | --ring: 346.8 77.2% 49.8%; 33 | --radius: 0.75rem; 34 | } 35 | 36 | .dark { 37 | --background: 20 14.3% 4.1%; 38 | --foreground: 0 0% 95%; 39 | --card: 24 9.8% 10%; 40 | --card-foreground: 0 0% 95%; 41 | --popover: 0 0% 9%; 42 | --popover-foreground: 0 0% 95%; 43 | --primary: 346.8 77.2% 49.8%; 44 | --primary-foreground: 355.7 100% 97.3%; 45 | --secondary: 240 3.7% 15.9%; 46 | --secondary-foreground: 0 0% 98%; 47 | --muted: 0 0% 15%; 48 | --muted-foreground: 240 5% 64.9%; 49 | --accent: 12 6.5% 15.1%; 50 | --accent-foreground: 0 0% 98%; 51 | --destructive: 0 62.8% 30.6%; 52 | --destructive-foreground: 0 85.7% 97.3%; 53 | --border: 240 3.7% 15.9%; 54 | --input: 240 3.7% 15.9%; 55 | --ring: 346.8 77.2% 49.8%; 56 | } 57 | } 58 | 59 | 60 | @layer base { 61 | * { 62 | @apply border-border; 63 | } 64 | body { 65 | @apply bg-background text-foreground; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /ecommerce-admin/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { ClerkProvider } from "@clerk/nextjs"; 2 | import { Inter } from "next/font/google"; 3 | 4 | import { ModalProvider } from "@/providers/modal-provider"; 5 | import { ToastProvider } from "@/providers/toast-provider"; 6 | import { ThemeProvider } from "@/providers/theme-provider"; 7 | 8 | import "./globals.css"; 9 | 10 | const inter = Inter({ subsets: ["latin"] }); 11 | 12 | export const metadata = { 13 | title: "Dashboard", 14 | description: "E-Commerce Dashboard", 15 | }; 16 | 17 | export default async function RootLayout({ 18 | children, 19 | }: { 20 | children: React.ReactNode; 21 | }) { 22 | return ( 23 | 24 | 25 | 26 | 31 | 32 | 33 | {children} 34 | 35 | 36 | 37 | 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /ecommerce-admin/app/loading.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Loader } from "@/components/ui/loader"; 4 | 5 | const Loading = () => { 6 | return ( 7 |
8 | 9 |
10 | ); 11 | }; 12 | 13 | export default Loading; 14 | -------------------------------------------------------------------------------- /ecommerce-admin/app/types/reset.d.ts: -------------------------------------------------------------------------------- 1 | import "@total-typescript/ts-reset"; 2 | -------------------------------------------------------------------------------- /ecommerce-admin/app/types/types.ts: -------------------------------------------------------------------------------- 1 | export interface Billboard { 2 | id: string; 3 | label: string; 4 | imageUrl: string; 5 | } 6 | 7 | export interface Category { 8 | id: string; 9 | name: string; 10 | billboard: Billboard; 11 | } 12 | 13 | export interface CartItem { 14 | product: Product; 15 | variant: Variant; 16 | quantity: number; 17 | } 18 | export interface Variant { 19 | id: string; 20 | colorId: string; 21 | sizeId: string; 22 | inStock: number; 23 | } 24 | 25 | export interface Product { 26 | id: string; 27 | category: Category; 28 | name: string; 29 | price: number; 30 | isFeatured: boolean; 31 | images: Image[]; 32 | description: string; 33 | variants: Variant[]; 34 | } 35 | 36 | export interface Size { 37 | id: string; 38 | name: string; 39 | value: string; 40 | } 41 | 42 | export interface Color { 43 | id: string; 44 | name: string; 45 | value: string; 46 | } 47 | 48 | export interface Image { 49 | id: string; 50 | url: string; 51 | } 52 | -------------------------------------------------------------------------------- /ecommerce-admin/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true 11 | }, 12 | "aliases": { 13 | "components": "@/components", 14 | "utils": "@/lib/utils" 15 | } 16 | } -------------------------------------------------------------------------------- /ecommerce-admin/components/main-nav.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Link from "next/link"; 4 | import { 5 | useParams, 6 | usePathname, 7 | useRouter, 8 | } from "next/navigation"; 9 | import { 10 | AppWindow, 11 | Box, 12 | Copy, 13 | LayoutDashboard, 14 | Palette, 15 | Ruler, 16 | Settings, 17 | ShoppingCart, 18 | } from "lucide-react"; 19 | 20 | import { cn } from "@/lib/utils"; 21 | 22 | export function MainNav({ 23 | className, 24 | ...props 25 | }: React.HTMLAttributes) { 26 | const pathname = usePathname(); 27 | const params = useParams(); 28 | const router = useRouter(); 29 | 30 | const routes = [ 31 | { 32 | href: `/${params.storeId}`, 33 | label: "Overview", 34 | active: pathname === `/${params.storeId}`, 35 | icon: , 36 | }, 37 | { 38 | href: `/${params.storeId}/billboards`, 39 | label: "Billboards", 40 | active: pathname === `/${params.storeId}/billboards`, 41 | icon: , 42 | }, 43 | { 44 | href: `/${params.storeId}/categories`, 45 | label: "Categories", 46 | active: pathname === `/${params.storeId}/categories`, 47 | icon: , 48 | }, 49 | { 50 | href: `/${params.storeId}/sizes`, 51 | label: "Sizes", 52 | active: pathname === `/${params.storeId}/sizes`, 53 | icon: , 54 | }, 55 | { 56 | href: `/${params.storeId}/colors`, 57 | label: "Colors", 58 | active: pathname === `/${params.storeId}/colors`, 59 | icon: , 60 | }, 61 | { 62 | href: `/${params.storeId}/products`, 63 | label: "Products", 64 | active: pathname === `/${params.storeId}/products`, 65 | icon: , 66 | }, 67 | { 68 | href: `/${params.storeId}/orders`, 69 | label: "Orders", 70 | active: pathname === `/${params.storeId}/orders`, 71 | icon: , 72 | }, 73 | { 74 | href: `/${params.storeId}/settings`, 75 | label: "Settings", 76 | active: pathname === `/${params.storeId}/settings`, 77 | icon: , 78 | }, 79 | ]; 80 | 81 | return ( 82 | 104 | ); 105 | } 106 | -------------------------------------------------------------------------------- /ecommerce-admin/components/modals/alert-modal.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Modal } from "@/components/ui/modal"; 4 | import { Button } from "@/components/ui/button"; 5 | import { MountedCheck } from "@/lib/mounted-check"; 6 | 7 | interface AlertModalProps { 8 | isOpen: boolean; 9 | onClose: () => void; 10 | onConfirm: () => void; 11 | loading: boolean; 12 | } 13 | 14 | export const AlertModal: React.FC = ({ 15 | isOpen, 16 | onClose, 17 | onConfirm, 18 | loading, 19 | }) => { 20 | return ( 21 | 22 | 28 |
29 | 32 | 35 |
36 |
37 |
38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /ecommerce-admin/components/modals/store-modal.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as z from "zod"; 4 | import axios from "axios"; 5 | import { useForm } from "react-hook-form"; 6 | import { zodResolver } from "@hookform/resolvers/zod"; 7 | import { useState } from "react"; 8 | import { toast } from "react-hot-toast"; 9 | 10 | import { Modal } from "@/components/ui/modal"; 11 | import { useStoreModal } from "@/hooks/use-store-model"; 12 | import { Input } from "@/components/ui/input"; 13 | import { Button } from "@/components//ui/button"; 14 | import { 15 | Form, 16 | FormItem, 17 | FormLabel, 18 | FormField, 19 | FormControl, 20 | FormMessage, 21 | } from "@/components/ui/form"; 22 | 23 | const formSchema = z.object({ 24 | name: z.string().min(1), 25 | }); 26 | 27 | export const StoreModal = () => { 28 | const storeModal = useStoreModal(); 29 | 30 | const [loading, setLoading] = useState(false); 31 | 32 | const form = useForm>({ 33 | resolver: zodResolver(formSchema), 34 | defaultValues: { 35 | name: "", 36 | }, 37 | }); 38 | 39 | const onSubmit = async (values: z.infer) => { 40 | try { 41 | setLoading(true); 42 | 43 | const response = await axios.post("/api/stores", values); 44 | 45 | //Complete refresh on page 46 | window.location.assign(`/${response.data.id}`); 47 | //store from response will be 100% loaded to database 48 | } catch (error) { 49 | toast.error("Something went wrong."); 50 | } finally { 51 | setLoading(false); 52 | } 53 | }; 54 | 55 | return ( 56 | 62 |
63 |
64 |
65 | 66 | ( 70 | 71 | Name 72 | 73 | 78 | 79 | 80 | 81 | )} 82 | /> 83 |
84 | 91 | 94 |
95 | 96 | 97 |
98 |
99 |
100 | ); 101 | }; 102 | -------------------------------------------------------------------------------- /ecommerce-admin/components/modals/use-category-modal.tsx: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | 3 | interface useCategoryModalStore { 4 | isOpen: boolean; 5 | isEdit: boolean; 6 | editId?: string; 7 | onOpen: () => void; 8 | onEdit: (id: string) => void; 9 | onClose: () => void; 10 | } 11 | 12 | export const useCategoryModal = create((set) => ({ 13 | isOpen: false, 14 | isEdit: false, 15 | editId: undefined, 16 | onOpen: () => set({ isOpen: true }), 17 | onEdit: (id: string) => set({ isOpen: true, isEdit: true, editId: id }), 18 | onClose: () => set({ isOpen: false, isEdit: false, editId: undefined }), 19 | })); 20 | -------------------------------------------------------------------------------- /ecommerce-admin/components/navbar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { UserButton } from "@clerk/nextjs"; 3 | import { useState } from "react"; 4 | 5 | import { Menu } from "lucide-react"; 6 | import { MainNav } from "@/components/main-nav"; 7 | import { ThemeToggle } from "@/components/theme-toggle"; 8 | import StoreSwitcher, { 9 | StoreSwitcherProps, 10 | } from "@/components/store-switcher"; 11 | import { 12 | Sheet, 13 | SheetContent, 14 | SheetTrigger, 15 | } from "@/components/ui/sheet"; 16 | import { Button } from "@/components/ui/button"; 17 | 18 | const Navbar = ({ items }: StoreSwitcherProps) => { 19 | const [open, setOpen] = useState(false); 20 | 21 | const onClick = () => { 22 | setOpen((prev) => !prev); 23 | }; 24 | 25 | return ( 26 |
27 |
28 | 29 | 30 | 36 | 37 | 41 |
42 | 43 |
44 | 45 |
46 |
47 |
48 |
49 |
50 | 51 | 52 |
53 |
54 |
55 |
56 | 57 | 58 |
59 |
60 |
61 | ); 62 | }; 63 | 64 | export default Navbar; 65 | -------------------------------------------------------------------------------- /ecommerce-admin/components/overview.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | Bar, 5 | BarChart, 6 | ResponsiveContainer, 7 | XAxis, 8 | YAxis, 9 | } from "recharts"; 10 | 11 | interface OverviewProps { 12 | data: any[]; 13 | } 14 | 15 | export const Overview: React.FC = ({ 16 | data, 17 | }) => { 18 | return ( 19 | 20 | 21 | 28 | `$${value}`} 34 | /> 35 | 40 | 41 | 42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /ecommerce-admin/components/recent-orders.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { formatDistanceToNow } from "date-fns"; 3 | 4 | import { Order, OrderItem, Product } from "@prisma/client"; 5 | 6 | import { Badge } from "@/components/ui/badge"; 7 | 8 | interface RecentOrderProps { 9 | data: (Order & { 10 | orderItems: (OrderItem & { 11 | product: Product; 12 | })[]; 13 | })[]; 14 | } 15 | 16 | const RecentOrders: React.FC = ({ 17 | data, 18 | }) => { 19 | return ( 20 |
21 | {data.map((order, index) => ( 22 |
23 | 26 |
27 |
28 | {order.orderItems.map((item, itemIndex) => ( 29 |
30 | {`${item.product.name} x ${item.quantity} `} 31 |
32 | ))} 33 |
34 |
35 | {formatDistanceToNow( 36 | new Date(order.createdAt) 37 | )}{" "} 38 | ago 39 |
40 |
41 | {order.isPaid ? ( 42 | order.isSent ? ( 43 | Sent 44 | ) : ( 45 | Pending 46 | ) 47 | ) : ( 48 | 49 | Processing 50 | 51 | )} 52 |
53 |
54 | 55 |
56 | ))} 57 |
58 | ); 59 | }; 60 | 61 | export default RecentOrders; 62 | -------------------------------------------------------------------------------- /ecommerce-admin/components/theme-toggle.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { Moon, Sun } from "lucide-react"; 5 | import { useTheme } from "next-themes"; 6 | 7 | import { Switch } from "@/components/ui/switch"; 8 | import { MountedCheck } from "@/lib/mounted-check"; 9 | 10 | export function ThemeToggle() { 11 | const { theme, setTheme } = useTheme(); 12 | 13 | const toggleTheme = () => { 14 | setTheme(theme === "light" ? "dark" : "light"); 15 | }; 16 | 17 | return ( 18 | 19 |
20 | {theme === "light" ? : } 21 | 26 |
27 |
28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /ecommerce-admin/components/ui/alert.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 alertVariants = cva( 7 | "relative w-full rounded-lg border p-4 [&:has(svg)]:pl-11 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", 8 | { 9 | variants: { 10 | variant: { 11 | default: "bg-background text-foreground", 12 | destructive: 13 | "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", 14 | }, 15 | }, 16 | defaultVariants: { 17 | variant: "default", 18 | }, 19 | } 20 | ); 21 | 22 | const Alert = React.forwardRef< 23 | HTMLDivElement, 24 | React.HTMLAttributes & VariantProps 25 | >(({ className, variant, ...props }, ref) => ( 26 |
32 | )); 33 | Alert.displayName = "Alert"; 34 | 35 | const AlertTitle = React.forwardRef< 36 | HTMLParagraphElement, 37 | React.HTMLAttributes 38 | >(({ className, ...props }, ref) => ( 39 |
44 | )); 45 | AlertTitle.displayName = "AlertTitle"; 46 | 47 | const AlertDescription = React.forwardRef< 48 | HTMLParagraphElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |
56 | )); 57 | AlertDescription.displayName = "AlertDescription"; 58 | 59 | export { Alert, AlertTitle, AlertDescription }; 60 | -------------------------------------------------------------------------------- /ecommerce-admin/components/ui/api-alert.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Copy, Server } from "lucide-react"; 4 | import { toast } from "react-hot-toast"; 5 | 6 | import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; 7 | import { Badge, BadgeProps } from "@/components/ui/badge"; 8 | import { Button } from "@/components/ui/button"; 9 | 10 | interface ApiAlertProps { 11 | title: string; 12 | description: string; 13 | variant: "public" | "admin"; 14 | } 15 | 16 | const textMap: Record = { 17 | public: "Public", 18 | admin: "Admin", 19 | }; 20 | 21 | const variantMap: Record = { 22 | public: "secondary", 23 | admin: "destructive", 24 | }; 25 | 26 | export const ApiAlert: React.FC = ({ 27 | title, 28 | description, 29 | variant = "public", 30 | }) => { 31 | const onCopy = () => { 32 | navigator.clipboard.writeText(description); 33 | toast.success("API Route coppied to the clippord"); 34 | }; 35 | 36 | return ( 37 | 38 | 39 | 40 | {title} 41 | {textMap[variant]} 42 | 43 | 44 | 45 | {description} 46 | 47 | 50 | 51 | 52 | ); 53 | }; 54 | -------------------------------------------------------------------------------- /ecommerce-admin/components/ui/api-list.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useOrigin } from "@/hooks/use-origin"; 4 | import { useParams } from "next/navigation"; 5 | import { ApiAlert } from "./api-alert"; 6 | 7 | interface ApiListProps { 8 | entityName: string; 9 | entityIdName: string; 10 | } 11 | 12 | export const ApiList: React.FC = ({ 13 | entityName, 14 | entityIdName, 15 | }) => { 16 | const origin = useOrigin(); 17 | const params = useParams(); 18 | 19 | const baseUrl = `${origin}/api/${params.storeId}`; 20 | 21 | return ( 22 | <> 23 | 28 | 33 | 38 | 43 | 48 | 49 | ); 50 | }; 51 | -------------------------------------------------------------------------------- /ecommerce-admin/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-full 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 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 hover:bg-destructive/80", 17 | outline: "text-foreground", 18 | success: 19 | "border-transparent bg-green-500 text-primary-foreground hover:bg-primary/80", 20 | warning: 21 | "border-transparent bg-amber-300 text-primary-foreground hover:bg-primary/80", 22 | }, 23 | }, 24 | defaultVariants: { 25 | variant: "default", 26 | }, 27 | } 28 | ); 29 | 30 | export interface BadgeProps 31 | extends React.HTMLAttributes, 32 | VariantProps {} 33 | 34 | function Badge({ className, variant, ...props }: BadgeProps) { 35 | return ( 36 |
37 | ); 38 | } 39 | 40 | export { Badge, badgeVariants }; 41 | -------------------------------------------------------------------------------- /ecommerce-admin/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 rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | }, 22 | size: { 23 | default: "h-10 px-4 py-2", 24 | sm: "h-9 rounded-md px-3", 25 | lg: "h-11 rounded-md px-8", 26 | icon: "h-10 w-10", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | } 34 | ); 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean; 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : "button"; 45 | return ( 46 | 51 | ); 52 | } 53 | ); 54 | Button.displayName = "Button"; 55 | 56 | export { Button, buttonVariants }; 57 | -------------------------------------------------------------------------------- /ecommerce-admin/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 | HTMLParagraphElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |

44 | )) 45 | CardTitle.displayName = "CardTitle" 46 | 47 | const CardDescription = React.forwardRef< 48 | HTMLParagraphElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |

56 | )) 57 | CardDescription.displayName = "CardDescription" 58 | 59 | const CardContent = React.forwardRef< 60 | HTMLDivElement, 61 | React.HTMLAttributes 62 | >(({ className, ...props }, ref) => ( 63 |

64 | )) 65 | CardContent.displayName = "CardContent" 66 | 67 | const CardFooter = React.forwardRef< 68 | HTMLDivElement, 69 | React.HTMLAttributes 70 | >(({ className, ...props }, ref) => ( 71 |
76 | )) 77 | CardFooter.displayName = "CardFooter" 78 | 79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 80 | -------------------------------------------------------------------------------- /ecommerce-admin/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 | -------------------------------------------------------------------------------- /ecommerce-admin/components/ui/heading.tsx: -------------------------------------------------------------------------------- 1 | interface HeadingProps { 2 | title: string; 3 | description: string; 4 | } 5 | 6 | export const Heading: React.FC = ({ title, description }) => { 7 | return ( 8 |
9 |

{title}

10 |

{description}

11 |
12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /ecommerce-admin/components/ui/image-upload.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Image from "next/image"; 4 | import { useEffect, useState } from "react"; 5 | import { CldUploadWidget } from "next-cloudinary"; 6 | import { ImagePlus, Trash } from "lucide-react"; 7 | 8 | import { Button } from "@/components/ui/button"; 9 | import { MountedCheck } from "@/lib/mounted-check"; 10 | 11 | interface imageUploadProps { 12 | disabled?: boolean; 13 | onChange: (value: string) => void; 14 | onRemove: (value: string) => void; 15 | value: string[]; 16 | } 17 | 18 | const ImageUpload: React.FC = ({ 19 | disabled, 20 | onChange, 21 | onRemove, 22 | value, 23 | }) => { 24 | const onUpload = (result: any) => { 25 | onChange(result.info.secure_url); 26 | }; 27 | 28 | return ( 29 | 30 |
31 |
32 | {value.map((url) => ( 33 |
37 |
38 | 46 |
47 | Image 48 |
49 | ))} 50 |
51 | 52 | {({ open }) => { 53 | const onClick = () => { 54 | open(); 55 | }; 56 | return ( 57 | 66 | ); 67 | }} 68 | 69 |
70 |
71 | ); 72 | }; 73 | 74 | export default ImageUpload; 75 | -------------------------------------------------------------------------------- /ecommerce-admin/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ); 21 | } 22 | ); 23 | Input.displayName = "Input"; 24 | 25 | export { Input }; 26 | -------------------------------------------------------------------------------- /ecommerce-admin/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 | -------------------------------------------------------------------------------- /ecommerce-admin/components/ui/loader.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ClipLoader } from "react-spinners"; 4 | 5 | export const Loader = () => { 6 | return ; 7 | }; 8 | -------------------------------------------------------------------------------- /ecommerce-admin/components/ui/modal.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | Dialog, 5 | DialogContent, 6 | DialogTitle, 7 | DialogHeader, 8 | DialogDescription, 9 | } from "@/components/ui/dialog"; 10 | 11 | interface ModalProps { 12 | title: string; 13 | description: string; 14 | isOpen: boolean; 15 | onClose: () => void; 16 | children?: React.ReactNode; 17 | } 18 | 19 | export const Modal: React.FC = ({ 20 | title, 21 | description, 22 | isOpen, 23 | onClose, 24 | children, 25 | }) => { 26 | const onChange = (open: boolean) => { 27 | if (!open) { 28 | onClose(); 29 | } 30 | }; 31 | 32 | return ( 33 | 34 | 35 | 36 | {title} 37 | {description} 38 | 39 |
{children}
40 |
41 |
42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /ecommerce-admin/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 PopoverContent = React.forwardRef< 13 | React.ElementRef, 14 | React.ComponentPropsWithoutRef 15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( 16 | 17 | 27 | 28 | )); 29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName; 30 | 31 | export { Popover, PopoverTrigger, PopoverContent }; 32 | -------------------------------------------------------------------------------- /ecommerce-admin/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 | -------------------------------------------------------------------------------- /ecommerce-admin/components/ui/switch.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SwitchPrimitives from "@radix-ui/react-switch" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Switch = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | 25 | 26 | )) 27 | Switch.displayName = SwitchPrimitives.Root.displayName 28 | 29 | export { Switch } 30 | -------------------------------------------------------------------------------- /ecommerce-admin/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 | 48 | )); 49 | TableFooter.displayName = "TableFooter"; 50 | 51 | const TableRow = React.forwardRef< 52 | HTMLTableRowElement, 53 | React.HTMLAttributes 54 | >(({ className, ...props }, ref) => ( 55 | 63 | )); 64 | TableRow.displayName = "TableRow"; 65 | 66 | const TableHead = React.forwardRef< 67 | HTMLTableCellElement, 68 | React.ThHTMLAttributes 69 | >(({ className, ...props }, ref) => ( 70 |
78 | )); 79 | TableHead.displayName = "TableHead"; 80 | 81 | const TableCell = React.forwardRef< 82 | HTMLTableCellElement, 83 | React.TdHTMLAttributes 84 | >(({ className, ...props }, ref) => ( 85 | 90 | )); 91 | TableCell.displayName = "TableCell"; 92 | 93 | const TableCaption = React.forwardRef< 94 | HTMLTableCaptionElement, 95 | React.HTMLAttributes 96 | >(({ className, ...props }, ref) => ( 97 |
102 | )); 103 | TableCaption.displayName = "TableCaption"; 104 | 105 | export { 106 | Table, 107 | TableHeader, 108 | TableBody, 109 | TableFooter, 110 | TableHead, 111 | TableRow, 112 | TableCell, 113 | TableCaption, 114 | }; 115 | -------------------------------------------------------------------------------- /ecommerce-admin/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface TextareaProps 6 | extends React.TextareaHTMLAttributes {} 7 | 8 | const Textarea = React.forwardRef( 9 | ({ className, ...props }, ref) => { 10 | return ( 11 |