├── .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 |
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 |
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 |
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 |
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 |
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 |
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 |
88 | {order.isPaid ? 'Successful' : 'Processing'}
89 |
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 |
100 |
101 | {numberOfUnpaidOrders} {numberOfUnpaidOrders > 1 ? 'orders' : 'order'}
102 |
103 |
104 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
100 | )
101 | })
102 | FormLabel.displayName = "FormLabel"
103 |
104 | const FormControl = React.forwardRef<
105 | React.ElementRef,
106 | React.ComponentPropsWithoutRef
107 | >(({ ...props }, ref) => {
108 | const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
109 |
110 | return (
111 |
122 | )
123 | })
124 | FormControl.displayName = "FormControl"
125 |
126 | const FormDescription = React.forwardRef<
127 | HTMLParagraphElement,
128 | React.HTMLAttributes
129 | >(({ className, ...props }, ref) => {
130 | const { formDescriptionId } = useFormField()
131 |
132 | return (
133 |
139 | )
140 | })
141 | FormDescription.displayName = "FormDescription"
142 |
143 | const FormMessage = React.forwardRef<
144 | HTMLParagraphElement,
145 | React.HTMLAttributes
146 | >(({ className, children, ...props }, ref) => {
147 | const { error, formMessageId } = useFormField()
148 | const body = error ? String(error?.message) : children
149 |
150 | if (!body) {
151 | return null
152 | }
153 |
154 | return (
155 |
161 | {body}
162 |
163 | )
164 | })
165 | FormMessage.displayName = "FormMessage"
166 |
167 | export {
168 | useFormField,
169 | Form,
170 | FormItem,
171 | FormLabel,
172 | FormControl,
173 | FormDescription,
174 | FormMessage,
175 | FormField,
176 | }
177 |
--------------------------------------------------------------------------------
/components/ui/heading.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | interface HeadingProps {
4 | title: string;
5 | description: string;
6 | }
7 | const Heading: React.FC = ({ title, description }) => {
8 | return (
9 |
10 |
{title}
11 |
{description}
12 |
13 | );
14 | };
15 |
16 | export default Heading;
17 |
--------------------------------------------------------------------------------
/components/ui/image-upload.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { CldUploadWidget } from "next-cloudinary";
4 | import { useEffect, useState } from "react";
5 |
6 | import { Button } from "@/components/ui/button";
7 | import Image from "next/image";
8 | import { ImagePlus, Trash } from "lucide-react";
9 |
10 | interface ImageUploadProps {
11 | disabled?: boolean;
12 | onChange: (value: string) => void;
13 | onRemove: (value: string) => void;
14 | value: string[];
15 | }
16 |
17 | const ImageUpload: React.FC = ({
18 | disabled,
19 | onChange,
20 | onRemove,
21 | value,
22 | }) => {
23 | // to prevent hydration error
24 | const [isMounted, setIsMounted] = useState(false);
25 |
26 | useEffect(() => {
27 | setIsMounted(true);
28 | }, []);
29 |
30 | const onUpload = (result: any) => {
31 | onChange(result.info.secure_url);
32 | };
33 |
34 | if (!isMounted) {
35 | return null;
36 | }
37 |
38 | return (
39 |
40 |
41 | {value.map(url => (
42 |
46 |
47 |
55 |
56 |
57 |
58 | ))}
59 |
60 |
61 | {({ open }) => {
62 | const onClick = () => {
63 | open();
64 | };
65 |
66 | return (
67 |
76 | );
77 | }}
78 |
79 |
80 | );
81 | };
82 |
83 | export default ImageUpload;
84 |
--------------------------------------------------------------------------------
/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | )
21 | }
22 | )
23 | Input.displayName = "Input"
24 |
25 | export { Input }
26 |
--------------------------------------------------------------------------------
/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const labelVariants = cva(
10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
11 | )
12 |
13 | const Label = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef &
16 | VariantProps
17 | >(({ className, ...props }, ref) => (
18 |
23 | ))
24 | Label.displayName = LabelPrimitive.Root.displayName
25 |
26 | export { Label }
27 |
--------------------------------------------------------------------------------
/components/ui/loader.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { BarLoader } from "react-spinners";
4 |
5 | export const Loader = () => {
6 | return ;
7 | };
8 |
--------------------------------------------------------------------------------
/components/ui/modal.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import {
4 | Dialog,
5 | DialogContent,
6 | DialogDescription,
7 | DialogHeader,
8 | DialogTitle,
9 | } from "@/components/ui/dialog";
10 |
11 | interface ModalProps {
12 | title: string;
13 | description: string;
14 | isOpen: boolean;
15 | onClose: () => void;
16 | children?: React.ReactNode;
17 | }
18 |
19 | export const Modal: React.FC = ({
20 | title,
21 | description,
22 | isOpen,
23 | onClose,
24 | children,
25 | }) => {
26 | const onChange = (open: boolean) => {
27 | if (!open) {
28 | onClose();
29 | }
30 | };
31 |
32 | return (
33 |
42 | );
43 | };
44 |
--------------------------------------------------------------------------------
/components/ui/nav.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 './tooltip';
8 | import { buttonVariants } from './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 Nav({ links, isCollapsed }: NavProps) {
21 | return (
22 |
23 |
67 |
68 | );
69 | }
70 |
--------------------------------------------------------------------------------
/components/ui/popover.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as PopoverPrimitive from "@radix-ui/react-popover"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Popover = PopoverPrimitive.Root
9 |
10 | const PopoverTrigger = PopoverPrimitive.Trigger
11 |
12 | const PopoverContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
16 |
17 |
27 |
28 | ))
29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName
30 |
31 | export { Popover, PopoverTrigger, PopoverContent }
32 |
--------------------------------------------------------------------------------
/components/ui/resizable.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { GripVertical } from 'lucide-react';
4 | import * as ResizablePrimitive from 'react-resizable-panels';
5 |
6 | import { cn } from '@/lib/utils';
7 |
8 | const ResizablePanelGroup = ({ className, ...props }: React.ComponentProps) => (
9 |
13 | );
14 |
15 | const ResizablePanel = ResizablePrimitive.Panel;
16 |
17 | const ResizableHandle = ({
18 | withHandle,
19 | className,
20 | ...props
21 | }: React.ComponentProps & {
22 | withHandle?: boolean;
23 | }) => (
24 | div]:rotate-90',
27 | className,
28 | )}
29 | {...props}
30 | >
31 | {withHandle && (
32 |
33 |
34 |
35 | )}
36 |
37 | );
38 |
39 | export { ResizablePanelGroup, ResizablePanel, ResizableHandle };
40 |
--------------------------------------------------------------------------------
/components/ui/scroll-area.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const ScrollArea = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, children, ...props }, ref) => (
12 |
17 |
18 | {children}
19 |
20 |
21 |
22 |
23 | ))
24 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
25 |
26 | const ScrollBar = React.forwardRef<
27 | React.ElementRef,
28 | React.ComponentPropsWithoutRef
29 | >(({ className, orientation = "vertical", ...props }, ref) => (
30 |
43 |
44 |
45 | ))
46 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
47 |
48 | export { ScrollArea, ScrollBar }
49 |
--------------------------------------------------------------------------------
/components/ui/select.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SelectPrimitive from "@radix-ui/react-select"
5 | import { Check, ChevronDown } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Select = SelectPrimitive.Root
10 |
11 | const SelectGroup = SelectPrimitive.Group
12 |
13 | const SelectValue = SelectPrimitive.Value
14 |
15 | const SelectTrigger = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef
18 | >(({ className, children, ...props }, ref) => (
19 |
27 | {children}
28 |
29 |
30 |
31 |
32 | ))
33 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
34 |
35 | const SelectContent = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, children, position = "popper", ...props }, ref) => (
39 |
40 |
51 |
58 | {children}
59 |
60 |
61 |
62 | ))
63 | SelectContent.displayName = SelectPrimitive.Content.displayName
64 |
65 | const SelectLabel = React.forwardRef<
66 | React.ElementRef,
67 | React.ComponentPropsWithoutRef
68 | >(({ className, ...props }, ref) => (
69 |
74 | ))
75 | SelectLabel.displayName = SelectPrimitive.Label.displayName
76 |
77 | const SelectItem = React.forwardRef<
78 | React.ElementRef,
79 | React.ComponentPropsWithoutRef
80 | >(({ className, children, ...props }, ref) => (
81 |
89 |
90 |
91 |
92 |
93 |
94 |
95 | {children}
96 |
97 | ))
98 | SelectItem.displayName = SelectPrimitive.Item.displayName
99 |
100 | const SelectSeparator = React.forwardRef<
101 | React.ElementRef,
102 | React.ComponentPropsWithoutRef
103 | >(({ className, ...props }, ref) => (
104 |
109 | ))
110 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName
111 |
112 | export {
113 | Select,
114 | SelectGroup,
115 | SelectValue,
116 | SelectTrigger,
117 | SelectContent,
118 | SelectLabel,
119 | SelectItem,
120 | SelectSeparator,
121 | }
122 |
--------------------------------------------------------------------------------
/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SeparatorPrimitive from "@radix-ui/react-separator"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Separator = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(
12 | (
13 | { className, orientation = "horizontal", decorative = true, ...props },
14 | ref
15 | ) => (
16 |
27 | )
28 | )
29 | Separator.displayName = SeparatorPrimitive.Root.displayName
30 |
31 | export { Separator }
32 |
--------------------------------------------------------------------------------
/components/ui/switch.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SwitchPrimitives from "@radix-ui/react-switch"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Switch = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 |
25 |
26 | ))
27 | Switch.displayName = SwitchPrimitives.Root.displayName
28 |
29 | export { Switch }
30 |
--------------------------------------------------------------------------------
/components/ui/table.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { cn } from '@/lib/utils';
4 |
5 | const Table = React.forwardRef>(
6 | ({ className, ...props }, ref) => (
7 |
10 | ),
11 | );
12 | Table.displayName = 'Table';
13 |
14 | const TableHeader = React.forwardRef>(
15 | ({ className, ...props }, ref) => ,
16 | );
17 | TableHeader.displayName = 'TableHeader';
18 |
19 | const TableBody = React.forwardRef>(
20 | ({ className, ...props }, ref) => (
21 |
22 | ),
23 | );
24 | TableBody.displayName = 'TableBody';
25 |
26 | const TableFooter = React.forwardRef>(
27 | ({ className, ...props }, ref) => (
28 |
29 | ),
30 | );
31 | TableFooter.displayName = 'TableFooter';
32 |
33 | const TableRow = React.forwardRef>(
34 | ({ className, ...props }, ref) => (
35 |
40 | ),
41 | );
42 | TableRow.displayName = 'TableRow';
43 |
44 | const TableHead = React.forwardRef>(
45 | ({ className, ...props }, ref) => (
46 | |
54 | ),
55 | );
56 | TableHead.displayName = 'TableHead';
57 |
58 | const TableCell = React.forwardRef>(
59 | ({ className, ...props }, ref) => (
60 | |
61 | ),
62 | );
63 | TableCell.displayName = 'TableCell';
64 |
65 | const TableCaption = React.forwardRef>(
66 | ({ className, ...props }, ref) => (
67 |
68 | ),
69 | );
70 | TableCaption.displayName = 'TableCaption';
71 |
72 | export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption };
73 |
--------------------------------------------------------------------------------
/components/ui/tabs.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TabsPrimitive from "@radix-ui/react-tabs"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Tabs = TabsPrimitive.Root
9 |
10 | const TabsList = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, ...props }, ref) => (
14 |
22 | ))
23 | TabsList.displayName = TabsPrimitive.List.displayName
24 |
25 | const TabsTrigger = React.forwardRef<
26 | React.ElementRef,
27 | React.ComponentPropsWithoutRef
28 | >(({ className, ...props }, ref) => (
29 |
37 | ))
38 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
39 |
40 | const TabsContent = React.forwardRef<
41 | React.ElementRef,
42 | React.ComponentPropsWithoutRef
43 | >(({ className, ...props }, ref) => (
44 |
52 | ))
53 | TabsContent.displayName = TabsPrimitive.Content.displayName
54 |
55 | export { Tabs, TabsList, TabsTrigger, TabsContent }
56 |
--------------------------------------------------------------------------------
/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | export interface TextareaProps
6 | extends React.TextareaHTMLAttributes {}
7 |
8 | const Textarea = React.forwardRef(
9 | ({ className, ...props }, ref) => {
10 | return (
11 |
19 | )
20 | }
21 | )
22 | Textarea.displayName = "Textarea"
23 |
24 | export { Textarea }
25 |
--------------------------------------------------------------------------------
/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const TooltipProvider = TooltipPrimitive.Provider
9 |
10 | const Tooltip = TooltipPrimitive.Root
11 |
12 | const TooltipTrigger = TooltipPrimitive.Trigger
13 |
14 | const TooltipContent = React.forwardRef<
15 | React.ElementRef,
16 | React.ComponentPropsWithoutRef
17 | >(({ className, sideOffset = 4, ...props }, ref) => (
18 |
27 | ))
28 | TooltipContent.displayName = TooltipPrimitive.Content.displayName
29 |
30 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
31 |
--------------------------------------------------------------------------------
/hooks/use-mail.ts:
--------------------------------------------------------------------------------
1 | import { create } from 'zustand';
2 | import { Mail, mails } from '@/lib/data';
3 |
4 | interface MailStore {
5 | selected: Mail['id'] | null;
6 | selectMail: (mailId: Mail['id']) => void;
7 | }
8 |
9 | export const useMailStore = create((set) => ({
10 | selected: mails[0].id,
11 | selectMail: (mailId) => set({ selected: mailId }),
12 | }));
13 |
--------------------------------------------------------------------------------
/hooks/use-origin.tsx:
--------------------------------------------------------------------------------
1 | // safely accessing the window object in nextJs13 little bit complicated
2 |
3 | import { useEffect, useState } from "react";
4 |
5 | // because most server side rendering frameworks don't have a window object and nextJs is one of them so we need to check if the window object is available or not before accessing it
6 |
7 | export const useOrigin = () => {
8 | const [mounted, setMounted] = useState(false);
9 |
10 | const origin =
11 | typeof window !== "undefined" && window.location.origin
12 | ? window.location.origin
13 | : "";
14 |
15 | // For avoiding hydration mismatch error in nextJs we are using useEffect hook to set the mounted state to true after the component is mounted
16 |
17 | useEffect(() => {
18 | setMounted(true);
19 | }, []);
20 |
21 | if (!mounted) return null;
22 | return origin;
23 | };
24 |
--------------------------------------------------------------------------------
/hooks/use-store-modal.tsx:
--------------------------------------------------------------------------------
1 | import { create } from "zustand";
2 |
3 | interface useStoreModalStore {
4 | isOpen: boolean;
5 | onOpen: () => void;
6 | onClose: () => void;
7 | }
8 |
9 | export const useStoreModal = create(set => ({
10 | isOpen: false,
11 | onOpen: () => set({ isOpen: true }),
12 | onClose: () => set({ isOpen: false }),
13 | }));
14 |
--------------------------------------------------------------------------------
/lib/prismadb.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from "@prisma/client";
2 |
3 | declare global {
4 | var prisma: PrismaClient | undefined;
5 | }
6 |
7 | const prismadb = globalThis.prisma || new PrismaClient();
8 |
9 | // if statement whether use global this or intialize new prisma client instance
10 |
11 | if (process.env.NODE_ENV !== "production") {
12 | globalThis.prisma = prismadb;
13 | }
14 |
15 | export default prismadb;
16 |
--------------------------------------------------------------------------------
/lib/stripe.ts:
--------------------------------------------------------------------------------
1 | import Stripe from "stripe";
2 |
3 | export const stripe = new Stripe(process.env.STRIPE_API_KEY!, {
4 | apiVersion: "2023-08-16",
5 | typescript: true,
6 | });
7 |
--------------------------------------------------------------------------------
/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from 'clsx';
2 | import { twMerge } from 'tailwind-merge';
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs));
6 | }
7 |
8 | export const rupeeFormatter = new Intl.NumberFormat('en-IN', {
9 | style: 'currency',
10 | currency: 'INR',
11 | });
12 |
13 | // Create a currency formatter for US dollars (USD)
14 | export const dollarFormatter = new Intl.NumberFormat('en-US', {
15 | style: 'currency', // Format the number as currency
16 | currency: 'USD', // Use US dollars as the currency symbol
17 | });
18 |
--------------------------------------------------------------------------------
/middleware.ts:
--------------------------------------------------------------------------------
1 | import { authMiddleware } from "@clerk/nextjs";
2 |
3 | // This example protects all routes including api/trpc routes
4 | // Please edit this to allow other routes to be public as needed.
5 | // See https://clerk.com/docs/nextjs/middleware for more information about configuring your middleware
6 | export default authMiddleware({
7 | publicRoutes: ["/api/:path*"],
8 | });
9 |
10 | export const config = {
11 | matcher: ["/((?!.*\\..*|_next).*)", "/", "/(api|trpc)(.*)"],
12 | };
13 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | images: {
4 | domains: ["res.cloudinary.com"],
5 | },
6 | };
7 |
8 | module.exports = nextConfig;
9 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ecommerce-admin",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint",
10 | "postinstall": "prisma generate"
11 | },
12 | "dependencies": {
13 | "@clerk/nextjs": "^4.29.3",
14 | "@hookform/resolvers": "^3.3.0",
15 | "@prisma/client": "^5.2.0",
16 | "@radix-ui/react-avatar": "^1.0.4",
17 | "@radix-ui/react-checkbox": "^1.0.4",
18 | "@radix-ui/react-dialog": "^1.0.4",
19 | "@radix-ui/react-dropdown-menu": "^2.0.5",
20 | "@radix-ui/react-label": "^2.0.2",
21 | "@radix-ui/react-popover": "^1.0.6",
22 | "@radix-ui/react-scroll-area": "^1.0.5",
23 | "@radix-ui/react-select": "^1.2.2",
24 | "@radix-ui/react-separator": "^1.0.3",
25 | "@radix-ui/react-slot": "^1.0.2",
26 | "@radix-ui/react-switch": "^1.0.3",
27 | "@radix-ui/react-tabs": "^1.0.4",
28 | "@radix-ui/react-tooltip": "^1.0.7",
29 | "@tanstack/react-table": "^8.9.3",
30 | "@types/node": "20.4.7",
31 | "@types/react": "18.2.18",
32 | "@types/react-dom": "18.2.7",
33 | "autoprefixer": "10.4.14",
34 | "axios": "^1.4.0",
35 | "class-variance-authority": "^0.7.0",
36 | "clsx": "^2.0.0",
37 | "cmdk": "^0.2.0",
38 | "date-fns": "^3.1.0",
39 | "eslint": "8.46.0",
40 | "eslint-config-next": "13.4.12",
41 | "lucide-react": "^0.263.1",
42 | "next": "13.4.12",
43 | "next-cloudinary": "^4.20.0",
44 | "next-themes": "^0.2.1",
45 | "postcss": "8.4.27",
46 | "react": "18.2.0",
47 | "react-day-picker": "^8.10.0",
48 | "react-dom": "18.2.0",
49 | "react-hook-form": "^7.45.4",
50 | "react-hot-toast": "^2.4.1",
51 | "react-resizable-panels": "^1.0.8",
52 | "react-spinners": "^0.13.8",
53 | "recharts": "^2.8.0",
54 | "stripe": "^13.8.0",
55 | "tailwind-merge": "^1.14.0",
56 | "tailwindcss": "3.3.3",
57 | "tailwindcss-animate": "^1.0.6",
58 | "typescript": "5.1.6",
59 | "zod": "^3.22.2",
60 | "zustand": "^4.4.1"
61 | },
62 | "devDependencies": {
63 | "prisma": "^5.2.0"
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/prisma/migrations/20240531104818_init_db_aiven/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateTable
2 | CREATE TABLE `Store` (
3 | `id` VARCHAR(191) NOT NULL,
4 | `name` VARCHAR(191) NOT NULL,
5 | `userId` VARCHAR(191) NOT NULL,
6 | `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
7 | `updatedAt` DATETIME(3) NOT NULL,
8 |
9 | PRIMARY KEY (`id`)
10 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
11 |
12 | -- CreateTable
13 | CREATE TABLE `Billboard` (
14 | `id` VARCHAR(191) NOT NULL,
15 | `storeId` VARCHAR(191) NOT NULL,
16 | `label` VARCHAR(191) NOT NULL,
17 | `imageUrl` VARCHAR(191) NOT NULL,
18 | `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
19 | `updatedAt` DATETIME(3) NOT NULL,
20 |
21 | INDEX `Billboard_storeId_idx`(`storeId`),
22 | PRIMARY KEY (`id`)
23 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
24 |
25 | -- CreateTable
26 | CREATE TABLE `Category` (
27 | `id` VARCHAR(191) NOT NULL,
28 | `storeId` VARCHAR(191) NOT NULL,
29 | `billboardId` VARCHAR(191) NOT NULL,
30 | `name` VARCHAR(191) NOT NULL,
31 | `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
32 | `updatedAt` DATETIME(3) NOT NULL,
33 |
34 | INDEX `Category_storeId_idx`(`storeId`),
35 | INDEX `Category_billboardId_idx`(`billboardId`),
36 | PRIMARY KEY (`id`)
37 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
38 |
39 | -- CreateTable
40 | CREATE TABLE `Size` (
41 | `id` VARCHAR(191) NOT NULL,
42 | `storeId` VARCHAR(191) NOT NULL,
43 | `name` VARCHAR(191) NOT NULL,
44 | `value` VARCHAR(191) NOT NULL,
45 | `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
46 | `updatedAt` DATETIME(3) NOT NULL,
47 |
48 | INDEX `Size_storeId_idx`(`storeId`),
49 | PRIMARY KEY (`id`)
50 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
51 |
52 | -- CreateTable
53 | CREATE TABLE `Color` (
54 | `id` VARCHAR(191) NOT NULL,
55 | `storeId` VARCHAR(191) NOT NULL,
56 | `name` VARCHAR(191) NOT NULL,
57 | `value` VARCHAR(191) NOT NULL,
58 | `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
59 | `updatedAt` DATETIME(3) NOT NULL,
60 |
61 | INDEX `Color_storeId_idx`(`storeId`),
62 | PRIMARY KEY (`id`)
63 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
64 |
65 | -- CreateTable
66 | CREATE TABLE `Product` (
67 | `id` VARCHAR(191) NOT NULL,
68 | `storeId` VARCHAR(191) NOT NULL,
69 | `categoryId` VARCHAR(191) NOT NULL,
70 | `name` VARCHAR(191) NOT NULL,
71 | `price` DECIMAL(65, 30) NOT NULL,
72 | `isFeatured` BOOLEAN NOT NULL DEFAULT false,
73 | `isArchived` BOOLEAN NOT NULL DEFAULT false,
74 | `sizeId` VARCHAR(191) NOT NULL,
75 | `colorId` VARCHAR(191) NOT NULL,
76 | `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
77 | `updatedAt` DATETIME(3) NOT NULL,
78 |
79 | INDEX `Product_storeId_idx`(`storeId`),
80 | INDEX `Product_categoryId_idx`(`categoryId`),
81 | INDEX `Product_sizeId_idx`(`sizeId`),
82 | INDEX `Product_colorId_idx`(`colorId`),
83 | PRIMARY KEY (`id`)
84 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
85 |
86 | -- CreateTable
87 | CREATE TABLE `Image` (
88 | `id` VARCHAR(191) NOT NULL,
89 | `productId` VARCHAR(191) NOT NULL,
90 | `url` VARCHAR(191) NOT NULL,
91 | `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
92 | `updatedAt` DATETIME(3) NOT NULL,
93 |
94 | INDEX `Image_productId_idx`(`productId`),
95 | PRIMARY KEY (`id`)
96 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
97 |
98 | -- CreateTable
99 | CREATE TABLE `Order` (
100 | `id` VARCHAR(191) NOT NULL,
101 | `storeId` VARCHAR(191) NOT NULL,
102 | `isPaid` BOOLEAN NOT NULL DEFAULT false,
103 | `phone` VARCHAR(191) NOT NULL DEFAULT '',
104 | `address` VARCHAR(191) NOT NULL DEFAULT '',
105 | `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
106 | `updatedAt` DATETIME(3) NOT NULL,
107 |
108 | INDEX `Order_storeId_idx`(`storeId`),
109 | PRIMARY KEY (`id`)
110 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
111 |
112 | -- CreateTable
113 | CREATE TABLE `OrderItem` (
114 | `id` VARCHAR(191) NOT NULL,
115 | `orderId` VARCHAR(191) NOT NULL,
116 | `productId` VARCHAR(191) NOT NULL,
117 |
118 | INDEX `OrderItem_orderId_idx`(`orderId`),
119 | INDEX `OrderItem_productId_idx`(`productId`),
120 | PRIMARY KEY (`id`)
121 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
122 |
--------------------------------------------------------------------------------
/prisma/migrations/migration_lock.toml:
--------------------------------------------------------------------------------
1 | # Please do not edit this file manually
2 | # It should be added in your version-control system (i.e. Git)
3 | provider = "mysql"
--------------------------------------------------------------------------------
/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | // This is your Prisma schema file,
2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema
3 |
4 | generator client {
5 | provider = "prisma-client-js"
6 | }
7 |
8 | datasource db {
9 | provider = "mysql"
10 | url = env("DATABASE_URL")
11 | relationMode = "prisma"
12 | }
13 |
14 |
15 | model Store {
16 | id String @id @default(uuid())
17 | name String
18 | userId String
19 | billboards Billboard[] @relation("StoreToBillboard")
20 | categories Category[] @relation("StoreToCategory")
21 | sizes Size[] @relation("StoreToSize")
22 | colors Color[] @relation("StoreToColor")
23 | products Product[] @relation("StoreToProduct")
24 | orders Order[] @relation("StoreToOrder")
25 | createdAt DateTime @default(now())
26 | updatedAt DateTime @updatedAt
27 |
28 | }
29 |
30 |
31 |
32 | model Billboard {
33 | id String @id @default(uuid())
34 | storeId String
35 | store Store @relation("StoreToBillboard", fields: [storeId], references: [id])
36 | label String
37 | imageUrl String
38 | categories Category[]
39 |
40 | createdAt DateTime @default(now())
41 | updatedAt DateTime @updatedAt
42 |
43 | @@index([storeId])
44 |
45 |
46 | }
47 |
48 | model Category {
49 | id String @id @default(uuid())
50 | storeId String // Foreign Key to Store
51 | store Store @relation("StoreToCategory", fields: [storeId], references: [id])
52 | products Product[] @relation("CategoryToProduct")
53 | billboardId String // Foreign Key to Billboard
54 | billboard Billboard @relation(fields: [billboardId], references: [id])
55 | name String
56 |
57 | createdAt DateTime @default(now())
58 | updatedAt DateTime @updatedAt
59 |
60 | @@index([storeId])
61 | @@index([billboardId])
62 | }
63 |
64 |
65 | model Size {
66 | id String @id @default(uuid())
67 | storeId String
68 | store Store @relation("StoreToSize", fields: [storeId], references: [id])
69 | name String
70 | value String
71 | products Product[]
72 | createdAt DateTime @default(now())
73 | updatedAt DateTime @updatedAt
74 |
75 | @@index([storeId])
76 |
77 |
78 | }
79 | model Color {
80 | id String @id @default(uuid())
81 | storeId String
82 | store Store @relation("StoreToColor", fields: [storeId], references: [id])
83 | name String
84 | value String
85 | products Product[]
86 | createdAt DateTime @default(now())
87 | updatedAt DateTime @updatedAt
88 |
89 | @@index([storeId])
90 |
91 |
92 | }
93 |
94 | model Product {
95 | id String @id @default(uuid())
96 | storeId String
97 | store Store @relation("StoreToProduct",fields: [storeId], references: [id])
98 |
99 | categoryId String
100 | category Category @relation("CategoryToProduct", fields: [categoryId], references: [id])
101 |
102 | name String
103 | price Decimal
104 | isFeatured Boolean @default(false)
105 | isArchived Boolean @default(false)
106 | sizeId String
107 | size Size @relation(fields: [sizeId], references: [id])
108 |
109 | colorId String
110 | color Color @relation(fields: [colorId], references: [id])
111 | images Image[] // for multiple images of product
112 | orderItems OrderItem[]
113 | createdAt DateTime @default(now())
114 | updatedAt DateTime @updatedAt
115 |
116 | @@index([storeId])
117 | @@index([categoryId])
118 | @@index([sizeId])
119 | @@index([colorId])
120 |
121 |
122 | }
123 |
124 | model Image {
125 | id String @id @default(uuid())
126 | productId String
127 | product Product @relation(fields: [productId], references: [id] , onDelete: Cascade)
128 | url String
129 | createdAt DateTime @default(now())
130 | updatedAt DateTime @updatedAt
131 |
132 | @@index([productId])
133 |
134 | }
135 |
136 |
137 | model Order {
138 |
139 |
140 | id String @id @default(uuid())
141 |
142 | storeId String
143 | store Store @relation("StoreToOrder",fields: [storeId], references: [id])
144 | orderItems OrderItem[]
145 | isPaid Boolean @default(false)
146 | phone String @default("") // we are filling this after payment
147 | address String @default("") // we are filling this after payment
148 | createdAt DateTime @default(now())
149 | updatedAt DateTime @updatedAt
150 |
151 | @@index([storeId])
152 |
153 |
154 | }
155 |
156 | model OrderItem {
157 | id String @id @default(uuid())
158 | orderId String
159 | order Order @relation(fields: [orderId], references: [id])
160 | productId String
161 | product Product @relation(fields: [productId], references: [id])
162 | @@index([orderId])
163 | @@index([productId])
164 |
165 | }
--------------------------------------------------------------------------------
/providers/modal-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useEffect, useState } from "react";
4 |
5 | import { StoreModal } from "@/components/modals/store-modal";
6 |
7 | export const ModalProvider = () => {
8 | const [isMounted, setIsMounted] = useState(false);
9 |
10 | useEffect(() => {
11 | setIsMounted(true);
12 | }, []);
13 |
14 | // we are in server side meaning we are not rendering any modal on server side
15 | if (!isMounted) {
16 | return null;
17 | }
18 |
19 | return (
20 | <>
21 |
22 | >
23 | );
24 | };
25 |
--------------------------------------------------------------------------------
/providers/theme-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import { ThemeProvider as NextThemesProvider } from "next-themes";
5 | import { type ThemeProviderProps } from "next-themes/dist/types";
6 |
7 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
8 | return {children};
9 | }
10 |
--------------------------------------------------------------------------------
/providers/toast-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Toaster } from "react-hot-toast";
4 |
5 | export const ToastProvider = () => {
6 | return ;
7 | };
8 |
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | darkMode: ["class"],
4 | content: [
5 | './pages/**/*.{ts,tsx}',
6 | './components/**/*.{ts,tsx}',
7 | './app/**/*.{ts,tsx}',
8 | './src/**/*.{ts,tsx}',
9 | ],
10 | theme: {
11 | container: {
12 | center: true,
13 | padding: "2rem",
14 | screens: {
15 | "2xl": "1400px",
16 | },
17 | },
18 | extend: {
19 | colors: {
20 | border: "hsl(var(--border))",
21 | input: "hsl(var(--input))",
22 | ring: "hsl(var(--ring))",
23 | background: "hsl(var(--background))",
24 | foreground: "hsl(var(--foreground))",
25 | primary: {
26 | DEFAULT: "hsl(var(--primary))",
27 | foreground: "hsl(var(--primary-foreground))",
28 | },
29 | secondary: {
30 | DEFAULT: "hsl(var(--secondary))",
31 | foreground: "hsl(var(--secondary-foreground))",
32 | },
33 | destructive: {
34 | DEFAULT: "hsl(var(--destructive))",
35 | foreground: "hsl(var(--destructive-foreground))",
36 | },
37 | muted: {
38 | DEFAULT: "hsl(var(--muted))",
39 | foreground: "hsl(var(--muted-foreground))",
40 | },
41 | accent: {
42 | DEFAULT: "hsl(var(--accent))",
43 | foreground: "hsl(var(--accent-foreground))",
44 | },
45 | popover: {
46 | DEFAULT: "hsl(var(--popover))",
47 | foreground: "hsl(var(--popover-foreground))",
48 | },
49 | card: {
50 | DEFAULT: "hsl(var(--card))",
51 | foreground: "hsl(var(--card-foreground))",
52 | },
53 | },
54 | borderRadius: {
55 | lg: "var(--radius)",
56 | md: "calc(var(--radius) - 2px)",
57 | sm: "calc(var(--radius) - 4px)",
58 | },
59 | keyframes: {
60 | "accordion-down": {
61 | from: { height: 0 },
62 | to: { height: "var(--radix-accordion-content-height)" },
63 | },
64 | "accordion-up": {
65 | from: { height: "var(--radix-accordion-content-height)" },
66 | to: { height: 0 },
67 | },
68 | },
69 | animation: {
70 | "accordion-down": "accordion-down 0.2s ease-out",
71 | "accordion-up": "accordion-up 0.2s ease-out",
72 | },
73 | },
74 | },
75 | plugins: [require("tailwindcss-animate")],
76 | }
--------------------------------------------------------------------------------
/template.config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This file is a configuration file generated by the `Template` extension on `vscode`
3 | * @see https://marketplace.visualstudio.com/items?itemName=yongwoo.template
4 | */
5 | module.exports = {
6 | // You can change the template path to another path
7 | templateRootPath: "./.templates",
8 | // After copying the template file the `replaceFileTextFn` function is executed
9 | replaceFileTextFn: (fileText, templateName, utils) => {
10 | // @see https://www.npmjs.com/package/change-case
11 | const { changeCase } = utils;
12 | // You can change the text in the file
13 | return fileText
14 | .replace(/__templateName__/g, templateName)
15 | .replace(
16 | /__templateNameToPascalCase__/g,
17 | changeCase.pascalCase(templateName)
18 | )
19 | .replace(
20 | /__templateNameToParamCase__/g,
21 | changeCase.paramCase(templateName)
22 | );
23 | },
24 | renameFileFn: (fileName, templateName, utils) => {
25 | const { path } = utils;
26 | const { base } = path.parse(fileName);
27 | return base.replace(/__templateName__/gm, templateName);
28 | },
29 | renameSubDirectoriesFn: (directoryName, templateName, _utils) => {
30 | const { changeCase } = _utils;
31 | const newDirectoryName = changeCase.paramCase(templateName);
32 | return directoryName.replace(/__templateName__/g, newDirectoryName);
33 | }
34 | };
35 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "bundler",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "incremental": true,
17 | "plugins": [
18 | {
19 | "name": "next"
20 | }
21 | ],
22 | "paths": {
23 | "@/*": ["./*"]
24 | }
25 | },
26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
27 | "exclude": ["node_modules"]
28 | }
29 |
--------------------------------------------------------------------------------