├── .eslintrc.json ├── .github ├── dependabot.yml └── workflows │ ├── dependency-review.yml │ └── label.yml ├── .gitignore ├── .templates ├── $$var_template │ └── $$var_filename.js ├── .editorconfig ├── template-sample-react-component │ ├── index.jsx │ └── index.scss └── template-sample │ └── index.js ├── .vscode └── settings.json ├── README.md ├── actions ├── get-graph-revenue.tsx ├── get-pending-amount.ts ├── get-sales-count.ts ├── get-stocks-count.ts └── get-total-revenue.ts ├── app ├── (auth) │ └── (routes) │ │ ├── layout.tsx │ │ ├── sign-in │ │ └── [[...sign-in]] │ │ │ └── page.tsx │ │ └── sign-up │ │ └── [[...sign-up]] │ │ └── page.tsx ├── (dashboard) │ └── [storeId] │ │ ├── (routes) │ │ ├── billboards │ │ │ ├── [billboardId] │ │ │ │ ├── components │ │ │ │ │ └── billboard-form.tsx │ │ │ │ └── page.tsx │ │ │ ├── components │ │ │ │ ├── Client.tsx │ │ │ │ ├── cell-action.tsx │ │ │ │ └── columns.tsx │ │ │ ├── loading.tsx │ │ │ └── page.tsx │ │ ├── categories │ │ │ ├── [categoryId] │ │ │ │ ├── components │ │ │ │ │ └── category-form.tsx │ │ │ │ └── page.tsx │ │ │ ├── components │ │ │ │ ├── Client.tsx │ │ │ │ ├── cell-action.tsx │ │ │ │ └── columns.tsx │ │ │ ├── loading.tsx │ │ │ └── page.tsx │ │ ├── colors │ │ │ ├── [colorId] │ │ │ │ ├── components │ │ │ │ │ └── color-form.tsx │ │ │ │ └── page.tsx │ │ │ ├── components │ │ │ │ ├── Client.tsx │ │ │ │ ├── cell-action.tsx │ │ │ │ └── columns.tsx │ │ │ ├── loading.tsx │ │ │ └── page.tsx │ │ ├── orders │ │ │ ├── components │ │ │ │ ├── Client.tsx │ │ │ │ └── columns.tsx │ │ │ ├── loading.tsx │ │ │ └── page.tsx │ │ ├── page.tsx │ │ ├── products │ │ │ ├── [productId] │ │ │ │ ├── components │ │ │ │ │ └── product-form.tsx │ │ │ │ └── page.tsx │ │ │ ├── components │ │ │ │ ├── Client.tsx │ │ │ │ ├── cell-action.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 │ │ │ ├── Client.tsx │ │ │ ├── cell-action.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 │ │ ├── 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 ├── components.json ├── components ├── dynamic-sticky-nav.tsx ├── mails │ ├── mail-display.tsx │ └── mail-list.tsx ├── main-nav.tsx ├── modals │ ├── alert-modal.tsx │ └── store-modal.tsx ├── navbar.tsx ├── overview.tsx ├── sideNav.tsx ├── store-switcher.tsx ├── theme-toggle.tsx └── ui │ ├── alert.tsx │ ├── api-alert.tsx │ ├── api-list.tsx │ ├── avatar.tsx │ ├── badge.tsx │ ├── button.tsx │ ├── calendar.tsx │ ├── card.tsx │ ├── checkbox.tsx │ ├── command.tsx │ ├── data-table-pagination.tsx │ ├── data-table.tsx │ ├── dialog.tsx │ ├── dropdown-menu.tsx │ ├── form.tsx │ ├── heading.tsx │ ├── image-upload.tsx │ ├── input.tsx │ ├── label.tsx │ ├── loader.tsx │ ├── modal.tsx │ ├── nav.tsx │ ├── popover.tsx │ ├── resizable.tsx │ ├── scroll-area.tsx │ ├── select.tsx │ ├── separator.tsx │ ├── switch.tsx │ ├── table.tsx │ ├── tabs.tsx │ ├── textarea.tsx │ └── tooltip.tsx ├── hooks ├── use-mail.ts ├── use-origin.tsx └── use-store-modal.tsx ├── lib ├── data.tsx ├── prismadb.ts ├── stripe.ts └── utils.ts ├── middleware.ts ├── next.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── prisma ├── migrations │ ├── 20240531104818_init_db_aiven │ │ └── migration.sql │ └── migration_lock.toml └── schema.prisma ├── providers ├── modal-provider.tsx ├── theme-provider.tsx └── toast-provider.tsx ├── public ├── next.svg └── vercel.svg ├── tailwind.config.js ├── template.config.js └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/dependency-review.yml: -------------------------------------------------------------------------------- 1 | # Dependency Review Action 2 | # 3 | # This Action will scan dependency manifest files that change as part of a Pull Request, surfacing known-vulnerable versions of the packages declared or updated in the PR. Once installed, if the workflow run is marked as required, PRs introducing known-vulnerable packages will be blocked from merging. 4 | # 5 | # Source repository: https://github.com/actions/dependency-review-action 6 | # Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement 7 | name: 'Dependency Review' 8 | on: [pull_request] 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | dependency-review: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: 'Checkout Repository' 18 | uses: actions/checkout@v3 19 | - name: 'Dependency Review' 20 | uses: actions/dependency-review-action@v3 21 | -------------------------------------------------------------------------------- /.github/workflows/label.yml: -------------------------------------------------------------------------------- 1 | # This workflow will triage pull requests and apply a label based on the 2 | # paths that are modified in the pull request. 3 | # 4 | # To use this workflow, you will need to set up a .github/labeler.yml 5 | # file with configuration. For more information, see: 6 | # https://github.com/actions/labeler 7 | 8 | name: Labeler 9 | on: [pull_request_target] 10 | 11 | jobs: 12 | label: 13 | 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: read 17 | pull-requests: write 18 | 19 | steps: 20 | - uses: actions/labeler@v4 21 | with: 22 | repo-token: "${{ secrets.GITHUB_TOKEN }}" 23 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.templates/$$var_template/$$var_filename.js: -------------------------------------------------------------------------------- 1 | export default function $$var_textInFile() { 2 | } 3 | -------------------------------------------------------------------------------- /.templates/.editorconfig: -------------------------------------------------------------------------------- 1 | # @see https://editorconfig-specification.readthedocs.io/en/latest/ 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | indent_style = space 11 | indent_size = 2 12 | charset = utf-8 13 | 14 | # 4 space indentation 15 | [*.py] 16 | indent_style = space 17 | indent_size = 4 18 | 19 | # Tab indentation (no size specified) 20 | [Makefile] 21 | indent_style = tab 22 | -------------------------------------------------------------------------------- /.templates/template-sample-react-component/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import classNames from "classnames/bind"; 3 | 4 | import styles from "./index.scss"; 5 | 6 | const cx = classNames.bind(styles); 7 | 8 | function __templateNameToPascalCase__() { 9 | return
Hello :)
; 10 | } 11 | 12 | export default __templateNameToPascalCase__; 13 | -------------------------------------------------------------------------------- /.templates/template-sample-react-component/index.scss: -------------------------------------------------------------------------------- 1 | .__templateNameToParamCase__ { 2 | display: inline-block; 3 | } 4 | -------------------------------------------------------------------------------- /.templates/template-sample/index.js: -------------------------------------------------------------------------------- 1 | export default function __templateNameToPascalCase__() { 2 | console.log("TemplateName -> __templateName__"); 3 | console.log("TemplateName to ParamCase -> __templateNameToParamCase__"); 4 | console.log("TemplateName to PascalCase -> __templateNameToPascalCase__"); 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "workbench.colorCustomizations": { 3 | "minimap.background": "#00000000", 4 | "scrollbar.shadow": "#00000000" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /actions/get-graph-revenue.tsx: -------------------------------------------------------------------------------- 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(); 34 | } 35 | 36 | // Adding the revenue for this order to the respective month 37 | monthlyRevenue[month] = (monthlyRevenue[month] || 0) + revenueForOrder; 38 | } 39 | 40 | // Converting the grouped data into the format expected by the graph 41 | const graphData: GraphData[] = [ 42 | { name: "Jan", total: 0 }, 43 | { name: "Feb", total: 0 }, 44 | { name: "Mar", total: 0 }, 45 | { name: "Apr", total: 0 }, 46 | { name: "May", total: 0 }, 47 | { name: "Jun", total: 0 }, 48 | { name: "Jul", total: 0 }, 49 | { name: "Aug", total: 0 }, 50 | { name: "Sep", total: 0 }, 51 | { name: "Oct", total: 0 }, 52 | { name: "Nov", total: 0 }, 53 | { name: "Dec", total: 0 }, 54 | ]; 55 | 56 | // Filling in the revenue data 57 | for (const month in monthlyRevenue) { 58 | graphData[parseInt(month)].total = monthlyRevenue[parseInt(month)]; 59 | } 60 | 61 | return graphData; 62 | }; 63 | -------------------------------------------------------------------------------- /actions/get-pending-amount.ts: -------------------------------------------------------------------------------- 1 | import prismadb from '@/lib/prismadb'; 2 | 3 | interface PendingAmountResult { 4 | numberOfUnpaidOrders: number; 5 | totalUnpaidAmount: number; 6 | } 7 | 8 | export const getPendingAmount = async (storeId: string): Promise => { 9 | const unpaidOrders = await prismadb.order.findMany({ 10 | where: { 11 | storeId, 12 | isPaid: false, 13 | }, 14 | include: { 15 | orderItems: { 16 | include: { 17 | product: true, 18 | }, 19 | }, 20 | }, 21 | }); 22 | 23 | const result: PendingAmountResult = unpaidOrders.reduce( 24 | (accumulatedResult, order) => { 25 | const orderTotal = order.orderItems.reduce((orderSum, item) => { 26 | return orderSum + item.product.price.toNumber(); 27 | }, 0); 28 | 29 | return { 30 | numberOfUnpaidOrders: accumulatedResult.numberOfUnpaidOrders + 1, 31 | totalUnpaidAmount: accumulatedResult.totalUnpaidAmount + orderTotal, 32 | }; 33 | }, 34 | { numberOfUnpaidOrders: 0, totalUnpaidAmount: 0 }, 35 | ); 36 | 37 | return result; 38 | }; 39 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /actions/get-stocks-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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/(auth)/(routes)/layout.tsx: -------------------------------------------------------------------------------- 1 | export default function AuthLayout({ 2 | children, 3 | }: { 4 | children: React.ReactNode; 5 | }) { 6 | return ( 7 |
8 | {children} 9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/(dashboard)/[storeId]/(routes)/billboards/[billboardId]/page.tsx: -------------------------------------------------------------------------------- 1 | import prismadb from "@/lib/prismadb"; 2 | import React from "react"; 3 | import { BillboardForm } from "./components/billboard-form"; 4 | 5 | const BillboardPage = async ({ 6 | params, 7 | }: { 8 | params: { billboardId: string }; 9 | }) => { 10 | const billboard = await prismadb.billboard.findFirst({ 11 | where: { 12 | id: params.billboardId, 13 | }, 14 | }); 15 | return ( 16 |
17 |
18 | 19 |
20 |
21 | ); 22 | }; 23 | 24 | export default BillboardPage; 25 | -------------------------------------------------------------------------------- /app/(dashboard)/[storeId]/(routes)/billboards/components/Client.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Button } from "@/components/ui/button"; 4 | import Heading from "@/components/ui/heading"; 5 | import { Separator } from "@/components/ui/separator"; 6 | import { Billboard } from "@prisma/client"; 7 | import { Plus } from "lucide-react"; 8 | import { useParams, useRouter } from "next/navigation"; 9 | import { BillboardColumn, columns } from "./columns"; 10 | import { DataTable } from "@/components/ui/data-table"; 11 | import { ApiList } from "@/components/ui/api-list"; 12 | 13 | interface BillboardClientProps { 14 | data: BillboardColumn[]; 15 | } 16 | 17 | const BillboardClient: React.FC = ({ data }) => { 18 | const router = useRouter(); 19 | const params = useParams(); 20 | return ( 21 | <> 22 |
23 | 27 | 28 | 33 |
34 | 35 | 36 | 40 | 41 | 42 | 43 | ); 44 | }; 45 | 46 | export default BillboardClient; 47 | -------------------------------------------------------------------------------- /app/(dashboard)/[storeId]/(routes)/billboards/components/cell-action.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Button } from "@/components/ui/button"; 4 | import { 5 | DropdownMenu, 6 | DropdownMenuContent, 7 | DropdownMenuItem, 8 | DropdownMenuLabel, 9 | DropdownMenuTrigger, 10 | } from "@/components/ui/dropdown-menu"; 11 | import { Copy, Edit, MoreHorizontal, Trash } from "lucide-react"; 12 | import { BillboardColumn } from "./columns"; 13 | import toast from "react-hot-toast"; 14 | import { useParams, useRouter } from "next/navigation"; 15 | import { useState } from "react"; 16 | import axios from "axios"; 17 | import { AlertModal } from "@/components/modals/alert-modal"; 18 | 19 | interface CellActionProps { 20 | data: BillboardColumn; 21 | } 22 | 23 | export const CellAction: React.FC = ({ data }) => { 24 | const router = useRouter(); 25 | const params = useParams(); 26 | 27 | // loading state and modal state 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 | // Delete store 40 | await axios.delete(`/api/${params.storeId}/billboards/${data.id}`); 41 | router.refresh(); 42 | toast.success("Billboard deleted successfully"); 43 | } catch (error) { 44 | toast.error( 45 | "Make sure you removed all categories using this billboard first. ", 46 | ); 47 | } finally { 48 | setLoading(false); 49 | setOpen(false); 50 | } 51 | }; 52 | return ( 53 | <> 54 | setOpen(false)} 57 | onConfirm={onDelete} 58 | loading={loading} 59 | /> 60 | 61 | 62 | 66 | 67 | 68 | Actions 69 | onCopy(data.id)}> 70 | 71 | Copy Id 72 | 73 | 74 | {/* Update */} 75 | 77 | router.push(`/${params.storeId}/billboards/${data.id}`) 78 | } 79 | > 80 | 81 | Update 82 | 83 | 84 | setOpen(true)}> 85 | 86 | Delete 87 | 88 | 89 | 90 | 91 | ); 92 | }; 93 | -------------------------------------------------------------------------------- /app/(dashboard)/[storeId]/(routes)/billboards/components/columns.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ColumnDef } from "@tanstack/react-table"; 4 | import { CellAction } from "./cell-action"; 5 | 6 | // This type is used to define the shape of our data. 7 | // You can use a Zod schema here if you want. 8 | export type BillboardColumn = { 9 | id: string; 10 | label: string; 11 | createdAt: string; 12 | }; 13 | 14 | export const columns: ColumnDef[] = [ 15 | { 16 | accessorKey: "label", 17 | header: "Label", 18 | }, 19 | { 20 | accessorKey: "createdAt", 21 | header: "Date", 22 | }, 23 | { 24 | id: "actions", 25 | cell: ({ row }) => , 26 | }, 27 | ]; 28 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/(dashboard)/[storeId]/(routes)/billboards/page.tsx: -------------------------------------------------------------------------------- 1 | import { format } from "date-fns"; 2 | 3 | import prismadb from "@/lib/prismadb"; 4 | import BillboardClient from "./components/Client"; 5 | 6 | const BillboardsPage = async ({ 7 | params, 8 | }: { 9 | params: { 10 | storeId: string; 11 | }; 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 = billboards.map(billboard => ({ 23 | id: billboard.id, 24 | label: billboard.label, 25 | createdAt: format(new Date(billboard.createdAt), "MMMM do ,yyyy"), 26 | })); 27 | 28 | return ( 29 |
30 |
31 | 32 |
33 |
34 | ); 35 | }; 36 | 37 | export default BillboardsPage; 38 | -------------------------------------------------------------------------------- /app/(dashboard)/[storeId]/(routes)/categories/[categoryId]/page.tsx: -------------------------------------------------------------------------------- 1 | import prismadb from "@/lib/prismadb"; 2 | import { CategoryForm } from "./components/category-form"; 3 | 4 | const CategoryPage = async ({ 5 | params, 6 | }: { 7 | params: { categoryId: string; storeId: string }; 8 | }) => { 9 | const category = await prismadb.category.findFirst({ 10 | where: { 11 | id: params.categoryId, 12 | }, 13 | }); 14 | 15 | // fetch all the billboards for the store 16 | 17 | const billboards = await prismadb.billboard.findMany({ 18 | where: { 19 | storeId: params.storeId, 20 | }, 21 | }); 22 | return ( 23 |
24 |
25 | 26 |
27 |
28 | ); 29 | }; 30 | 31 | export default CategoryPage; 32 | -------------------------------------------------------------------------------- /app/(dashboard)/[storeId]/(routes)/categories/components/Client.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Button } from "@/components/ui/button"; 4 | import Heading from "@/components/ui/heading"; 5 | import { Separator } from "@/components/ui/separator"; 6 | import { Billboard } from "@prisma/client"; 7 | import { Plus } from "lucide-react"; 8 | import { useParams, useRouter } from "next/navigation"; 9 | import { CategoryColumn, columns } from "./columns"; 10 | import { DataTable } from "@/components/ui/data-table"; 11 | import { ApiList } from "@/components/ui/api-list"; 12 | 13 | interface CategoryClientProps { 14 | data: CategoryColumn[]; 15 | } 16 | 17 | const CategoryClient: React.FC = ({ data }) => { 18 | const router = useRouter(); 19 | const params = useParams(); 20 | return ( 21 | <> 22 |
23 | 28 | 29 | 34 |
35 | 36 | 37 | 38 | 39 | 40 | 41 | ); 42 | }; 43 | 44 | export default CategoryClient; 45 | -------------------------------------------------------------------------------- /app/(dashboard)/[storeId]/(routes)/categories/components/cell-action.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Button } from "@/components/ui/button"; 4 | import { 5 | DropdownMenu, 6 | DropdownMenuContent, 7 | DropdownMenuItem, 8 | DropdownMenuLabel, 9 | DropdownMenuTrigger, 10 | } from "@/components/ui/dropdown-menu"; 11 | import { Copy, Edit, MoreHorizontal, Trash } from "lucide-react"; 12 | import { CategoryColumn } from "./columns"; 13 | import toast from "react-hot-toast"; 14 | import { useParams, useRouter } from "next/navigation"; 15 | import { useState } from "react"; 16 | import axios from "axios"; 17 | import { AlertModal } from "@/components/modals/alert-modal"; 18 | 19 | interface CellActionProps { 20 | data: CategoryColumn; 21 | } 22 | 23 | export const CellAction: React.FC = ({ data }) => { 24 | const router = useRouter(); 25 | const params = useParams(); 26 | 27 | // loading state and modal state 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("Category Id copied to clipboard."); 34 | }; 35 | 36 | const onDelete = async () => { 37 | try { 38 | setLoading(true); 39 | // Delete store 40 | await axios.delete(`/api/${params.storeId}/categories/${data.id}`); 41 | router.refresh(); 42 | 43 | toast.success("Category deleted successfully"); 44 | } catch (error) { 45 | toast.error( 46 | "Make sure you removed all products using this category first. ", 47 | ); 48 | } finally { 49 | setLoading(false); 50 | setOpen(false); 51 | } 52 | }; 53 | return ( 54 | <> 55 | setOpen(false)} 58 | onConfirm={onDelete} 59 | loading={loading} 60 | /> 61 | 62 | 63 | 67 | 68 | 69 | Actions 70 | onCopy(data.id)}> 71 | 72 | Copy Id 73 | 74 | 75 | {/* Update */} 76 | 78 | router.push(`/${params.storeId}/categories/${data.id}`) 79 | } 80 | > 81 | 82 | Update 83 | 84 | 85 | setOpen(true)}> 86 | 87 | Delete 88 | 89 | 90 | 91 | 92 | ); 93 | }; 94 | -------------------------------------------------------------------------------- /app/(dashboard)/[storeId]/(routes)/categories/components/columns.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ColumnDef } from "@tanstack/react-table"; 4 | import { CellAction } from "./cell-action"; 5 | 6 | // This type is used to define the shape of our data. 7 | // You can use a Zod schema here if you want. 8 | export type CategoryColumn = { 9 | id: string; 10 | name: string; 11 | billboardLabel: string; 12 | createdAt: string; 13 | }; 14 | 15 | export const columns: ColumnDef[] = [ 16 | { 17 | accessorKey: "name", 18 | header: "Name", 19 | }, 20 | { 21 | accessorKey: "billboard", 22 | header: "Billboard", 23 | cell: ({ row }) => row.original.billboardLabel, 24 | }, 25 | { 26 | accessorKey: "createdAt", 27 | header: "Date", 28 | }, 29 | { 30 | id: "actions", 31 | cell: ({ row }) => , 32 | }, 33 | ]; 34 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/(dashboard)/[storeId]/(routes)/categories/page.tsx: -------------------------------------------------------------------------------- 1 | import { format } from "date-fns"; 2 | 3 | import prismadb from "@/lib/prismadb"; 4 | import BillboardClient from "./components/Client"; 5 | import { CategoryColumn } from "./components/columns"; 6 | import CategoryClient from "./components/Client"; 7 | 8 | const CategoriesPage = async ({ 9 | params, 10 | }: { 11 | params: { 12 | storeId: string; 13 | }; 14 | }) => { 15 | const categories = await prismadb.category.findMany({ 16 | where: { 17 | storeId: params.storeId, 18 | }, 19 | include: { 20 | billboard: true, 21 | }, 22 | orderBy: { 23 | createdAt: "desc", 24 | }, 25 | }); 26 | 27 | const formattedCategories: CategoryColumn[] = categories.map(item => ({ 28 | id: item.id, 29 | name: item.name, 30 | billboardLabel: item.billboard.label, 31 | createdAt: format(new Date(item.createdAt), "MMMM do ,yyyy"), 32 | })); 33 | 34 | return ( 35 |
36 |
37 | 38 |
39 |
40 | ); 41 | }; 42 | 43 | export default CategoriesPage; 44 | -------------------------------------------------------------------------------- /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 ({ params }: { params: { colorId: string } }) => { 5 | const color = await prismadb.color.findUnique({ 6 | where: { 7 | id: params.colorId, 8 | }, 9 | }); 10 | return ( 11 |
12 |
13 | 14 |
15 |
16 | ); 17 | }; 18 | 19 | export default ColorPage; 20 | -------------------------------------------------------------------------------- /app/(dashboard)/[storeId]/(routes)/colors/components/Client.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ApiList } from "@/components/ui/api-list"; 4 | import { Button } from "@/components/ui/button"; 5 | import { DataTable } from "@/components/ui/data-table"; 6 | import Heading from "@/components/ui/heading"; 7 | import { Separator } from "@/components/ui/separator"; 8 | import { Plus } from "lucide-react"; 9 | import { useParams, useRouter } from "next/navigation"; 10 | 11 | import { ColorColumn, columns } from "./columns"; 12 | 13 | interface ColorClientProps { 14 | data: ColorColumn[]; 15 | } 16 | 17 | const ColorClient: React.FC = ({ data }) => { 18 | const router = useRouter(); 19 | const params = useParams(); 20 | return ( 21 | <> 22 |
23 | 27 | 28 | 31 |
32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | ); 40 | }; 41 | 42 | export default ColorClient; 43 | -------------------------------------------------------------------------------- /app/(dashboard)/[storeId]/(routes)/colors/components/cell-action.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { AlertModal } from "@/components/modals/alert-modal"; 4 | import { Button } from "@/components/ui/button"; 5 | import { 6 | DropdownMenu, 7 | DropdownMenuContent, 8 | DropdownMenuItem, 9 | DropdownMenuLabel, 10 | DropdownMenuTrigger, 11 | } from "@/components/ui/dropdown-menu"; 12 | import axios from "axios"; 13 | import { Copy, Edit, MoreHorizontal, Trash } from "lucide-react"; 14 | import { useParams, useRouter } from "next/navigation"; 15 | import { useState } from "react"; 16 | import toast from "react-hot-toast"; 17 | import { ColorColumn } from "./columns"; 18 | 19 | interface CellActionProps { 20 | data: ColorColumn; 21 | } 22 | 23 | export const CellAction: React.FC = ({ data }) => { 24 | const router = useRouter(); 25 | const params = useParams(); 26 | 27 | // loading state and modal state 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 | // Delete store 40 | await axios.delete(`/api/${params.storeId}/colors/${data.id}`); 41 | router.refresh(); 42 | toast.success("Color deleted successfully"); 43 | } catch (error) { 44 | toast.error( 45 | "Make sure you have deleted all the products with this color id before deleting this color.", 46 | ); 47 | } finally { 48 | setLoading(false); 49 | setOpen(false); 50 | } 51 | }; 52 | return ( 53 | <> 54 | setOpen(false)} 57 | onConfirm={onDelete} 58 | loading={loading} 59 | /> 60 | 61 | 62 | 66 | 67 | 68 | Actions 69 | onCopy(data.id)}> 70 | 71 | Copy Id 72 | 73 | 74 | {/* Update */} 75 | router.push(`/${params.storeId}/colors/${data.id}`)} 77 | > 78 | 79 | Update 80 | 81 | 82 | setOpen(true)}> 83 | 84 | Delete 85 | 86 | 87 | 88 | 89 | ); 90 | }; 91 | -------------------------------------------------------------------------------- /app/(dashboard)/[storeId]/(routes)/colors/components/columns.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ColumnDef } from "@tanstack/react-table"; 4 | import { CellAction } from "./cell-action"; 5 | import { SizeColumn } from "../../sizes/components/columns"; 6 | 7 | // This type is used to define the shape of our data. 8 | // You can use a Zod schema here if you want. 9 | export type ColorColumn = { 10 | id: string; 11 | name: string; 12 | value: string; 13 | createdAt: string; 14 | }; 15 | 16 | export const columns: ColumnDef[] = [ 17 | { 18 | accessorKey: "name", 19 | header: "Name", 20 | }, 21 | 22 | { 23 | accessorKey: "value", 24 | header: "Value", 25 | cell: ({ row }) => ( 26 |
27 | {row.original.value} 28 |
34 |
35 | ), 36 | }, 37 | { 38 | accessorKey: "createdAt", 39 | header: "Date", 40 | }, 41 | { 42 | id: "actions", 43 | cell: ({ row }) => , 44 | }, 45 | ]; 46 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/(dashboard)/[storeId]/(routes)/colors/page.tsx: -------------------------------------------------------------------------------- 1 | import { format } from "date-fns"; 2 | 3 | import prismadb from "@/lib/prismadb"; 4 | import ColorsClient from "./components/Client"; 5 | import { ColorColumn } from "./components/columns"; 6 | 7 | const ColorsPage = async ({ 8 | params, 9 | }: { 10 | params: { 11 | storeId: string; 12 | }; 13 | }) => { 14 | const colors = await prismadb.color.findMany({ 15 | where: { 16 | storeId: params.storeId, 17 | }, 18 | orderBy: { 19 | createdAt: "desc", 20 | }, 21 | }); 22 | 23 | const formattedColors: ColorColumn[] = colors.map(item => ({ 24 | id: item.id, 25 | name: item.name, 26 | value: item.value, 27 | createdAt: format(new Date(item.createdAt), "MMMM do ,yyyy"), 28 | })); 29 | 30 | return ( 31 |
32 |
33 | 34 |
35 |
36 | ); 37 | }; 38 | 39 | export default ColorsPage; 40 | -------------------------------------------------------------------------------- /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 | import { OrderColumn, columns } from './columns'; 7 | 8 | interface OrderClientProps { 9 | data: OrderColumn[]; 10 | } 11 | 12 | const OrderClient: React.FC = ({ data }) => { 13 | return ( 14 | <> 15 | 16 | 17 | 18 | 19 | 20 | ); 21 | }; 22 | 23 | export default OrderClient; 24 | -------------------------------------------------------------------------------- /app/(dashboard)/[storeId]/(routes)/orders/components/columns.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { ColumnDef } from '@tanstack/react-table'; 4 | import { 5 | DropdownMenu, 6 | DropdownMenuCheckboxItem, 7 | DropdownMenuContent, 8 | DropdownMenuItem, 9 | DropdownMenuLabel, 10 | DropdownMenuSeparator, 11 | DropdownMenuTrigger, 12 | } from '@/components/ui/dropdown-menu'; 13 | import { Button } from '@/components/ui/button'; 14 | import { MoreHorizontal, ArrowUpDown } from 'lucide-react'; 15 | import toast from 'react-hot-toast'; 16 | // This type is used to define the shape of our data. 17 | // You can use a Zod schema here if you want. 18 | export type OrderColumn = { 19 | id: string; 20 | phone: string; 21 | address: string; 22 | isPaid: boolean; 23 | totalPrice: string; 24 | products: string; 25 | createdAt: string; 26 | }; 27 | 28 | const onCopy = (id: string) => { 29 | navigator.clipboard.writeText(id); 30 | toast.success('Order ID copied to clipboard.'); 31 | }; 32 | export const columns: ColumnDef[] = [ 33 | // truuncate tghe long id in between 34 | { 35 | accessorKey: 'id', 36 | header: 'Order ID', 37 | cell: ({ row }) => { 38 | const order = row.original; 39 | 40 | return ( 41 |
42 | # {order.id.split('-')[0]} 43 |
44 | ); 45 | }, 46 | }, 47 | { 48 | accessorKey: 'products', 49 | header: 'Products', 50 | }, 51 | { 52 | accessorKey: 'phone', 53 | header: 'Phone', 54 | }, 55 | 56 | { 57 | accessorKey: 'createdAt', 58 | header: ({ column }) => { 59 | return ( 60 | 64 | ); 65 | }, 66 | }, 67 | 68 | { 69 | accessorKey: 'address', 70 | header: 'Address', 71 | }, 72 | { 73 | accessorKey: 'totalPrice', 74 | header: 'Order Amount', 75 | }, 76 | { 77 | accessorKey: 'isPaid', 78 | header: 'Status', 79 | cell: ({ row }) => { 80 | const order = row.original; 81 | 82 | return ( 83 |
84 |
90 | ); 91 | }, 92 | }, 93 | { 94 | id: 'actions', 95 | cell: ({ row }) => { 96 | const order = row.original; 97 | 98 | return ( 99 | 100 | 101 | 105 | 106 | 107 | Actions 108 | onCopy(order.id.split(' ')[0])}>Copy order ID 109 | 110 | View customer 111 | View payment details 112 | 113 | 114 | ); 115 | }, 116 | }, 117 | ]; 118 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/(dashboard)/[storeId]/(routes)/orders/page.tsx: -------------------------------------------------------------------------------- 1 | import { format } from 'date-fns'; 2 | 3 | import prismadb from '@/lib/prismadb'; 4 | import { rupeeFormatter } from '@/lib/utils'; 5 | 6 | import { OrderColumn } from './components/columns'; 7 | import OrderClient from './components/Client'; 8 | import { getTotalRevenue } from '@/actions/get-total-revenue'; 9 | import { getSalesCount } from '@/actions/get-sales-count'; 10 | import { getStockCount } from '@/actions/get-stocks-count'; 11 | import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; 12 | import Heading from '@/components/ui/heading'; 13 | 14 | import { Separator } from '@/components/ui/separator'; 15 | import { BaggageClaim, ChevronRight, CreditCard, DollarSign, HelpCircle, IndianRupee, Package2 } from 'lucide-react'; 16 | import { getPendingAmount } from '@/actions/get-pending-amount'; 17 | import Link from 'next/link'; 18 | 19 | const OrdersPage = async ({ params }: { params: { storeId: string } }) => { 20 | const totalRevenue = await getTotalRevenue(params.storeId); 21 | const { numberOfUnpaidOrders, totalUnpaidAmount } = await getPendingAmount(params.storeId); 22 | const salesCount = await getSalesCount(params.storeId); 23 | 24 | const orders = await prismadb.order.findMany({ 25 | where: { 26 | storeId: params.storeId, 27 | }, 28 | include: { 29 | orderItems: { 30 | include: { 31 | product: true, 32 | }, 33 | }, 34 | }, 35 | orderBy: { 36 | createdAt: 'desc', 37 | }, 38 | }); 39 | 40 | const formattedOrders: OrderColumn[] = orders.map((item) => ({ 41 | id: item.isPaid.valueOf() ? item.id : `${item.id} (Unpaid)`, 42 | 43 | phone: item.phone, 44 | address: item.address, 45 | products: item.orderItems.map((orderItem) => orderItem.product.name).join(', '), 46 | totalPrice: rupeeFormatter.format( 47 | item.orderItems.reduce((total, item) => { 48 | return total + Number(item.product.price); 49 | }, 0), 50 | ), 51 | isPaid: item.isPaid, 52 | createdAt: format(item.createdAt, 'MMMM do, yyyy'), 53 | })); 54 | 55 | return ( 56 |
57 |
58 | 59 | 60 | 61 |
62 | 63 | 64 | Online Orders 65 | 66 | 67 | 68 | 69 |
+{salesCount} orders
70 |
71 | 72 | Next Payout Date: 73 |
74 | Today , 4:00 PM 75 | 76 |
77 |
78 |
79 | 80 | 81 | Amount Processed 82 | 83 | 84 | 85 | 86 |
{rupeeFormatter.format(totalRevenue)}
87 |
88 |
89 | 90 | 91 | Amount Pending 92 | 93 | 94 | 95 | 96 |
{rupeeFormatter.format(totalUnpaidAmount)}
97 | 98 |
99 | 105 | 106 | 107 |
108 | 109 |
110 |
111 | ); 112 | }; 113 | 114 | export default OrdersPage; 115 | -------------------------------------------------------------------------------- /app/(dashboard)/[storeId]/(routes)/page.tsx: -------------------------------------------------------------------------------- 1 | import { getGraphRevenue } from '@/actions/get-graph-revenue'; 2 | import { getSalesCount } from '@/actions/get-sales-count'; 3 | import { getStockCount } from '@/actions/get-stocks-count'; 4 | import { getTotalRevenue } from '@/actions/get-total-revenue'; 5 | import { Overview } from '@/components/overview'; 6 | import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; 7 | import Heading from '@/components/ui/heading'; 8 | import { Separator } from '@/components/ui/separator'; 9 | import { rupeeFormatter } from '@/lib/utils'; 10 | 11 | import { CreditCard, DollarSign, Package2 } from 'lucide-react'; 12 | 13 | interface DashboardPageProps { 14 | params: { 15 | storeId: string; 16 | }; 17 | } 18 | 19 | const DashboardPage: React.FC = async ({ params }) => { 20 | const totalRevenue = await getTotalRevenue(params.storeId); 21 | const salesCount = await getSalesCount(params.storeId); 22 | const stockCount = await getStockCount(params.storeId); 23 | const graphRevenue = await getGraphRevenue(params.storeId); 24 | 25 | return ( 26 |
27 |
28 | 29 | 30 | 31 |
32 | 33 | 34 | Total Revenue 35 | 36 | 37 | 38 | 39 |
{rupeeFormatter.format(totalRevenue)}
40 |
41 |
42 | 43 | 44 | Sales 45 | 46 | 47 | 48 | 49 |
+{salesCount}
50 |
51 |
52 | 53 | 54 | Products in Stock 55 | 56 | 57 | 58 | 59 |
{stockCount}
60 |
61 |
62 |
63 | 64 | 65 | Overview 66 | 67 | 68 | 69 | 70 | 71 |
72 |
73 | ); 74 | }; 75 | 76 | export default DashboardPage; 77 | -------------------------------------------------------------------------------- /app/(dashboard)/[storeId]/(routes)/products/[productId]/page.tsx: -------------------------------------------------------------------------------- 1 | import prismadb from "@/lib/prismadb"; 2 | import React from "react"; 3 | import { ProductForm } from "./components/product-form"; 4 | 5 | const ProductPage = async ({ 6 | params, 7 | }: { 8 | params: { productId: string; storeId: string }; 9 | }) => { 10 | // Server code 11 | //fetch product -> pass product to form 12 | 13 | const product = await prismadb.product.findUnique({ 14 | where: { 15 | id: params.productId, 16 | }, 17 | include: { 18 | images: true, 19 | }, 20 | }); 21 | 22 | //fetch categories -> pass categories to form 23 | const categories = await prismadb.category.findMany({ 24 | where: { 25 | storeId: params.storeId, 26 | }, 27 | }); 28 | 29 | //Load sizes -> pass sizes to form 30 | const sizes = await prismadb.size.findMany({ 31 | where: { 32 | storeId: params.storeId, 33 | }, 34 | }); 35 | 36 | //Load colors -> pass colors to form 37 | const colors = await prismadb.color.findMany({ 38 | where: { 39 | storeId: params.storeId, 40 | }, 41 | }); 42 | 43 | return ( 44 |
45 |
46 | 52 |
53 |
54 | ); 55 | }; 56 | 57 | export default ProductPage; 58 | -------------------------------------------------------------------------------- /app/(dashboard)/[storeId]/(routes)/products/components/Client.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Button } from "@/components/ui/button"; 4 | import Heading from "@/components/ui/heading"; 5 | import { Separator } from "@/components/ui/separator"; 6 | import { Billboard } from "@prisma/client"; 7 | import { Plus } from "lucide-react"; 8 | import { useParams, useRouter } from "next/navigation"; 9 | import { ProductColumn, columns } from "./columns"; 10 | import { DataTable } from "@/components/ui/data-table"; 11 | import { ApiList } from "@/components/ui/api-list"; 12 | 13 | interface ProductClientProps { 14 | data: ProductColumn[]; 15 | } 16 | 17 | const ProductsClient: React.FC = ({ data }) => { 18 | const router = useRouter(); 19 | const params = useParams(); 20 | return ( 21 | <> 22 |
23 | 27 | 28 | 31 |
32 | 33 | 34 | 35 | 36 | 37 | 38 | ); 39 | }; 40 | 41 | export default ProductsClient; 42 | -------------------------------------------------------------------------------- /app/(dashboard)/[storeId]/(routes)/products/components/cell-action.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Button } from "@/components/ui/button"; 4 | import { 5 | DropdownMenu, 6 | DropdownMenuContent, 7 | DropdownMenuItem, 8 | DropdownMenuLabel, 9 | DropdownMenuTrigger, 10 | } from "@/components/ui/dropdown-menu"; 11 | import { Copy, Edit, MoreHorizontal, Trash } from "lucide-react"; 12 | 13 | import toast from "react-hot-toast"; 14 | import { useParams, useRouter } from "next/navigation"; 15 | import { useState } from "react"; 16 | import axios from "axios"; 17 | import { AlertModal } from "@/components/modals/alert-modal"; 18 | import { ProductColumn } from "./columns"; 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 | // loading state and modal state 29 | const [loading, setLoading] = useState(false); 30 | const [open, setOpen] = useState(false); 31 | 32 | const onCopy = (id: string) => { 33 | navigator.clipboard.writeText(id); 34 | toast.success("Product Id copied to clipboard."); 35 | }; 36 | 37 | const onDelete = async () => { 38 | try { 39 | setLoading(true); 40 | // Delete store 41 | await axios.delete(`/api/${params.storeId}/products/${data.id}`); 42 | router.refresh(); 43 | toast.success("Product deleted successfully"); 44 | } catch (error) { 45 | toast.error("Something went wrong. Please try again."); 46 | } finally { 47 | setLoading(false); 48 | setOpen(false); 49 | } 50 | }; 51 | return ( 52 | <> 53 | setOpen(false)} 56 | onConfirm={onDelete} 57 | loading={loading} 58 | /> 59 | 60 | 61 | 65 | 66 | 67 | Actions 68 | onCopy(data.id)}> 69 | 70 | Copy Id 71 | 72 | 73 | {/* Update */} 74 | 76 | router.push(`/${params.storeId}/products/${data.id}`) 77 | } 78 | > 79 | 80 | Update 81 | 82 | 83 | setOpen(true)}> 84 | 85 | Delete 86 | 87 | 88 | 89 | 90 | ); 91 | }; 92 | -------------------------------------------------------------------------------- /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 | 7 | export type ProductColumn = { 8 | id: string; 9 | name: string; 10 | price: string; 11 | category: string; 12 | size: string; 13 | color: string; 14 | createdAt: string; 15 | isFeatured: boolean; 16 | isArchived: boolean; 17 | }; 18 | 19 | export const columns: ColumnDef[] = [ 20 | { 21 | accessorKey: "name", 22 | header: "Name", 23 | }, 24 | { 25 | accessorKey: "isArchived", 26 | header: "Archived", 27 | }, 28 | { 29 | accessorKey: "isFeatured", 30 | header: "Featured", 31 | }, 32 | { 33 | accessorKey: "price", 34 | header: "Price", 35 | }, 36 | { 37 | accessorKey: "category", 38 | header: "Category", 39 | }, 40 | { 41 | accessorKey: "size", 42 | header: "Size", 43 | }, 44 | { 45 | accessorKey: "color", 46 | header: "Color", 47 | cell: ({ row }) => ( 48 |
49 | {row.original.color} 50 |
54 |
55 | ), 56 | }, 57 | { 58 | accessorKey: "createdAt", 59 | header: "Date", 60 | }, 61 | { 62 | id: "actions", 63 | cell: ({ row }) => , 64 | }, 65 | ]; 66 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/(dashboard)/[storeId]/(routes)/products/page.tsx: -------------------------------------------------------------------------------- 1 | import { format } from 'date-fns'; 2 | 3 | import prismadb from '@/lib/prismadb'; 4 | import { rupeeFormatter } from '@/lib/utils'; 5 | 6 | import { ProductColumn } from './components/columns'; 7 | import ProductsClient from './components/Client'; 8 | 9 | const ProductsPage = async ({ params }: { params: { storeId: string } }) => { 10 | const products = await prismadb.product.findMany({ 11 | where: { 12 | storeId: params.storeId, 13 | }, 14 | include: { 15 | category: true, 16 | size: true, 17 | color: true, 18 | }, 19 | orderBy: { 20 | createdAt: 'desc', 21 | }, 22 | }); 23 | 24 | // code transforms an array of products into a new array called formattedProducts, where each item in the new array has properties that are derived from the corresponding properties of the items in the original products array, with some additional formatting applied to certain properties. 25 | const formattedProducts: ProductColumn[] = products.map((item) => ({ 26 | id: item.id, 27 | name: item.name, 28 | isFeatured: item.isFeatured, 29 | isArchived: item.isArchived, 30 | price: rupeeFormatter.format(item.price.toNumber()), 31 | category: item.category.name, 32 | size: item.size.name, 33 | color: item.color.value, 34 | createdAt: format(item.createdAt, 'MMMM do, yyyy'), 35 | })); 36 | 37 | return ( 38 |
39 |
40 | 41 |
42 |
43 | ); 44 | }; 45 | 46 | export default ProductsPage; 47 | -------------------------------------------------------------------------------- /app/(dashboard)/[storeId]/(routes)/settings/components/settings-form.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { AlertModal } from "@/components/modals/alert-modal"; 4 | import { ApiAlert } from "@/components/ui/api-alert"; 5 | import { Button } from "@/components/ui/button"; 6 | import { 7 | Form, 8 | FormControl, 9 | FormField, 10 | FormItem, 11 | FormLabel, 12 | FormMessage, 13 | } from "@/components/ui/form"; 14 | import Heading from "@/components/ui/heading"; 15 | import { Input } from "@/components/ui/input"; 16 | import { Separator } from "@/components/ui/separator"; 17 | import { useOrigin } from "@/hooks/use-origin"; 18 | import { zodResolver } from "@hookform/resolvers/zod"; 19 | import axios from "axios"; 20 | import { Trash } from "lucide-react"; 21 | import { useParams, useRouter } from "next/navigation"; 22 | 23 | import { useState } from "react"; 24 | import { useForm } from "react-hook-form"; 25 | import toast from "react-hot-toast"; 26 | import { z } from "zod"; 27 | 28 | interface SettingsFormProps { 29 | initialData: any; 30 | } 31 | 32 | // formSchema -> SettingsFormValues -> SettingsForm using react hook form -> onSubmit -> update store 33 | const formSchema = z.object({ 34 | name: z.string().min(3).max(25).nonempty(), 35 | }); 36 | 37 | type SettingsFormValues = z.infer; 38 | 39 | export const SettingsForm: React.FC = ({ initialData }) => { 40 | const params = useParams(); 41 | const router = useRouter(); 42 | 43 | const [open, setOpen] = useState(false); 44 | const [loading, setLoading] = useState(false); 45 | const origin = useOrigin(); 46 | const form = useForm({ 47 | resolver: zodResolver(formSchema), 48 | defaultValues: initialData, 49 | }); 50 | 51 | // onDelete -> delete store -> refresh page -> redirect to root page (root layout will check if user has store and open createStore Modal if not found -> create store page will check if user has store and redirect to dashboard if found ) 52 | 53 | const onSubmit = async (data: SettingsFormValues) => { 54 | try { 55 | setLoading(true); 56 | // Update store 57 | await axios.patch(`/api/stores/${params.storeId}`, data); 58 | 59 | router.refresh(); 60 | toast.success("Store updated successfully"); 61 | } catch (error) { 62 | toast.error("Something went wrong"); 63 | } finally { 64 | setLoading(false); 65 | } 66 | }; 67 | 68 | const onDelete = async () => { 69 | try { 70 | setLoading(true); 71 | // Delete store 72 | await axios.delete(`/api/stores/${params.storeId}`); 73 | router.refresh(); 74 | 75 | router.push("/"); 76 | toast.success("Store deleted successfully"); 77 | } catch (error) { 78 | toast.error("Make sure you removed all products and categories first"); 79 | } finally { 80 | setLoading(false); 81 | setOpen(false); 82 | } 83 | }; 84 | 85 | return ( 86 | <> 87 | setOpen(false)} 90 | onConfirm={onDelete} 91 | loading={loading} 92 | /> 93 |
94 | 95 | 96 | 104 |
105 | 106 | 107 | {/* Form and spreading the form using react hook form */} 108 | 109 |
110 | 114 |
115 | ( 119 | 120 | Name 121 | 122 | 127 | 128 | 129 | 130 | )} 131 | /> 132 |
133 | 136 |
137 | 138 | 139 | 144 | 145 | ); 146 | }; 147 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/(dashboard)/[storeId]/(routes)/settings/page.tsx: -------------------------------------------------------------------------------- 1 | import prismadb from "@/lib/prismadb"; 2 | import { auth } from "@clerk/nextjs"; 3 | import { redirect } from "next/navigation"; 4 | 5 | import React from "react"; 6 | import { SettingsForm } from "./components/settings-form"; 7 | 8 | interface SettingsPageProps { 9 | params: { 10 | storeId: string; 11 | }; 12 | } 13 | const SettingsPage: React.FC = async ({ params }) => { 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 | return ( 31 |
32 |
33 | 34 |
35 |
36 | ); 37 | }; 38 | 39 | export default SettingsPage; 40 | -------------------------------------------------------------------------------- /app/(dashboard)/[storeId]/(routes)/sizes/[sizeId]/page.tsx: -------------------------------------------------------------------------------- 1 | import prismadb from "@/lib/prismadb"; 2 | import React from "react"; 3 | import { SizeForm } from "./components/size-form"; 4 | 5 | const SizePage = async ({ params }: { params: { sizeId: string } }) => { 6 | const size = await prismadb.size.findUnique({ 7 | where: { 8 | id: params.sizeId, 9 | }, 10 | }); 11 | return ( 12 |
13 |
14 | 15 |
16 |
17 | ); 18 | }; 19 | 20 | export default SizePage; 21 | -------------------------------------------------------------------------------- /app/(dashboard)/[storeId]/(routes)/sizes/components/Client.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Button } from "@/components/ui/button"; 4 | import Heading from "@/components/ui/heading"; 5 | import { Separator } from "@/components/ui/separator"; 6 | import { Billboard } from "@prisma/client"; 7 | import { Plus } from "lucide-react"; 8 | import { useParams, useRouter } from "next/navigation"; 9 | import { SizeColumn, columns } from "./columns"; 10 | import { DataTable } from "@/components/ui/data-table"; 11 | import { ApiList } from "@/components/ui/api-list"; 12 | 13 | interface SizesClientProps { 14 | data: SizeColumn[]; 15 | } 16 | 17 | const SizesClient: React.FC = ({ data }) => { 18 | const router = useRouter(); 19 | const params = useParams(); 20 | return ( 21 | <> 22 |
23 | 27 | 28 | 31 |
32 | 33 | 34 | 35 | 39 | 40 | 41 | 42 | ); 43 | }; 44 | 45 | export default SizesClient; 46 | -------------------------------------------------------------------------------- /app/(dashboard)/[storeId]/(routes)/sizes/components/cell-action.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Button } from "@/components/ui/button"; 4 | import { 5 | DropdownMenu, 6 | DropdownMenuContent, 7 | DropdownMenuItem, 8 | DropdownMenuLabel, 9 | DropdownMenuTrigger, 10 | } from "@/components/ui/dropdown-menu"; 11 | import { Copy, Edit, MoreHorizontal, Trash } from "lucide-react"; 12 | import { SizeColumn } from "./columns"; 13 | import toast from "react-hot-toast"; 14 | import { useParams, useRouter } from "next/navigation"; 15 | import { useState } from "react"; 16 | import axios from "axios"; 17 | import { AlertModal } from "@/components/modals/alert-modal"; 18 | 19 | interface CellActionProps { 20 | data: SizeColumn; 21 | } 22 | 23 | export const CellAction: React.FC = ({ data }) => { 24 | const router = useRouter(); 25 | const params = useParams(); 26 | 27 | // loading state and modal state 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 | // Delete store 40 | await axios.delete(`/api/${params.storeId}/sizes/${data.id}`); 41 | router.refresh(); 42 | toast.success("Size deleted successfully"); 43 | } catch (error) { 44 | toast.error("Make sure you removed all products using this size first. "); 45 | } finally { 46 | setLoading(false); 47 | setOpen(false); 48 | } 49 | }; 50 | return ( 51 | <> 52 | setOpen(false)} 55 | onConfirm={onDelete} 56 | loading={loading} 57 | /> 58 | 59 | 60 | 64 | 65 | 66 | Actions 67 | onCopy(data.id)}> 68 | 69 | Copy Id 70 | 71 | 72 | {/* Update */} 73 | router.push(`/${params.storeId}/sizes/${data.id}`)} 75 | > 76 | 77 | Update 78 | 79 | 80 | setOpen(true)}> 81 | 82 | Delete 83 | 6 84 | 85 | 86 | 87 | ); 88 | }; 89 | -------------------------------------------------------------------------------- /app/(dashboard)/[storeId]/(routes)/sizes/components/columns.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ColumnDef } from "@tanstack/react-table"; 4 | import { CellAction } from "./cell-action"; 5 | 6 | // This type is used to define the shape of our data. 7 | // You can use a Zod schema here if you want. 8 | export type SizeColumn = { 9 | id: string; 10 | name: string; 11 | value: string; 12 | createdAt: string; 13 | }; 14 | 15 | export const columns: ColumnDef[] = [ 16 | { 17 | accessorKey: "name", 18 | header: "Name", 19 | }, 20 | 21 | { 22 | accessorKey: "value", 23 | header: "Value", 24 | }, 25 | { 26 | accessorKey: "createdAt", 27 | header: "Date", 28 | }, 29 | { 30 | id: "actions", 31 | cell: ({ row }) => , 32 | }, 33 | ]; 34 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/(dashboard)/[storeId]/(routes)/sizes/page.tsx: -------------------------------------------------------------------------------- 1 | import { format } from "date-fns"; 2 | 3 | import prismadb from "@/lib/prismadb"; 4 | import SizesClient from "./components/Client"; 5 | import { SizeColumn } from "./components/columns"; 6 | 7 | const SizesPage = async ({ 8 | params, 9 | }: { 10 | params: { 11 | storeId: string; 12 | }; 13 | }) => { 14 | const sizes = await prismadb.size.findMany({ 15 | where: { 16 | storeId: params.storeId, 17 | }, 18 | orderBy: { 19 | createdAt: "desc", 20 | }, 21 | }); 22 | 23 | const formattedSizes: SizeColumn[] = sizes.map(item => ({ 24 | id: item.id, 25 | name: item.name, 26 | value: item.value, 27 | createdAt: format(new Date(item.createdAt), "MMMM do ,yyyy"), 28 | })); 29 | 30 | return ( 31 |
32 |
33 | 34 |
35 |
36 | ); 37 | }; 38 | 39 | export default SizesPage; 40 | -------------------------------------------------------------------------------- /app/(dashboard)/[storeId]/layout.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from 'next/navigation'; 2 | import { auth } from '@clerk/nextjs'; 3 | 4 | import Navbar from '@/components/navbar'; 5 | import prismadb from '@/lib/prismadb'; 6 | 7 | import { accounts, mails } from '@/lib/data'; 8 | import { cookies } from 'next/headers'; 9 | 10 | import { StickyDynamicNav } from '@/components/dynamic-sticky-nav'; 11 | 12 | export default async function DashboardLayout({ 13 | children, 14 | params, 15 | }: { 16 | children: React.ReactNode; 17 | params: { storeId: string }; 18 | }) { 19 | const { userId } = auth(); 20 | const layout = cookies().get('react-resizable-panels:layout'); 21 | const collapsed = cookies().get('react-resizable-panels:collapsed'); 22 | 23 | const defaultLayout = layout ? JSON.parse(layout.value) : undefined; 24 | const defaultCollapsed = collapsed ? JSON.parse(collapsed.value) : undefined; 25 | 26 | if (!userId) { 27 | redirect('/sign-in'); 28 | } 29 | 30 | // Checking if the store exists and belongs to the user before rendering the page 31 | const store = await prismadb.store.findFirst({ 32 | where: { 33 | id: params.storeId, 34 | userId, 35 | }, 36 | }); 37 | 38 | if (!store) { 39 | redirect('/'); 40 | } 41 | 42 | return ( 43 | <> 44 |
45 | 46 | {children} 47 |
48 |
49 | 56 |
57 | 58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /app/(root)/(routes)/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useStoreModal } from "@/hooks/use-store-modal"; 4 | import { useEffect } from "react"; 5 | 6 | const SetUpPage = () => { 7 | const onOpen = useStoreModal(state => state.onOpen); 8 | const isOpen = useStoreModal(state => state.isOpen); 9 | 10 | useEffect(() => { 11 | if (!isOpen) { 12 | onOpen(); 13 | } 14 | }, [isOpen, onOpen]); 15 | 16 | return null; 17 | }; 18 | 19 | export default SetUpPage; 20 | -------------------------------------------------------------------------------- /app/(root)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation"; 2 | import { auth } from "@clerk/nextjs"; 3 | 4 | // layout for the setup page (the page where the user creates their store) and show modal if the user don't have a store and if they do, redirect them to their store page (dashboard) instead of the setup page 5 | 6 | // flow will be from the sign in page to the setup page to the dashboard page 7 | // layout --> Root layout -----> check if the user exists( user is authenticated ) --> if not, redirect to sign in page ------> if user is authenticated then check if there is any store in the prisma db associated with the user if exists then redirect to the dashboard(of first store) ---> if no store is associated then navigate to create store modal 8 | 9 | import prismadb from "@/lib/prismadb"; 10 | 11 | export default async function SetupLayout({ 12 | children, 13 | }: { 14 | children: React.ReactNode; 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 | userId, 25 | }, 26 | }); 27 | 28 | if (store) { 29 | redirect(`/${store.id}`); 30 | } 31 | 32 | return <>{children}; 33 | } 34 | -------------------------------------------------------------------------------- /app/api/[storeId]/billboards/[billboardId]/route.ts: -------------------------------------------------------------------------------- 1 | import prismadb from "@/lib/prismadb"; 2 | import { auth } from "@clerk/nextjs"; 3 | import { NextResponse } from "next/server"; 4 | 5 | export async function GET( 6 | req: Request, 7 | { params }: { params: { billboardId: string } }, 8 | ) { 9 | try { 10 | if (!params.billboardId) { 11 | return new NextResponse("Billboard id is required", { status: 400 }); 12 | } 13 | 14 | const billboard = await prismadb.billboard.findUnique({ 15 | where: { 16 | id: params.billboardId, 17 | }, 18 | }); 19 | 20 | return NextResponse.json(billboard); 21 | } catch (error) { 22 | console.log("[BILLBOARD_GET]", error); 23 | return new NextResponse("Internal error", { status: 500 }); 24 | } 25 | } 26 | 27 | export async function PATCH( 28 | req: Request, 29 | { 30 | params, 31 | }: { 32 | params: { 33 | storeId: string; 34 | billboardId: string; 35 | }; 36 | }, 37 | ) { 38 | try { 39 | const { userId } = auth(); 40 | if (!userId) { 41 | return new NextResponse("Unauthorized", { status: 401 }); 42 | } 43 | const { billboardId } = params; 44 | const body = await req.json(); 45 | const { label, imageUrl } = body; 46 | 47 | if (!label) { 48 | return new NextResponse("Label is Required", { status: 400 }); 49 | } 50 | 51 | if (!imageUrl) { 52 | return new NextResponse("Image URL is Required", { status: 400 }); 53 | } 54 | 55 | if (!params.storeId) { 56 | return new NextResponse("Store ID is Required", { status: 400 }); 57 | } 58 | if (!params.billboardId) { 59 | return new NextResponse("Billboard ID is Required", { status: 400 }); 60 | } 61 | 62 | const storeByUserId = await prismadb.store.findFirst({ 63 | where: { 64 | id: params.storeId, 65 | userId, 66 | }, 67 | }); 68 | 69 | if (!storeByUserId) { 70 | return new NextResponse("Unauthorized", { status: 403 }); 71 | } 72 | 73 | // find and update billboard 74 | 75 | const billboard = await prismadb.billboard.updateMany({ 76 | where: { 77 | id: params.billboardId, 78 | }, 79 | data: { 80 | label, 81 | imageUrl, 82 | }, 83 | }); 84 | 85 | return NextResponse.json(billboard); 86 | } catch (error: any) { 87 | console.log(`[STORE_PATCH] `, error); 88 | return new NextResponse("Internal Server Error", { 89 | status: 500, 90 | }); 91 | } 92 | } 93 | 94 | export async function DELETE( 95 | req: Request, 96 | { params }: { params: { storeId: string; billboardId: string } }, 97 | ) { 98 | try { 99 | const { userId } = auth(); 100 | if (!userId) { 101 | return new NextResponse("Unauthorized", { status: 401 }); 102 | } 103 | const { storeId } = params; 104 | 105 | if (!storeId) { 106 | return new NextResponse("Store ID is Required", { status: 400 }); 107 | } 108 | 109 | if (!params.billboardId) { 110 | return new NextResponse("Billboard ID is Required", { status: 400 }); 111 | } 112 | 113 | const storeByUserId = await prismadb.store.findFirst({ 114 | where: { 115 | id: params.storeId, 116 | userId, 117 | }, 118 | }); 119 | 120 | if (!storeByUserId) { 121 | return new NextResponse("Unauthorized", { status: 403 }); 122 | } 123 | 124 | // find and update store 125 | 126 | const billboard = await prismadb.billboard.deleteMany({ 127 | where: { 128 | id: params.billboardId, 129 | }, 130 | }); 131 | return NextResponse.json(billboard); 132 | } catch (error: any) { 133 | console.log(`[BILLBOARDS_DELETE] `, error); 134 | return new NextResponse("Internal Server Error", { 135 | status: 500, 136 | }); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /app/api/[storeId]/billboards/route.ts: -------------------------------------------------------------------------------- 1 | import prismadb from "@/lib/prismadb"; 2 | import { auth } from "@clerk/nextjs"; 3 | import { NextResponse } from "next/server"; 4 | 5 | export async function POST( 6 | req: Request, 7 | { params }: { params: { storeId: string } }, 8 | ) { 9 | try { 10 | const { userId } = auth(); // we have access to the user id here that wants to create new store using our api 11 | 12 | const body = await req.json(); 13 | const { label, imageUrl } = body; 14 | 15 | if (!userId) { 16 | return new NextResponse("Unautheticated", { status: 401 }); 17 | } 18 | if (!label) { 19 | return new NextResponse("Label is required", { status: 400 }); 20 | } 21 | if (!imageUrl) { 22 | return new NextResponse("Image URL is required", { status: 400 }); 23 | } 24 | 25 | if (!params.storeId) { 26 | return new NextResponse("Store ID is required", { status: 400 }); 27 | } 28 | 29 | //! check if the storeId exists for the authenticated user 30 | 31 | const storeByUserId = await prismadb.store.findFirst({ 32 | where: { 33 | id: params.storeId, 34 | userId, 35 | }, 36 | }); 37 | 38 | if (!storeByUserId) { 39 | return new NextResponse("Unauthorized", { status: 403 }); 40 | } 41 | // create new billboard using prisma client instance and return the billboard data to the client 42 | 43 | const billboard = await prismadb.billboard.create({ 44 | data: { 45 | label, 46 | imageUrl, 47 | storeId: params.storeId, 48 | }, 49 | }); 50 | 51 | return NextResponse.json(billboard); 52 | } catch (error) { 53 | console.log(`[BILLBOARDS_POST] ${error}`, error); 54 | return new NextResponse("Internal Server Error", { status: 500 }); 55 | } 56 | } 57 | 58 | // Getting all the billboards for a store by storeId 59 | 60 | export async function GET( 61 | req: Request, 62 | { params }: { params: { storeId: string } }, 63 | ) { 64 | try { 65 | const { userId } = auth(); // we have access to the user id here that wants to create new store using our api 66 | 67 | if (!userId) { 68 | return new NextResponse("Unautheticated", { status: 401 }); 69 | } 70 | 71 | if (!params.storeId) { 72 | return new NextResponse("Store ID is required", { status: 400 }); 73 | } 74 | 75 | // get all the billboards for the storeId 76 | 77 | const billboards = await prismadb.billboard.findMany({ 78 | where: { 79 | storeId: params.storeId, 80 | }, 81 | }); 82 | 83 | return NextResponse.json(billboards); 84 | } catch (error) { 85 | console.log(`[BILLBOARDS_GET] ${error}`, error); 86 | return new NextResponse("Internal Server Error", { status: 500 }); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /app/api/[storeId]/categories/[categoryId]/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 GET( 7 | req: Request, 8 | { params }: { params: { categoryId: string } }, 9 | ) { 10 | try { 11 | if (!params.categoryId) { 12 | return new NextResponse("Category id is required", { status: 400 }); 13 | } 14 | 15 | const category = await prismadb.category.findUnique({ 16 | where: { 17 | id: params.categoryId, 18 | }, 19 | include: { 20 | billboard: true, 21 | }, 22 | }); 23 | 24 | return NextResponse.json(category); 25 | } catch (error) { 26 | console.log("[CATEGORY_GET]", error); 27 | return new NextResponse("Internal error", { status: 500 }); 28 | } 29 | } 30 | 31 | export async function DELETE( 32 | req: Request, 33 | { params }: { params: { categoryId: string; storeId: string } }, 34 | ) { 35 | try { 36 | const { userId } = auth(); 37 | 38 | if (!userId) { 39 | return new NextResponse("Unauthenticated", { status: 403 }); 40 | } 41 | 42 | if (!params.categoryId) { 43 | return new NextResponse("Category id is required", { status: 400 }); 44 | } 45 | 46 | const storeByUserId = await prismadb.store.findFirst({ 47 | where: { 48 | id: params.storeId, 49 | userId, 50 | }, 51 | }); 52 | 53 | if (!storeByUserId) { 54 | return new NextResponse("Unauthorized", { status: 405 }); 55 | } 56 | 57 | const category = await prismadb.category.delete({ 58 | where: { 59 | id: params.categoryId, 60 | }, 61 | }); 62 | 63 | return NextResponse.json(category); 64 | } catch (error) { 65 | console.log("[CATEGORY_DELETE]", error); 66 | return new NextResponse("Internal error", { status: 500 }); 67 | } 68 | } 69 | 70 | export async function PATCH( 71 | req: Request, 72 | { params }: { params: { categoryId: string; storeId: string } }, 73 | ) { 74 | try { 75 | const { userId } = auth(); 76 | 77 | const body = await req.json(); 78 | 79 | const { name, billboardId } = body; 80 | 81 | if (!userId) { 82 | return new NextResponse("Unauthenticated", { status: 403 }); 83 | } 84 | 85 | if (!billboardId) { 86 | return new NextResponse("Billboard ID is required", { status: 400 }); 87 | } 88 | 89 | if (!name) { 90 | return new NextResponse("Name is required", { status: 400 }); 91 | } 92 | 93 | if (!params.categoryId) { 94 | return new NextResponse("Category id is required", { status: 400 }); 95 | } 96 | 97 | const storeByUserId = await prismadb.store.findFirst({ 98 | where: { 99 | id: params.storeId, 100 | userId, 101 | }, 102 | }); 103 | 104 | if (!storeByUserId) { 105 | return new NextResponse("Unauthorized", { status: 405 }); 106 | } 107 | 108 | const category = await prismadb.category.update({ 109 | where: { 110 | id: params.categoryId, 111 | }, 112 | data: { 113 | name, 114 | billboardId, 115 | }, 116 | }); 117 | 118 | return NextResponse.json(category); 119 | } catch (error) { 120 | console.log("[CATEGORY_PATCH]", error); 121 | return new NextResponse("Internal error", { status: 500 }); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /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 | 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 { name, billboardId } = body; 16 | 17 | if (!userId) { 18 | return new NextResponse("Unauthenticated", { status: 403 }); 19 | } 20 | 21 | if (!name) { 22 | return new NextResponse("Name is required", { status: 400 }); 23 | } 24 | 25 | if (!billboardId) { 26 | return new NextResponse("Billboard ID is required", { status: 400 }); 27 | } 28 | 29 | if (!params.storeId) { 30 | return new NextResponse("Store id is required", { status: 400 }); 31 | } 32 | 33 | const storeByUserId = await prismadb.store.findFirst({ 34 | where: { 35 | id: params.storeId, 36 | userId, 37 | } 38 | }); 39 | 40 | if (!storeByUserId) { 41 | return new NextResponse("Unauthorized", { status: 405 }); 42 | } 43 | 44 | const category = await prismadb.category.create({ 45 | data: { 46 | name, 47 | billboardId, 48 | storeId: params.storeId, 49 | } 50 | }); 51 | 52 | return NextResponse.json(category); 53 | } catch (error) { 54 | console.log('[CATEGORIES_POST]', error); 55 | return new NextResponse("Internal error", { status: 500 }); 56 | } 57 | }; 58 | 59 | export async function GET( 60 | req: Request, 61 | { params }: { params: { storeId: string } } 62 | ) { 63 | try { 64 | if (!params.storeId) { 65 | return new NextResponse("Store id is required", { status: 400 }); 66 | } 67 | 68 | const categories = await prismadb.category.findMany({ 69 | where: { 70 | storeId: params.storeId 71 | } 72 | }); 73 | 74 | return NextResponse.json(categories); 75 | } catch (error) { 76 | console.log('[CATEGORIES_GET]', error); 77 | return new NextResponse("Internal error", { status: 500 }); 78 | } 79 | }; 80 | -------------------------------------------------------------------------------- /app/api/[storeId]/checkout/route.ts: -------------------------------------------------------------------------------- 1 | import Stripe from "stripe"; 2 | import { NextResponse } from "next/server"; 3 | 4 | import { stripe } from "@/lib/stripe"; 5 | import prismadb from "@/lib/prismadb"; 6 | 7 | const corsHeaders = { 8 | "Access-Control-Allow-Origin": "*", 9 | "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS", 10 | "Access-Control-Allow-Headers": "Content-Type, Authorization", 11 | }; 12 | 13 | export async function OPTIONS() { 14 | return NextResponse.json({}, { headers: corsHeaders }); 15 | } 16 | 17 | export async function POST( 18 | req: Request, 19 | { params }: { params: { storeId: string } }, 20 | ) { 21 | const { productIds } = await req.json(); 22 | 23 | if (!productIds || productIds.length === 0) { 24 | return new NextResponse("Product ids are required", { status: 400 }); 25 | } 26 | 27 | const products = await prismadb.product.findMany({ 28 | where: { 29 | id: { 30 | in: productIds, 31 | }, 32 | }, 33 | }); 34 | 35 | const line_items: Stripe.Checkout.SessionCreateParams.LineItem[] = []; 36 | 37 | products.forEach(product => { 38 | line_items.push({ 39 | quantity: 1, 40 | price_data: { 41 | currency: "USD", 42 | product_data: { 43 | name: product.name, 44 | }, 45 | unit_amount: product.price.toNumber() * 100, 46 | }, 47 | }); 48 | }); 49 | 50 | const order = await prismadb.order.create({ 51 | data: { 52 | storeId: params.storeId, 53 | isPaid: false, 54 | orderItems: { 55 | create: productIds.map((productId: string) => ({ 56 | product: { 57 | connect: { 58 | id: productId, 59 | }, 60 | }, 61 | })), 62 | }, 63 | }, 64 | }); 65 | 66 | const session = await stripe.checkout.sessions.create({ 67 | line_items, 68 | mode: "payment", 69 | billing_address_collection: "required", 70 | phone_number_collection: { 71 | enabled: true, 72 | }, 73 | success_url: `${process.env.FRONTEND_STORE_URL}/cart?success=1`, 74 | cancel_url: `${process.env.FRONTEND_STORE_URL}/cart?canceled=1`, 75 | metadata: { 76 | orderId: order.id, 77 | }, 78 | }); 79 | 80 | return NextResponse.json( 81 | { url: session.url }, 82 | { 83 | headers: corsHeaders, 84 | }, 85 | ); 86 | } 87 | -------------------------------------------------------------------------------- /app/api/[storeId]/colors/[colorId]/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 GET( 7 | req: Request, 8 | { params }: { params: { colorId: string } }, 9 | ) { 10 | try { 11 | if (!params.colorId) { 12 | return new NextResponse("Color id is required", { status: 400 }); 13 | } 14 | 15 | const color = await prismadb.color.findUnique({ 16 | where: { 17 | id: params.colorId, 18 | }, 19 | }); 20 | 21 | return NextResponse.json(color); 22 | } catch (error) { 23 | console.log("[COLOR_GET]", error); 24 | return new NextResponse("Internal error", { status: 500 }); 25 | } 26 | } 27 | 28 | export async function DELETE( 29 | req: Request, 30 | { params }: { params: { colorId: string; storeId: string } }, 31 | ) { 32 | try { 33 | const { userId } = auth(); 34 | 35 | if (!userId) { 36 | return new NextResponse("Unauthenticated", { status: 403 }); 37 | } 38 | 39 | if (!params.colorId) { 40 | return new NextResponse("Color id is required", { status: 400 }); 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", { status: 405 }); 52 | } 53 | 54 | const color = await prismadb.color.delete({ 55 | where: { 56 | id: params.colorId, 57 | }, 58 | }); 59 | 60 | return NextResponse.json(color); 61 | } catch (error) { 62 | console.log("[COLOR_DELETE]", error); 63 | return new NextResponse("Internal error", { status: 500 }); 64 | } 65 | } 66 | 67 | export async function PATCH( 68 | req: Request, 69 | { params }: { params: { colorId: string; storeId: string } }, 70 | ) { 71 | try { 72 | const { userId } = auth(); 73 | 74 | const body = await req.json(); 75 | 76 | const { name, value } = body; 77 | 78 | if (!userId) { 79 | return new NextResponse("Unauthenticated", { status: 403 }); 80 | } 81 | 82 | if (!name) { 83 | return new NextResponse("Name is required", { status: 400 }); 84 | } 85 | 86 | if (!value) { 87 | return new NextResponse("Value is required", { status: 400 }); 88 | } 89 | 90 | if (!params.colorId) { 91 | return new NextResponse("Color id is required", { status: 400 }); 92 | } 93 | 94 | const storeByUserId = await prismadb.store.findFirst({ 95 | where: { 96 | id: params.storeId, 97 | userId, 98 | }, 99 | }); 100 | 101 | if (!storeByUserId) { 102 | return new NextResponse("Unauthorized", { status: 405 }); 103 | } 104 | 105 | const color = await prismadb.color.update({ 106 | where: { 107 | id: params.colorId, 108 | }, 109 | data: { 110 | name, 111 | value, 112 | }, 113 | }); 114 | 115 | return NextResponse.json(color); 116 | } catch (error) { 117 | console.log("[COLOR_PATCH]", error); 118 | return new NextResponse("Internal error", { status: 500 }); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /app/api/[storeId]/colors/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | 3 | import prismadb from "@/lib/prismadb"; 4 | import { auth } from "@clerk/nextjs"; 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 { name, value } = body; 16 | 17 | if (!userId) { 18 | return new NextResponse("Unauthenticated", { status: 403 }); 19 | } 20 | 21 | if (!name) { 22 | return new NextResponse("Name is required", { status: 400 }); 23 | } 24 | 25 | if (!value) { 26 | return new NextResponse("Value is required", { status: 400 }); 27 | } 28 | 29 | if (!params.storeId) { 30 | return new NextResponse("Store id is required", { status: 400 }); 31 | } 32 | 33 | const storeByUserId = await prismadb.store.findFirst({ 34 | where: { 35 | id: params.storeId, 36 | userId, 37 | }, 38 | }); 39 | 40 | if (!storeByUserId) { 41 | return new NextResponse("Unauthorized", { status: 405 }); 42 | } 43 | 44 | const color = await prismadb.color.create({ 45 | data: { 46 | name, 47 | value, 48 | storeId: params.storeId, 49 | }, 50 | }); 51 | 52 | return NextResponse.json(color); 53 | } catch (error) { 54 | console.log("[COLORS_POST]", error); 55 | return new NextResponse("Internal error", { status: 500 }); 56 | } 57 | } 58 | 59 | export async function GET( 60 | req: Request, 61 | { params }: { params: { storeId: string } }, 62 | ) { 63 | try { 64 | if (!params.storeId) { 65 | return new NextResponse("Store id is required", { status: 400 }); 66 | } 67 | 68 | const colors = await prismadb.color.findMany({ 69 | where: { 70 | storeId: params.storeId, 71 | }, 72 | }); 73 | 74 | return NextResponse.json(colors); 75 | } catch (error) { 76 | console.log("[COLORS_GET]", error); 77 | return new NextResponse("Internal error", { status: 500 }); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /app/api/[storeId]/products/[productId]/route.ts: -------------------------------------------------------------------------------- 1 | import prismadb from "@/lib/prismadb"; 2 | import { auth } from "@clerk/nextjs"; 3 | import { NextResponse } from "next/server"; 4 | 5 | export async function GET( 6 | req: Request, 7 | { params }: { params: { productId: string } }, 8 | ) { 9 | try { 10 | if (!params.productId) { 11 | return new NextResponse("Product id is required", { status: 400 }); 12 | } 13 | 14 | // find and update store by id 15 | 16 | const product = await prismadb.product.findUnique({ 17 | where: { 18 | id: params.productId, 19 | }, 20 | 21 | include: { 22 | // include the relations to get the full data of the product 23 | category: true, 24 | color: true, 25 | size: true, 26 | images: true, 27 | }, 28 | }); 29 | 30 | return NextResponse.json(product); 31 | } catch (error) { 32 | console.log("[PRODUCT_GET]", error); 33 | return new NextResponse("Internal error", { status: 500 }); 34 | } 35 | } 36 | 37 | export async function PATCH( 38 | req: Request, 39 | { 40 | params, 41 | }: { 42 | params: { 43 | storeId: string; 44 | productId: string; 45 | }; 46 | }, 47 | ) { 48 | try { 49 | const { userId } = auth(); 50 | if (!userId) { 51 | return new NextResponse("Unauthorized", { status: 401 }); 52 | } 53 | const body = await req.json(); 54 | const { 55 | name, 56 | price, 57 | categoryId, 58 | colorId, 59 | sizeId, 60 | images, 61 | isFeatured, 62 | isArchived, 63 | } = body; 64 | 65 | const storeByUserId = await prismadb.store.findFirst({ 66 | where: { 67 | id: params.storeId, 68 | userId, 69 | }, 70 | }); 71 | 72 | if (!storeByUserId) { 73 | return new NextResponse("Unauthorized", { status: 403 }); 74 | } 75 | 76 | if (!name) { 77 | return new NextResponse("Name is required", { status: 400 }); 78 | } 79 | 80 | if (!categoryId) { 81 | return new NextResponse("Category Id is required", { status: 400 }); 82 | } 83 | 84 | if (!colorId) { 85 | return new NextResponse("Color Id is required", { status: 400 }); 86 | } 87 | 88 | if (!sizeId) { 89 | return new NextResponse("Size Id is required", { status: 400 }); 90 | } 91 | 92 | if (!price) { 93 | return new NextResponse("Price is required", { status: 400 }); 94 | } 95 | 96 | if (!images || images.length === 0) { 97 | return new NextResponse("Images are required", { status: 400 }); 98 | } 99 | 100 | if (!params.productId) { 101 | return new NextResponse("Product ID is required", { status: 400 }); 102 | } 103 | 104 | // General query to update the product 105 | await prismadb.product.update({ 106 | where: { 107 | id: params.productId, 108 | }, 109 | data: { 110 | name, 111 | price, 112 | categoryId, 113 | colorId, 114 | sizeId, 115 | images: { 116 | deleteMany: {}, 117 | }, 118 | isFeatured, 119 | isArchived, 120 | }, 121 | }); 122 | const product = await prismadb.product.update({ 123 | where: { 124 | id: params.productId, 125 | }, 126 | data: { 127 | images: { 128 | createMany: { 129 | data: [...images.map((image: { url: string }) => image)], 130 | }, 131 | }, 132 | }, 133 | }); 134 | 135 | return NextResponse.json(product); 136 | } catch (error: any) { 137 | console.log(`[PRODUCT_PATCH] `, error); 138 | return new NextResponse("Internal Server Error", { 139 | status: 500, 140 | }); 141 | } 142 | } 143 | 144 | export async function DELETE( 145 | req: Request, 146 | { params }: { params: { storeId: string; productId: string } }, 147 | ) { 148 | try { 149 | const { userId } = auth(); 150 | if (!userId) { 151 | return new NextResponse("Unauthorized", { status: 401 }); 152 | } 153 | const { storeId } = params; 154 | 155 | if (!storeId) { 156 | return new NextResponse("Store ID is Required", { status: 400 }); 157 | } 158 | 159 | if (!params.productId) { 160 | return new NextResponse("Product ID is Required", { status: 400 }); 161 | } 162 | 163 | const storeByUserId = await prismadb.store.findFirst({ 164 | where: { 165 | id: params.storeId, 166 | userId, 167 | }, 168 | }); 169 | 170 | if (!storeByUserId) { 171 | return new NextResponse("Unauthorized", { status: 403 }); 172 | } 173 | 174 | // find and update store 175 | 176 | const product = await prismadb.product.deleteMany({ 177 | where: { 178 | id: params.productId, 179 | }, 180 | }); 181 | return NextResponse.json(product); 182 | } catch (error: any) { 183 | console.log(`[PRODUCT_DELETE] `, error); 184 | return new NextResponse("Internal Server Error", { 185 | status: 500, 186 | }); 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /app/api/[storeId]/products/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 { name, price, categoryId, colorId, sizeId, images, isFeatured, isArchived } = body; 16 | 17 | if (!userId) { 18 | return new NextResponse("Unauthenticated", { status: 403 }); 19 | } 20 | 21 | if (!name) { 22 | return new NextResponse("Name is required", { status: 400 }); 23 | } 24 | 25 | if (!images || !images.length) { 26 | return new NextResponse("Images are required", { status: 400 }); 27 | } 28 | 29 | if (!price) { 30 | return new NextResponse("Price is required", { status: 400 }); 31 | } 32 | 33 | if (!categoryId) { 34 | return new NextResponse("Category id is required", { status: 400 }); 35 | } 36 | 37 | if (!colorId) { 38 | return new NextResponse("Color id is required", { status: 400 }); 39 | } 40 | 41 | if (!sizeId) { 42 | return new NextResponse("Size id is required", { status: 400 }); 43 | } 44 | 45 | if (!params.storeId) { 46 | return new NextResponse("Store id is required", { status: 400 }); 47 | } 48 | 49 | const storeByUserId = await prismadb.store.findFirst({ 50 | where: { 51 | id: params.storeId, 52 | userId 53 | } 54 | }); 55 | 56 | if (!storeByUserId) { 57 | return new NextResponse("Unauthorized", { status: 405 }); 58 | } 59 | 60 | const product = await prismadb.product.create({ 61 | data: { 62 | name, 63 | price, 64 | isFeatured, 65 | isArchived, 66 | categoryId, 67 | colorId, 68 | sizeId, 69 | storeId: params.storeId, 70 | images: { 71 | createMany: { 72 | data: [ 73 | ...images.map((image: { url: string }) => image), 74 | ], 75 | }, 76 | }, 77 | }, 78 | }); 79 | 80 | return NextResponse.json(product); 81 | } catch (error) { 82 | console.log('[PRODUCTS_POST]', error); 83 | return new NextResponse("Internal error", { status: 500 }); 84 | } 85 | }; 86 | 87 | export async function GET( 88 | req: Request, 89 | { params }: { params: { storeId: string } }, 90 | ) { 91 | try { 92 | const { searchParams } = new URL(req.url) 93 | const categoryId = searchParams.get('categoryId') || undefined; 94 | const colorId = searchParams.get('colorId') || undefined; 95 | const sizeId = searchParams.get('sizeId') || undefined; 96 | const isFeatured = searchParams.get('isFeatured'); 97 | 98 | if (!params.storeId) { 99 | return new NextResponse("Store id is required", { status: 400 }); 100 | } 101 | 102 | const products = await prismadb.product.findMany({ 103 | where: { 104 | storeId: params.storeId, 105 | categoryId, 106 | colorId, 107 | sizeId, 108 | isFeatured: isFeatured ? true : undefined, 109 | isArchived: false, 110 | }, 111 | include: { 112 | images: true, 113 | category: true, 114 | color: true, 115 | size: true, 116 | }, 117 | orderBy: { 118 | createdAt: 'desc', 119 | } 120 | }); 121 | 122 | return NextResponse.json(products); 123 | } catch (error) { 124 | console.log('[PRODUCTS_GET]', error); 125 | return new NextResponse("Internal error", { status: 500 }); 126 | } 127 | }; 128 | -------------------------------------------------------------------------------- /app/api/[storeId]/sizes/[sizeId]/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | 3 | import prismadb from "@/lib/prismadb"; 4 | import { auth } from "@clerk/nextjs"; 5 | 6 | export async function GET( 7 | req: Request, 8 | { params }: { params: { sizeId: string } } 9 | ) { 10 | try { 11 | if (!params.sizeId) { 12 | return new NextResponse("Size id is required", { status: 400 }); 13 | } 14 | 15 | const size = await prismadb.size.findUnique({ 16 | where: { 17 | id: params.sizeId 18 | } 19 | }); 20 | 21 | return NextResponse.json(size); 22 | } catch (error) { 23 | console.log('[SIZE_GET]', error); 24 | return new NextResponse("Internal error", { status: 500 }); 25 | } 26 | }; 27 | 28 | export async function DELETE( 29 | req: Request, 30 | { params }: { params: { sizeId: string, storeId: string } } 31 | ) { 32 | try { 33 | const { userId } = auth(); 34 | 35 | if (!userId) { 36 | return new NextResponse("Unauthenticated", { status: 403 }); 37 | } 38 | 39 | if (!params.sizeId) { 40 | return new NextResponse("Size id is required", { status: 400 }); 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", { status: 405 }); 52 | } 53 | 54 | const size = await prismadb.size.delete({ 55 | where: { 56 | id: params.sizeId 57 | } 58 | }); 59 | 60 | return NextResponse.json(size); 61 | } catch (error) { 62 | console.log('[SIZE_DELETE]', error); 63 | return new NextResponse("Internal error", { status: 500 }); 64 | } 65 | }; 66 | 67 | 68 | export async function PATCH( 69 | req: Request, 70 | { params }: { params: { sizeId: string, storeId: string } } 71 | ) { 72 | try { 73 | const { userId } = auth(); 74 | 75 | const body = await req.json(); 76 | 77 | const { name, value } = body; 78 | 79 | if (!userId) { 80 | return new NextResponse("Unauthenticated", { status: 403 }); 81 | } 82 | 83 | if (!name) { 84 | return new NextResponse("Name is required", { status: 400 }); 85 | } 86 | 87 | if (!value) { 88 | return new NextResponse("Value is required", { status: 400 }); 89 | } 90 | 91 | 92 | if (!params.sizeId) { 93 | return new NextResponse("Size id is required", { status: 400 }); 94 | } 95 | 96 | const storeByUserId = await prismadb.store.findFirst({ 97 | where: { 98 | id: params.storeId, 99 | userId 100 | } 101 | }); 102 | 103 | if (!storeByUserId) { 104 | return new NextResponse("Unauthorized", { status: 405 }); 105 | } 106 | 107 | const size = await prismadb.size.update({ 108 | where: { 109 | id: params.sizeId 110 | }, 111 | data: { 112 | name, 113 | value 114 | } 115 | }); 116 | 117 | return NextResponse.json(size); 118 | } catch (error) { 119 | console.log('[SIZE_PATCH]', error); 120 | return new NextResponse("Internal error", { status: 500 }); 121 | } 122 | }; 123 | -------------------------------------------------------------------------------- /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 | 13 | const body = await req.json(); 14 | 15 | const { name, value } = body; 16 | 17 | if (!userId) { 18 | return new NextResponse("Unauthenticated", { status: 403 }); 19 | } 20 | 21 | if (!name) { 22 | return new NextResponse("Name is required", { status: 400 }); 23 | } 24 | 25 | if (!value) { 26 | return new NextResponse("Value is required", { status: 400 }); 27 | } 28 | 29 | if (!params.storeId) { 30 | return new NextResponse("Store id is required", { status: 400 }); 31 | } 32 | 33 | const storeByUserId = await prismadb.store.findFirst({ 34 | where: { 35 | id: params.storeId, 36 | userId 37 | } 38 | }); 39 | 40 | if (!storeByUserId) { 41 | return new NextResponse("Unauthorized", { status: 405 }); 42 | } 43 | 44 | const size = await prismadb.size.create({ 45 | data: { 46 | name, 47 | value, 48 | storeId: params.storeId 49 | } 50 | }); 51 | 52 | return NextResponse.json(size); 53 | } catch (error) { 54 | console.log('[SIZES_POST]', error); 55 | return new NextResponse("Internal error", { status: 500 }); 56 | } 57 | }; 58 | 59 | export async function GET( 60 | req: Request, 61 | { params }: { params: { storeId: string } } 62 | ) { 63 | try { 64 | if (!params.storeId) { 65 | return new NextResponse("Store id is required", { status: 400 }); 66 | } 67 | 68 | const sizes = await prismadb.size.findMany({ 69 | where: { 70 | storeId: params.storeId 71 | } 72 | }); 73 | 74 | return NextResponse.json(sizes); 75 | } catch (error) { 76 | console.log('[SIZES_GET]', error); 77 | return new NextResponse("Internal error", { status: 500 }); 78 | } 79 | }; 80 | -------------------------------------------------------------------------------- /app/api/stores/[storeId]/route.ts: -------------------------------------------------------------------------------- 1 | import prismadb from "@/lib/prismadb"; 2 | import { auth } from "@clerk/nextjs"; 3 | import { NextResponse } from "next/server"; 4 | 5 | export async function PATCH( 6 | req: Request, 7 | { params }: { params: { storeId: string } }, 8 | ) { 9 | try { 10 | const { userId } = auth(); 11 | if (!userId) { 12 | return new NextResponse("Unauthorized", { status: 401 }); 13 | } 14 | const { storeId } = params; 15 | const body = await req.json(); 16 | const { name } = body; 17 | 18 | if (!name) { 19 | return new NextResponse("Name is Required", { status: 400 }); 20 | } 21 | if (!storeId) { 22 | return new NextResponse("Store ID is Required", { status: 400 }); 23 | } 24 | 25 | // find and update store 26 | 27 | const store = await prismadb.store.updateMany({ 28 | where: { 29 | id: storeId, 30 | userId, 31 | }, 32 | data: { 33 | name, 34 | }, 35 | }); 36 | return NextResponse.json(store); 37 | } catch (error: any) { 38 | console.log(`[STORE_PATCH] `, error); 39 | return new NextResponse("Internal Server Error", { 40 | status: 500, 41 | }); 42 | } 43 | } 44 | 45 | export async function DELETE( 46 | req: Request, 47 | { params }: { params: { storeId: string } }, 48 | ) { 49 | try { 50 | const { userId } = auth(); 51 | if (!userId) { 52 | return new NextResponse("Unauthorized", { status: 401 }); 53 | } 54 | const { storeId } = params; 55 | 56 | if (!storeId) { 57 | return new NextResponse("Store ID is Required", { status: 400 }); 58 | } 59 | 60 | // find and update store 61 | 62 | const store = await prismadb.store.deleteMany({ 63 | where: { 64 | id: storeId, 65 | userId, 66 | }, 67 | }); 68 | return NextResponse.json(store); 69 | } catch (error: any) { 70 | console.log(`[STORE_DELETE] `, error); 71 | return new NextResponse("Internal Server Error", { 72 | status: 500, 73 | }); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /app/api/stores/route.ts: -------------------------------------------------------------------------------- 1 | import prismadb from "@/lib/prismadb"; 2 | import { auth } from "@clerk/nextjs"; 3 | import { NextResponse } from "next/server"; 4 | 5 | export async function POST(req: Request) { 6 | try { 7 | const { userId } = auth(); // we have access to the user id here that wants to create new store using our api 8 | 9 | const body = await req.json(); 10 | const { name } = body; 11 | 12 | if (!name) { 13 | return new NextResponse("Bad Request", { status: 400 }); 14 | } 15 | 16 | if (!userId) { 17 | return new NextResponse("Name is required", { status: 400 }); 18 | } 19 | 20 | // create new store using prisma client instance and return the store data to the client 21 | const store = await prismadb.store.create({ 22 | data: { 23 | name, 24 | userId, 25 | }, 26 | }); 27 | 28 | return NextResponse.json(store); 29 | } catch (error) { 30 | console.log(`[STORES_POST] ${error}`, error); 31 | return new NextResponse("Internal Server Error", { status: 500 }); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /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("Stripe-Signature") as string; 11 | 12 | let event: Stripe.Event; 13 | 14 | try { 15 | event = stripe.webhooks.constructEvent( 16 | body, 17 | signature, 18 | process.env.STRIPE_WEBHOOK_SECRET!, 19 | ); 20 | } catch (error: any) { 21 | return new NextResponse(`Webhook Error: ${error.message}`, { status: 400 }); 22 | } 23 | 24 | const session = event.data.object as Stripe.Checkout.Session; 25 | const address = session?.customer_details?.address; 26 | 27 | const addressComponents = [ 28 | address?.line1, 29 | address?.line2, 30 | address?.city, 31 | address?.state, 32 | address?.postal_code, 33 | address?.country, 34 | ]; 35 | 36 | const addressString = addressComponents.filter(c => c !== null).join(", "); 37 | 38 | if (event.type === "checkout.session.completed") { 39 | const order = await prismadb.order.update({ 40 | where: { 41 | id: session?.metadata?.orderId, 42 | }, 43 | data: { 44 | isPaid: true, 45 | address: addressString, 46 | phone: session?.customer_details?.phone || "", 47 | }, 48 | include: { 49 | orderItems: true, 50 | }, 51 | }); 52 | 53 | const productIds = order.orderItems.map(orderItem => orderItem.productId); 54 | 55 | await prismadb.product.updateMany({ 56 | where: { 57 | id: { 58 | in: [...productIds], 59 | }, 60 | }, 61 | data: { 62 | isArchived: true, 63 | }, 64 | }); 65 | } 66 | 67 | return new NextResponse(null, { status: 200 }); 68 | } 69 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lalitdotdev/omnidash/c532101e888fdf47fedabf88f380ef252ae07ecf/app/favicon.ico -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @html, body, 6 | :root { 7 | height: 100%; 8 | } 9 | 10 | @layer base { 11 | :root { 12 | --background: 0 0% 100%; 13 | --foreground: 222.2 84% 4.9%; 14 | 15 | --muted: 210 40% 96.1%; 16 | --muted-foreground: 215.4 16.3% 46.9%; 17 | 18 | --popover: 0 0% 100%; 19 | --popover-foreground: 222.2 84% 4.9%; 20 | 21 | --card: 0 0% 100%; 22 | --card-foreground: 222.2 84% 4.9%; 23 | 24 | --border: 214.3 31.8% 91.4%; 25 | --input: 214.3 31.8% 91.4%; 26 | 27 | --primary: 222.2 47.4% 11.2%; 28 | --primary-foreground: 210 40% 98%; 29 | 30 | --secondary: 210 40% 96.1%; 31 | --secondary-foreground: 222.2 47.4% 11.2%; 32 | 33 | --accent: 210 40% 96.1%; 34 | --accent-foreground: 222.2 47.4% 11.2%; 35 | 36 | --destructive: 0 84.2% 60.2%; 37 | --destructive-foreground: 210 40% 98%; 38 | 39 | --ring: 215 20.2% 65.1%; 40 | 41 | --radius: 0.5rem; 42 | } 43 | 44 | .dark { 45 | --background: 222.2 84% 4.9%; 46 | --foreground: 210 40% 98%; 47 | 48 | --muted: 217.2 32.6% 17.5%; 49 | --muted-foreground: 215 20.2% 65.1%; 50 | 51 | --popover: 222.2 84% 4.9%; 52 | --popover-foreground: 210 40% 98%; 53 | 54 | --card: 222.2 84% 4.9%; 55 | --card-foreground: 210 40% 98%; 56 | 57 | --border: 217.2 32.6% 17.5%; 58 | --input: 217.2 32.6% 17.5%; 59 | 60 | --primary: 210 40% 98%; 61 | --primary-foreground: 222.2 47.4% 11.2%; 62 | 63 | --secondary: 217.2 32.6% 17.5%; 64 | --secondary-foreground: 210 40% 98%; 65 | 66 | --accent: 217.2 32.6% 17.5%; 67 | --accent-foreground: 210 40% 98%; 68 | 69 | --destructive: 0 62.8% 30.6%; 70 | --destructive-foreground: 0 85.7% 97.3%; 71 | 72 | --ring: 217.2 32.6% 17.5%; 73 | } 74 | } 75 | 76 | @layer base { 77 | * { 78 | @apply border-border; 79 | } 80 | body { 81 | @apply bg-background text-foreground; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { ModalProvider } from "@/providers/modal-provider"; 2 | import { ToastProvider } from "@/providers/toast-provider"; 3 | import { ClerkProvider } from "@clerk/nextjs"; 4 | import type { Metadata } from "next"; 5 | import { Inter } from "next/font/google"; 6 | import "./globals.css"; 7 | import { ThemeProvider } from "@/providers/theme-provider"; 8 | 9 | const inter = Inter({ subsets: ["latin"] }); 10 | 11 | export const metadata: Metadata = { 12 | title: "StoreDash", 13 | description: "Generated by create next app", 14 | }; 15 | 16 | export default function RootLayout({ 17 | children, 18 | }: { 19 | children: React.ReactNode; 20 | }) { 21 | return ( 22 | 23 | 24 | 25 | 31 | 32 | 33 | {children} 34 | 35 | 36 | 37 | 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /components/mails/mail-list.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentProps } from 'react'; 2 | import { formatDistanceToNow } from 'date-fns'; 3 | 4 | import { cn } from '@/lib/utils'; 5 | import { Mail, mails } from '@/lib/data'; 6 | import { useMailStore } from '@/hooks/use-mail'; 7 | import { ScrollArea } from '../ui/scroll-area'; 8 | import { Badge } from '../ui/badge'; 9 | 10 | interface MailListProps { 11 | items: Mail[]; 12 | } 13 | 14 | export function MailList({ items }: MailListProps) { 15 | const { selected, selectMail } = useMailStore(); 16 | 17 | return ( 18 | 19 |
20 | {items.map((item) => ( 21 | 56 | ))} 57 |
58 |
59 | ); 60 | } 61 | 62 | function getBadgeVariantFromLabel(label: string): ComponentProps['variant'] { 63 | if (['work'].includes(label.toLowerCase())) { 64 | return 'default'; 65 | } 66 | 67 | if (['personal'].includes(label.toLowerCase())) { 68 | return 'outline'; 69 | } 70 | 71 | return 'secondary'; 72 | } 73 | -------------------------------------------------------------------------------- /components/main-nav.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Link from "next/link"; 4 | import { useParams, usePathname } from "next/navigation"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | export function MainNav({ 9 | className, 10 | ...props 11 | }: React.HTMLAttributes) { 12 | const pathname = usePathname(); 13 | const params = useParams(); 14 | 15 | const routes = [ 16 | { 17 | href: `/${params.storeId}`, 18 | label: "Overview", 19 | active: pathname === `/${params.storeId}`, 20 | }, 21 | { 22 | href: `/${params.storeId}/billboards`, 23 | label: "Billboards", 24 | active: pathname === `/${params.storeId}/billboards`, 25 | }, 26 | { 27 | href: `/${params.storeId}/categories`, 28 | label: "Categories", 29 | active: pathname === `/${params.storeId}/categories`, 30 | }, 31 | { 32 | href: `/${params.storeId}/sizes`, 33 | label: "Sizes", 34 | active: pathname === `/${params.storeId}/sizes`, 35 | }, 36 | { 37 | href: `/${params.storeId}/colors`, 38 | label: "Colors", 39 | active: pathname === `/${params.storeId}/colors`, 40 | }, 41 | { 42 | href: `/${params.storeId}/products`, 43 | label: "Products", 44 | active: pathname === `/${params.storeId}/products`, 45 | }, 46 | { 47 | href: `/${params.storeId}/orders`, 48 | label: "Orders", 49 | active: pathname === `/${params.storeId}/orders`, 50 | }, 51 | { 52 | href: `/${params.storeId}/settings`, 53 | label: "Settings", 54 | active: pathname === `/${params.storeId}/settings`, 55 | }, 56 | ]; 57 | 58 | return ( 59 | 79 | ); 80 | } 81 | -------------------------------------------------------------------------------- /components/modals/alert-modal.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect, useState } from "react"; 4 | import { Modal } from "@/components/ui/modal"; 5 | import { Button } from "@/components/ui/button"; 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 | const [isMounted, setIsMounted] = useState(false); 21 | useEffect(() => { 22 | setIsMounted(true); 23 | }, []); 24 | 25 | if (!isMounted) return null; 26 | 27 | return ( 28 | 34 |
35 | 38 | 41 |
42 |
43 | ); 44 | }; 45 | -------------------------------------------------------------------------------- /components/modals/store-modal.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as z from "zod"; 4 | import axios from "axios"; 5 | import { zodResolver } from "@hookform/resolvers/zod"; 6 | import { useForm } from "react-hook-form"; 7 | import { toast } from "react-hot-toast"; 8 | import { useRouter } from "next/navigation"; 9 | import { useState } from "react"; 10 | 11 | import { Modal } from "@/components/ui/modal"; 12 | import { Input } from "@/components/ui/input"; 13 | import { 14 | Form, 15 | FormControl, 16 | FormDescription, 17 | FormField, 18 | FormItem, 19 | FormLabel, 20 | FormMessage, 21 | } from "@/components/ui/form"; 22 | import { useStoreModal } from "@/hooks/use-store-modal"; 23 | import { Button } from "@/components/ui/button"; 24 | 25 | const formSchema = z.object({ 26 | name: z.string().min(1), 27 | }); 28 | 29 | export const StoreModal = () => { 30 | const storeModal = useStoreModal(); 31 | const router = useRouter(); 32 | 33 | const [loading, setLoading] = useState(false); 34 | 35 | // Defining hook for form 36 | const form = useForm>({ 37 | resolver: zodResolver(formSchema), 38 | defaultValues: { 39 | name: "", 40 | }, 41 | }); 42 | 43 | const onSubmit = async (values: z.infer) => { 44 | console.log("values", values); 45 | try { 46 | setLoading(true); 47 | const response = await axios.post("/api/stores", values); 48 | // window.location.assign is used to redirect the user with a refresh to the store page (dashboard) 49 | window.location.assign(`/${response.data.id}`); 50 | } catch (error) { 51 | toast.error("Something went wrong"); 52 | } finally { 53 | setLoading(false); 54 | } 55 | }; 56 | 57 | return ( 58 | 64 |
65 |
66 |
67 |
68 | 69 | ( 73 | 74 | Name 75 | 76 | 81 | 82 | 83 | 84 | )} 85 | /> 86 |
87 | 94 | 97 |
98 | 99 | 100 |
101 |
102 |
103 |
104 | ); 105 | }; 106 | -------------------------------------------------------------------------------- /components/navbar.tsx: -------------------------------------------------------------------------------- 1 | import { UserButton, auth } from '@clerk/nextjs'; 2 | import { redirect } from 'next/navigation'; 3 | 4 | import StoreSwitcher from '@/components/store-switcher'; 5 | 6 | import prismadb from '@/lib/prismadb'; 7 | 8 | import { MainNav } from './main-nav'; 9 | import { ThemeToggle } from './theme-toggle'; 10 | 11 | const Navbar = async () => { 12 | const { userId } = auth(); 13 | 14 | if (!userId) { 15 | redirect('/sign-in'); 16 | } 17 | 18 | const stores = await prismadb.store.findMany({ 19 | where: { 20 | userId, 21 | }, 22 | }); 23 | 24 | return ( 25 |
26 |
27 | 28 | 29 |
30 | 31 | 32 |
33 |
34 |
35 | ); 36 | }; 37 | 38 | export default Navbar; 39 | -------------------------------------------------------------------------------- /components/overview.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Bar, BarChart, ResponsiveContainer, XAxis, YAxis } from "recharts"; 4 | 5 | interface OverviewProps { 6 | data: any[]; 7 | } 8 | 9 | export const Overview: React.FC = ({ data }) => { 10 | return ( 11 | 12 | 13 | 20 | `$${value}`} 26 | /> 27 | 28 | 29 | 30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /components/sideNav.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Link from 'next/link'; 4 | import { LucideIcon } from 'lucide-react'; 5 | 6 | import { cn } from '@/lib/utils'; 7 | import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip'; 8 | import { buttonVariants } from './ui/button'; 9 | 10 | interface NavProps { 11 | isCollapsed: boolean; 12 | links: { 13 | title: string; 14 | label?: string; 15 | icon: LucideIcon; 16 | variant: 'default' | 'ghost'; 17 | }[]; 18 | } 19 | 20 | export function SideNav({ links, isCollapsed }: NavProps) { 21 | return ( 22 |
23 | 67 |
68 | ); 69 | } 70 | -------------------------------------------------------------------------------- /components/store-switcher.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { Check, ChevronsUpDown, PlusCircle, Store } from "lucide-react"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | import { Button } from "@/components/ui/button"; 8 | import { 9 | Command, 10 | CommandEmpty, 11 | CommandGroup, 12 | CommandInput, 13 | CommandItem, 14 | CommandList, 15 | CommandSeparator, 16 | } from "@/components/ui/command"; 17 | import { 18 | Popover, 19 | PopoverContent, 20 | PopoverTrigger, 21 | } from "@/components/ui/popover"; 22 | import { useStoreModal } from "@/hooks/use-store-modal"; 23 | import { useParams, useRouter } from "next/navigation"; 24 | 25 | type PopoverTriggerProps = React.ComponentPropsWithoutRef< 26 | typeof PopoverTrigger 27 | >; 28 | 29 | interface StoreSwitcherProps extends PopoverTriggerProps { 30 | items: Record[]; 31 | } 32 | 33 | export default function StoreSwitcher({ 34 | className, 35 | items = [], 36 | }: StoreSwitcherProps) { 37 | const storeModal = useStoreModal(); 38 | const params = useParams(); 39 | const router = useRouter(); 40 | 41 | const formattedItems = items.map(item => ({ 42 | label: item.name, 43 | value: item.id, 44 | })); 45 | 46 | const currentStore = formattedItems.find( 47 | item => item.value === params.storeId, 48 | ); 49 | 50 | const [open, setOpen] = React.useState(false); 51 | 52 | const onStoreSelect = (store: { value: string; label: string }) => { 53 | setOpen(false); 54 | router.push(`/${store.value}`); 55 | }; 56 | 57 | return ( 58 | 59 | 60 | 72 | 73 | 74 | 75 | 76 | 77 | No store found. 78 | 79 | {formattedItems.map(store => ( 80 | onStoreSelect(store)} 83 | className="text-sm" 84 | > 85 | 86 | {store.label} 87 | 95 | 96 | ))} 97 | 98 | 99 | 100 | 101 | 102 | { 104 | setOpen(false); 105 | storeModal.onOpen(); 106 | }} 107 | > 108 | 109 | Create Store 110 | 111 | 112 | 113 | 114 | 115 | 116 | ); 117 | } 118 | -------------------------------------------------------------------------------- /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 { Button } from "@/components/ui/button"; 8 | import { 9 | DropdownMenu, 10 | DropdownMenuContent, 11 | DropdownMenuItem, 12 | DropdownMenuTrigger, 13 | } from "@/components/ui/dropdown-menu"; 14 | 15 | export function ThemeToggle() { 16 | const { setTheme } = useTheme(); 17 | 18 | return ( 19 | 20 | 21 | 26 | 27 | 28 | setTheme("light")}> 29 | Light 30 | 31 | setTheme("dark")}> 32 | Dark 33 | 34 | setTheme("system")}> 35 | System 36 | 37 | 38 | 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /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 [&>svg~*]:pl-7 [&>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 | -------------------------------------------------------------------------------- /components/ui/api-alert.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Copy, Server } from "lucide-react"; 4 | import { Alert, AlertDescription, AlertTitle } from "./alert"; 5 | import { Badge, BadgeProps } from "./badge"; 6 | import { Button } from "./button"; 7 | import toast from "react-hot-toast"; 8 | 9 | interface ApiAlertProps { 10 | title: string; 11 | description: string; 12 | variant: "public" | "admin"; 13 | } 14 | 15 | const textMap: Record = { 16 | public: "Public", 17 | admin: "Admin", 18 | }; 19 | 20 | const variantMap: Record = { 21 | public: "secondary", 22 | admin: "destructive", 23 | }; 24 | 25 | export const ApiAlert: React.FC = ({ 26 | title, 27 | description, 28 | variant = "public", 29 | }) => { 30 | const onCopy = (description: string) => { 31 | navigator.clipboard.writeText(description); 32 | toast.success("API Route copied to clipboard."); 33 | }; 34 | 35 | return ( 36 | 37 | 38 | 39 | {title} 40 | {textMap[variant]} 41 | 42 | 43 | 44 | {description} 45 | 46 | 49 | 50 | 51 | ); 52 | }; 53 | -------------------------------------------------------------------------------- /components/ui/api-list.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ApiAlert } from "@/components/ui/api-alert"; 4 | import { useOrigin } from "@/hooks/use-origin"; 5 | import { useParams } from "next/navigation"; 6 | 7 | interface ApiListProps { 8 | entityName: string; 9 | entityIdName: string; 10 | } 11 | export const ApiList: React.FC = ({ 12 | entityName, 13 | entityIdName, 14 | }) => { 15 | const params = useParams(); 16 | const origin = useOrigin(); 17 | 18 | const baseUrl = `${origin}/api/${params.storeId}`; 19 | return ( 20 | <> 21 | {/* API ALERTS*/} 22 | 27 | 32 | 37 | 42 | 47 | 48 | ); 49 | }; 50 | -------------------------------------------------------------------------------- /components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AvatarPrimitive from "@radix-ui/react-avatar" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Avatar = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | )) 21 | Avatar.displayName = AvatarPrimitive.Root.displayName 22 | 23 | const AvatarImage = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, ...props }, ref) => ( 27 | 32 | )) 33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName 34 | 35 | const AvatarFallback = React.forwardRef< 36 | React.ElementRef, 37 | React.ComponentPropsWithoutRef 38 | >(({ className, ...props }, ref) => ( 39 | 47 | )) 48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName 49 | 50 | export { Avatar, AvatarImage, AvatarFallback } 51 | -------------------------------------------------------------------------------- /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 | }, 19 | }, 20 | defaultVariants: { 21 | variant: "default", 22 | }, 23 | } 24 | ) 25 | 26 | export interface BadgeProps 27 | extends React.HTMLAttributes, 28 | VariantProps {} 29 | 30 | function Badge({ className, variant, ...props }: BadgeProps) { 31 | return ( 32 |
33 | ) 34 | } 35 | 36 | export { Badge, badgeVariants } 37 | -------------------------------------------------------------------------------- /components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center 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 | -------------------------------------------------------------------------------- /components/ui/calendar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { ChevronLeft, ChevronRight } from "lucide-react" 5 | import { DayPicker } from "react-day-picker" 6 | 7 | import { cn } from "@/lib/utils" 8 | import { buttonVariants } from "@/components/ui/button" 9 | 10 | export type CalendarProps = React.ComponentProps 11 | 12 | function Calendar({ 13 | className, 14 | classNames, 15 | showOutsideDays = true, 16 | ...props 17 | }: CalendarProps) { 18 | return ( 19 | , 58 | IconRight: ({ ...props }) => , 59 | }} 60 | {...props} 61 | /> 62 | ) 63 | } 64 | Calendar.displayName = "Calendar" 65 | 66 | export { Calendar } 67 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /components/ui/checkbox.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox" 5 | import { Check } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const Checkbox = React.forwardRef< 10 | React.ElementRef, 11 | React.ComponentPropsWithoutRef 12 | >(({ className, ...props }, ref) => ( 13 | 21 | 24 | 25 | 26 | 27 | )) 28 | Checkbox.displayName = CheckboxPrimitive.Root.displayName 29 | 30 | export { Checkbox } 31 | -------------------------------------------------------------------------------- /components/ui/data-table-pagination.tsx: -------------------------------------------------------------------------------- 1 | import { Table } from '@tanstack/react-table'; 2 | import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './select'; 3 | import { Button } from './button'; 4 | import { ChevronLeftIcon, ChevronRightIcon, ChevronsLeft, ChevronsRight } from 'lucide-react'; 5 | 6 | interface DataTablePaginationProps { 7 | table: Table; 8 | } 9 | 10 | export function DataTablePagination({ table }: DataTablePaginationProps) { 11 | return ( 12 |
13 |
14 | {table.getFilteredSelectedRowModel().rows.length} of {table.getFilteredRowModel().rows.length} row(s) selected. 15 |
16 |
17 |
18 |

Rows per page

19 | 36 |
37 |
38 | Page {table.getState().pagination.pageIndex + 1} of {table.getPageCount()} 39 |
40 |
41 | 50 | 59 | 68 | 77 |
78 |
79 |
80 | ); 81 | } 82 | -------------------------------------------------------------------------------- /components/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as DialogPrimitive from "@radix-ui/react-dialog" 5 | import { X } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const Dialog = DialogPrimitive.Root 10 | 11 | const DialogTrigger = DialogPrimitive.Trigger 12 | 13 | const DialogPortal = ({ 14 | className, 15 | ...props 16 | }: DialogPrimitive.DialogPortalProps) => ( 17 | 18 | ) 19 | DialogPortal.displayName = DialogPrimitive.Portal.displayName 20 | 21 | const DialogOverlay = React.forwardRef< 22 | React.ElementRef, 23 | React.ComponentPropsWithoutRef 24 | >(({ className, ...props }, ref) => ( 25 | 33 | )) 34 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName 35 | 36 | const DialogContent = React.forwardRef< 37 | React.ElementRef, 38 | React.ComponentPropsWithoutRef 39 | >(({ className, children, ...props }, ref) => ( 40 | 41 | 42 | 50 | {children} 51 | 52 | 53 | Close 54 | 55 | 56 | 57 | )) 58 | DialogContent.displayName = DialogPrimitive.Content.displayName 59 | 60 | const DialogHeader = ({ 61 | className, 62 | ...props 63 | }: React.HTMLAttributes) => ( 64 |
71 | ) 72 | DialogHeader.displayName = "DialogHeader" 73 | 74 | const DialogFooter = ({ 75 | className, 76 | ...props 77 | }: React.HTMLAttributes) => ( 78 |
85 | ) 86 | DialogFooter.displayName = "DialogFooter" 87 | 88 | const DialogTitle = React.forwardRef< 89 | React.ElementRef, 90 | React.ComponentPropsWithoutRef 91 | >(({ className, ...props }, ref) => ( 92 | 100 | )) 101 | DialogTitle.displayName = DialogPrimitive.Title.displayName 102 | 103 | const DialogDescription = React.forwardRef< 104 | React.ElementRef, 105 | React.ComponentPropsWithoutRef 106 | >(({ className, ...props }, ref) => ( 107 | 112 | )) 113 | DialogDescription.displayName = DialogPrimitive.Description.displayName 114 | 115 | export { 116 | Dialog, 117 | DialogTrigger, 118 | DialogContent, 119 | DialogHeader, 120 | DialogFooter, 121 | DialogTitle, 122 | DialogDescription, 123 | } 124 | -------------------------------------------------------------------------------- /components/ui/form.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as LabelPrimitive from "@radix-ui/react-label" 3 | import { Slot } from "@radix-ui/react-slot" 4 | import { 5 | Controller, 6 | ControllerProps, 7 | FieldPath, 8 | FieldValues, 9 | FormProvider, 10 | useFormContext, 11 | } from "react-hook-form" 12 | 13 | import { cn } from "@/lib/utils" 14 | import { Label } from "@/components/ui/label" 15 | 16 | const Form = FormProvider 17 | 18 | type FormFieldContextValue< 19 | TFieldValues extends FieldValues = FieldValues, 20 | TName extends FieldPath = FieldPath 21 | > = { 22 | name: TName 23 | } 24 | 25 | const FormFieldContext = React.createContext( 26 | {} as FormFieldContextValue 27 | ) 28 | 29 | const FormField = < 30 | TFieldValues extends FieldValues = FieldValues, 31 | TName extends FieldPath = FieldPath 32 | >({ 33 | ...props 34 | }: ControllerProps) => { 35 | return ( 36 | 37 | 38 | 39 | ) 40 | } 41 | 42 | const useFormField = () => { 43 | const fieldContext = React.useContext(FormFieldContext) 44 | const itemContext = React.useContext(FormItemContext) 45 | const { getFieldState, formState } = useFormContext() 46 | 47 | const fieldState = getFieldState(fieldContext.name, formState) 48 | 49 | if (!fieldContext) { 50 | throw new Error("useFormField should be used within ") 51 | } 52 | 53 | const { id } = itemContext 54 | 55 | return { 56 | id, 57 | name: fieldContext.name, 58 | formItemId: `${id}-form-item`, 59 | formDescriptionId: `${id}-form-item-description`, 60 | formMessageId: `${id}-form-item-message`, 61 | ...fieldState, 62 | } 63 | } 64 | 65 | type FormItemContextValue = { 66 | id: string 67 | } 68 | 69 | const FormItemContext = React.createContext( 70 | {} as FormItemContextValue 71 | ) 72 | 73 | const FormItem = React.forwardRef< 74 | HTMLDivElement, 75 | React.HTMLAttributes 76 | >(({ className, ...props }, ref) => { 77 | const id = React.useId() 78 | 79 | return ( 80 | 81 |
82 | 83 | ) 84 | }) 85 | FormItem.displayName = "FormItem" 86 | 87 | const FormLabel = React.forwardRef< 88 | React.ElementRef, 89 | React.ComponentPropsWithoutRef 90 | >(({ className, ...props }, ref) => { 91 | const { error, formItemId } = useFormField() 92 | 93 | return ( 94 |