diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
new file mode 100644
index 000000000..b36496d5c
--- /dev/null
+++ b/.github/workflows/codeql.yml
@@ -0,0 +1,93 @@
+# For most projects, this workflow file will not need changing; you simply need
+# to commit it to your repository.
+#
+# You may wish to alter this file to override the set of languages analyzed,
+# or to provide custom queries or build logic.
+#
+# ******** NOTE ********
+# We have attempted to detect the languages in your repository. Please check
+# the `language` matrix defined below to confirm you have the correct set of
+# supported CodeQL languages.
+#
+name: "CodeQL"
+
+on:
+ push:
+ branches: [ "dev", "devops", "main", "staging" ]
+ pull_request:
+ branches: [ "dev", "devops", "main", "staging" ]
+ schedule:
+ - cron: '35 14 * * 1'
+
+jobs:
+ analyze:
+ name: Analyze (${{ matrix.language }})
+ # Runner size impacts CodeQL analysis time. To learn more, please see:
+ # - https://gh.io/recommended-hardware-resources-for-running-codeql
+ # - https://gh.io/supported-runners-and-hardware-resources
+ # - https://gh.io/using-larger-runners (GitHub.com only)
+ # Consider using larger runners or machines with greater resources for possible analysis time improvements.
+ runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
+ timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }}
+ permissions:
+ # required for all workflows
+ security-events: write
+
+ # required to fetch internal or private CodeQL packs
+ packages: read
+
+ # only required for workflows in private repositories
+ actions: read
+ contents: read
+
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - language: javascript-typescript
+ build-mode: none
+ # CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift'
+ # Use `c-cpp` to analyze code written in C, C++ or both
+ # Use 'java-kotlin' to analyze code written in Java, Kotlin or both
+ # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
+ # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
+ # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
+ # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
+ # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ # Initializes the CodeQL tools for scanning.
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@v3
+ with:
+ languages: ${{ matrix.language }}
+ build-mode: ${{ matrix.build-mode }}
+ # If you wish to specify custom queries, you can do so here or in a config file.
+ # By default, queries listed here will override any specified in a config file.
+ # Prefix the list here with "+" to use these queries and those in the config file.
+
+ # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
+ # queries: security-extended,security-and-quality
+
+ # If the analyze step fails for one of the languages you are analyzing with
+ # "We were unable to automatically build your code", modify the matrix above
+ # to set the build mode to "manual" for that language. Then modify this step
+ # to build your code.
+ # âšī¸ Command-line programs to run using the OS shell.
+ # đ See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
+ - if: matrix.build-mode == 'manual'
+ shell: bash
+ run: |
+ echo 'If you are using a "manual" build mode for one or more of the' \
+ 'languages you are analyzing, replace this with the commands to build' \
+ 'your code, for example:'
+ echo ' make bootstrap'
+ echo ' make release'
+ exit 1
+
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@v3
+ with:
+ category: "/language:${{matrix.language}}"
diff --git a/src/app/dashboard/(admin)/_components/layout/logo/index.tsx b/src/app/dashboard/(admin)/_components/layout/logo/index.tsx
index 91298a4ea..5cd558f9f 100644
--- a/src/app/dashboard/(admin)/_components/layout/logo/index.tsx
+++ b/src/app/dashboard/(admin)/_components/layout/logo/index.tsx
@@ -5,16 +5,18 @@ const DashboardLogo = () => {
return (
-
+
+
+
{
const currentPath = pathname?.split("/")[2];
return (
-
-
-
-
+
+
+
+
-
+
{navlinks.map((item, index) => (
{item.route}
))}
-
+
-
diff --git a/src/app/dashboard/(user-dashboard)/products/[productId]/form-images/project-logo.tsx b/src/app/dashboard/(user-dashboard)/products/[productId]/form-images/project-logo.tsx
new file mode 100644
index 000000000..c8cffcf8b
--- /dev/null
+++ b/src/app/dashboard/(user-dashboard)/products/[productId]/form-images/project-logo.tsx
@@ -0,0 +1,128 @@
+import { X } from "lucide-react";
+import React, { useState } from "react";
+import { UseFormReturn } from "react-hook-form";
+
+import BlurImage from "~/components/miscellaneous/blur-image";
+import { Button } from "~/components/ui/button";
+import { cn } from "~/lib/utils";
+import { EditProduct } from "../../_components/schema/schema";
+
+type Properties = {
+ form: UseFormReturn;
+ name: string;
+};
+const handleImageChange = (event: React.ChangeEvent) => {
+ event.preventDefault();
+};
+const ProjectLogo = ({ form, name }: Properties) => {
+ const [isDragging, setIsDragging] = useState(false);
+
+ const projectLogo = form.getValues("media");
+
+ const handleDrop = (event: React.DragEvent) => {
+ event.preventDefault();
+ event.stopPropagation();
+
+ const target = event.dataTransfer.files[0] as File;
+
+ if (target.type === "image/gif") {
+ return name;
+ }
+ const event_data = {
+ target: {
+ files: event.dataTransfer.files,
+ },
+ };
+ handleImageChange(
+ event_data as unknown as React.ChangeEvent,
+ );
+ setIsDragging(false);
+ };
+
+ return (
+
+
+
+
+ Uploading... {" "}
+ 10%
+
+
+
+
+ {isDragging && (
+
+ )}
+
+ {projectLogo.url && typeof projectLogo.url === "string" ? (
+
+ ) : (
+
{
+ event.preventDefault();
+ event.stopPropagation();
+ setIsDragging(true);
+ }}
+ onDragLeave={(event) => {
+ event.preventDefault();
+ event.stopPropagation();
+ setIsDragging(false);
+ }}
+ className="grid h-full w-full place-items-center"
+ >
+
+
+ Upload New
+
+ Add Image or drag it here
+
+
+
+ )}
+
+ );
+};
+
+export default ProjectLogo;
diff --git a/src/app/dashboard/(user-dashboard)/products/[productId]/page.tsx b/src/app/dashboard/(user-dashboard)/products/[productId]/page.tsx
new file mode 100644
index 000000000..3d166772b
--- /dev/null
+++ b/src/app/dashboard/(user-dashboard)/products/[productId]/page.tsx
@@ -0,0 +1,16 @@
+import { notFound } from "next/navigation";
+
+import ProductDetailsContent from "./product-details-content";
+
+const ProductDetailsPage = async ({
+ params,
+}: {
+ params: { productId: string };
+}) => {
+ const id = params.productId;
+ if (!id) return notFound();
+
+ return ;
+};
+
+export default ProductDetailsPage;
diff --git a/src/app/dashboard/(user-dashboard)/products/[productId]/product-details-component.tsx b/src/app/dashboard/(user-dashboard)/products/[productId]/product-details-component.tsx
new file mode 100644
index 000000000..926992880
--- /dev/null
+++ b/src/app/dashboard/(user-dashboard)/products/[productId]/product-details-component.tsx
@@ -0,0 +1,468 @@
+"use client";
+
+import { zodResolver } from "@hookform/resolvers/zod";
+import { AnimatePresence, motion } from "framer-motion";
+import { ArrowLeft, Loader, Plus } from "lucide-react";
+import { useRouter } from "next-nprogress-bar";
+import Link from "next/link";
+import { useTransition } from "react";
+import { useForm } from "react-hook-form";
+import { z } from "zod";
+
+import WordCounter from "~/components/miscellaneous/WordCounter";
+import { Button } from "~/components/ui/button";
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "~/components/ui/form";
+import { Input } from "~/components/ui/input";
+import {
+ Select,
+ SelectContent,
+ SelectGroup,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "~/components/ui/select";
+import { Textarea } from "~/components/ui/textarea";
+import { useProducts } from "~/hooks/admin-product/use-products.persistence";
+import { cn, generateId, getCurrentDateTime, simulateDelay } from "~/lib/utils";
+import { ProductTableProperties } from "~/types/admin-product.types";
+import { EditProductSchema, MAX_CHAR } from "../_components/schema/schema";
+import { STOCKS_SELECT } from "../data/categories.mock";
+import { shouldDisableAddStocksButton } from "../data/stocks.mock";
+import ProjectLogo from "./form-images/project-logo";
+
+const ProductDetailsComponent = ({
+ product,
+}: {
+ product: ProductTableProperties;
+}) => {
+ const { addProduct } = useProducts();
+ const [isLoading, startTransition] = useTransition();
+ const router = useRouter();
+
+ const new_product_form = useForm>({
+ defaultValues: {
+ product_name: product.name || "",
+ description: product.description || "",
+ price: "",
+ category: "",
+ quantity: "",
+ media: {
+ url: product.image || "",
+ id: "",
+ },
+ stocks: [
+ {
+ id: "P0001",
+ size: "Small",
+ stock: "0",
+ price: "",
+ },
+ {
+ id: "P0002",
+ size: "Standard",
+ stock: String(product.stock) || "0",
+ price: String(product.price) || "",
+ },
+ {
+ id: "P0003",
+ size: "Large",
+ stock: "0",
+ price: "",
+ },
+ ],
+ },
+ resolver: zodResolver(EditProductSchema),
+ });
+ const FORM_STOCKS = new_product_form.getValues("stocks");
+ const handleAddNewStock = () => {
+ const previousStocks = FORM_STOCKS;
+ new_product_form.setValue("stocks", [
+ ...previousStocks,
+ {
+ id: `S${generateId(5)}`,
+ size: "",
+ stock: "",
+ price: "",
+ },
+ ]);
+ };
+
+ const hasProjectLogo = new_product_form.getValues("media.url");
+ new_product_form.watch("stocks");
+ // DISABLE
+ const disableAddStockButton = shouldDisableAddStocksButton({
+ stocks: FORM_STOCKS,
+ });
+ const onSubmit = async (values: z.infer) => {
+ startTransition(async () => {
+ const date_data = getCurrentDateTime();
+ await simulateDelay(3);
+ addProduct({
+ product_id: `P${generateId(6)}`,
+ category: values.category,
+ description: values.description,
+ name: values.product_name,
+ price: Number(values.price),
+ stock: Number(values.quantity),
+ image: "/product/product-image.webp",
+ status: "in_stock",
+ date_added: date_data.date_added,
+ time: date_data.time,
+ });
+ new_product_form.reset();
+ });
+ };
+
+ return (
+
+
+ );
+};
+
+export default ProductDetailsComponent;
diff --git a/src/app/dashboard/(user-dashboard)/products/[productId]/product-details-content.tsx b/src/app/dashboard/(user-dashboard)/products/[productId]/product-details-content.tsx
new file mode 100644
index 000000000..2c6556361
--- /dev/null
+++ b/src/app/dashboard/(user-dashboard)/products/[productId]/product-details-content.tsx
@@ -0,0 +1,36 @@
+"use client";
+
+import { ChevronRight, Loader } from "lucide-react";
+
+import { useProducts } from "~/hooks/admin-product/use-products.persistence";
+import ProductDetailsComponent from "./product-details-component";
+
+const ProductDetailsContent = ({ productId }: { productId: string }) => {
+ const { products } = useProducts();
+ const product = products?.find((p) => p.product_id === productId);
+
+ if (!product)
+ return (
+
+
+ Loading... {" "}
+ .
+
+
+ );
+
+ return (
+
+
+
+ Products
+
+
+ Product Details
+
+
+
+ );
+};
+
+export default ProductDetailsContent;
diff --git a/src/app/dashboard/(user-dashboard)/products/_components/new-product-modal.tsx b/src/app/dashboard/(user-dashboard)/products/_components/new-product-modal.tsx
index 57132cddc..0fa32827e 100644
--- a/src/app/dashboard/(user-dashboard)/products/_components/new-product-modal.tsx
+++ b/src/app/dashboard/(user-dashboard)/products/_components/new-product-modal.tsx
@@ -29,7 +29,7 @@ import { Textarea } from "~/components/ui/textarea";
import { useProductModal } from "~/hooks/admin-product/use-product.modal";
import { useProducts } from "~/hooks/admin-product/use-products.persistence";
import { cn, generateId, getCurrentDateTime, simulateDelay } from "~/lib/utils";
-import { CATEGORIES } from "../data/categories.moct";
+import { CATEGORIES } from "../data/categories.mock";
import ProjectLogo from "./form-images/project-logo";
import { NewProductSchema } from "./schema/schema";
diff --git a/src/app/dashboard/(user-dashboard)/products/_components/product-body-shadcn.tsx b/src/app/dashboard/(user-dashboard)/products/_components/product-body-shadcn.tsx
index 3bd423007..9bbed4cd8 100644
--- a/src/app/dashboard/(user-dashboard)/products/_components/product-body-shadcn.tsx
+++ b/src/app/dashboard/(user-dashboard)/products/_components/product-body-shadcn.tsx
@@ -1,6 +1,7 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { AnimatePresence, motion } from "framer-motion";
import { Edit2, MoreVertical, Trash } from "lucide-react";
+import { useRouter } from "next-nprogress-bar";
import { useEffect, useRef } from "react";
import BlurImage from "~/components/miscellaneous/blur-image";
@@ -33,6 +34,7 @@ const ProductBodyShadcn = ({
setIsDelete,
} = useProductModal();
const modalReference = useRef(null);
+ const router = useRouter();
useEffect(() => {
if (!isActionModal || !modalReference.current) return;
@@ -62,6 +64,10 @@ const ProductBodyShadcn = ({
updateProductId(id);
setIsDelete(true);
};
+ const handleEditAction = (id: string) => {
+ setIsActionModal(false);
+ router.push(`/dashboard/products/${id}`);
+ };
return (
filteredProducts.length > 0 &&
@@ -142,10 +148,10 @@ const ProductBodyShadcn = ({
)}
>
{product.status === "in_stock" && "In Stock"}
@@ -184,6 +190,7 @@ const ProductBodyShadcn = ({
handleEditAction(product.product_id)}
size={"sm"}
className={cn(
"flex h-8 cursor-pointer items-center justify-start gap-x-2 px-2 py-1 text-xs min-[500px]:text-sm",
diff --git a/src/app/dashboard/(user-dashboard)/products/_components/schema/schema.ts b/src/app/dashboard/(user-dashboard)/products/_components/schema/schema.ts
index 768b851ab..a95dc544f 100644
--- a/src/app/dashboard/(user-dashboard)/products/_components/schema/schema.ts
+++ b/src/app/dashboard/(user-dashboard)/products/_components/schema/schema.ts
@@ -1,11 +1,83 @@
import { z } from "zod";
+export const MAX_CHAR = 170;
+export const EditProductSchema = z.object({
+ product_name: z.string().min(3, { message: "Product name is required" }),
+ description: z
+ .string()
+ .min(3, { message: "Description is required" })
+ .max(MAX_CHAR, { message: "Description is too long" }),
+ category: z.string().min(3, { message: "Availability is required" }),
+ price: z
+ .string()
+ .min(1, { message: "Price is required" })
+ .refine(
+ (value) => {
+ if (typeof Number(value) === "number") {
+ return Number(value) > 0;
+ }
+ return true;
+ },
+ {
+ message: "Price must be a positive number",
+ },
+ ),
+ quantity: z
+ .string()
+ .min(1, { message: "Quantity is required" })
+ .refine(
+ (value) => {
+ if (typeof Number(value) === "number") {
+ return Number(value) > 0;
+ }
+ return true;
+ },
+ {
+ message: "Quantity must be a positive number",
+ },
+ ),
+ media: z.object(
+ {
+ url: z.string(),
+ id: z.string(),
+ },
+ { message: "Media is required" },
+ ),
+ stocks: z.array(
+ z.object({
+ id: z.string(),
+ size: z.string(),
+ stock: z.string().refine(
+ (value) => {
+ if (typeof Number(value) === "number") {
+ return Number(value) > 0;
+ }
+ return true;
+ },
+ {
+ message: "stock is required",
+ },
+ ),
+ price: z.string().refine(
+ (value) => {
+ if (typeof Number(value) === "number") {
+ return Number(value) > 0;
+ }
+ return true;
+ },
+ {
+ message: "price is required",
+ },
+ ),
+ }),
+ ),
+});
export const NewProductSchema = z.object({
product_name: z.string().min(3, { message: "Product name is required" }),
description: z
.string()
.min(3, { message: "Description is required" })
- .max(160, { message: "Description is too long" }),
+ .max(MAX_CHAR, { message: "Description is too long" }),
category: z.string().min(3, { message: "Category is required" }),
price: z
.string()
@@ -45,3 +117,4 @@ export const NewProductSchema = z.object({
});
export type NewProduct = z.infer;
+export type EditProduct = z.infer;
diff --git a/src/app/dashboard/(user-dashboard)/products/data/categories.moct.ts b/src/app/dashboard/(user-dashboard)/products/data/categories.mock.ts
similarity index 62%
rename from src/app/dashboard/(user-dashboard)/products/data/categories.moct.ts
rename to src/app/dashboard/(user-dashboard)/products/data/categories.mock.ts
index 03bc674ce..18c4dc253 100644
--- a/src/app/dashboard/(user-dashboard)/products/data/categories.moct.ts
+++ b/src/app/dashboard/(user-dashboard)/products/data/categories.mock.ts
@@ -10,3 +10,8 @@ export const CATEGORIES: CategoriesType[] = [
{ value: "monitor", label: "Monitor" },
{ value: "audio", label: "Audio" },
];
+export const STOCKS_SELECT: CategoriesType[] = [
+ { value: "in_stock", label: "In Stock" },
+ { value: "low_on_stock", label: "Low on Stock" },
+ { value: "out_of_stock", label: "Out of Stock" },
+];
diff --git a/src/app/dashboard/(user-dashboard)/products/data/stocks.mock.ts b/src/app/dashboard/(user-dashboard)/products/data/stocks.mock.ts
new file mode 100644
index 000000000..ec640d483
--- /dev/null
+++ b/src/app/dashboard/(user-dashboard)/products/data/stocks.mock.ts
@@ -0,0 +1,39 @@
+import { EditProduct } from "../_components/schema/schema";
+
+export const STOCKS: EditProduct["stocks"] = [
+ {
+ id: "P0001",
+ size: "Small",
+ stock: "",
+ price: "",
+ },
+ {
+ id: "P0002",
+ size: "Medium",
+ stock: "",
+ price: "",
+ },
+ {
+ id: "P0003",
+ size: "Large",
+ stock: "",
+ price: "S",
+ },
+];
+
+export const shouldDisableAddStocksButton = ({
+ stocks,
+}: {
+ stocks: typeof STOCKS;
+}) => {
+ if (stocks.length === 0) return false;
+
+ return stocks.some(
+ (stock) =>
+ stock.size!.length === 0 ||
+ stock.stock!.length === 0 ||
+ stock.price!.length === 0,
+ )
+ ? true
+ : false;
+};
diff --git a/src/app/dashboard/(user-dashboard)/products/layout.tsx b/src/app/dashboard/(user-dashboard)/products/layout.tsx
new file mode 100644
index 000000000..74f45465f
--- /dev/null
+++ b/src/app/dashboard/(user-dashboard)/products/layout.tsx
@@ -0,0 +1,27 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+"use client";
+
+import { useEffect } from "react";
+
+import { useProducts } from "~/hooks/admin-product/use-products.persistence";
+import { PRODUCT_TABLE } from "./data/product.mock";
+
+export default function UserProductLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ const { addProducts } = useProducts();
+ useEffect(() => {
+ const is_saved = localStorage.getItem("admin_products");
+ if (is_saved) {
+ const parse_data = JSON.parse(is_saved);
+ if (parse_data.state.products) return;
+ setTimeout(() => {
+ addProducts(PRODUCT_TABLE);
+ }, 5000);
+ }
+ }, []);
+
+ return <>{children}>;
+}
diff --git a/src/app/dashboard/(user-dashboard)/products/page.tsx b/src/app/dashboard/(user-dashboard)/products/page.tsx
index acbff045c..1bb5df0fc 100644
--- a/src/app/dashboard/(user-dashboard)/products/page.tsx
+++ b/src/app/dashboard/(user-dashboard)/products/page.tsx
@@ -6,13 +6,11 @@ import dynamic from "next/dynamic";
import { useEffect, useState } from "react";
import { useProductModal } from "~/hooks/admin-product/use-product.modal";
-import { useProducts } from "~/hooks/admin-product/use-products.persistence";
import NewProductModal from "./_components/new-product-modal";
import ProductDeleteModal from "./_components/product-delete-modal";
import ProductDetailModal from "./_components/product-detail-modal";
import ProductDetailView from "./_components/product-detail-view";
import ProductHeader from "./_components/product-header";
-import { PRODUCT_TABLE } from "./data/product.mock";
const ProductContent = dynamic(() => import("./_components/product-content"), {
ssr: false,
@@ -24,7 +22,7 @@ const ProductFilter = dynamic(() => import("./_components/product-filter"), {
const ProductPage = () => {
const [searchTerm, setSearchTerm] = useState("");
const [view, setView] = useState<"list" | "grid">("list");
- const { addProducts } = useProducts();
+
const {
isOpen,
updateProductId,
@@ -38,16 +36,6 @@ const ProductPage = () => {
setIsDelete,
} = useProductModal();
- useEffect(() => {
- const is_saved = localStorage.getItem("admin_products");
- if (is_saved) {
- const parse_data = JSON.parse(is_saved);
- if (parse_data.state.products) return;
- setTimeout(() => {
- addProducts(PRODUCT_TABLE);
- }, 5000);
- }
- }, []);
useEffect(() => {
const handleEscape = (event: KeyboardEvent) => {
if (event.key === "Escape") {
diff --git a/src/app/dashboard/(user-dashboard)/settings/_components/layout/sidebar/index.tsx b/src/app/dashboard/(user-dashboard)/settings/_components/layout/sidebar/index.tsx
index 4d20d5cff..84b9fb5a0 100644
--- a/src/app/dashboard/(user-dashboard)/settings/_components/layout/sidebar/index.tsx
+++ b/src/app/dashboard/(user-dashboard)/settings/_components/layout/sidebar/index.tsx
@@ -94,7 +94,7 @@ const SettingsSidebar: FC = ({ sideNavitems = sideItems }) => {
const organizationPath = pathname?.split("/")[4];
return (
-
+
diff --git a/src/app/dashboard/(user-dashboard)/settings/layout.tsx b/src/app/dashboard/(user-dashboard)/settings/layout.tsx
index b6b98a2bd..bef3e0f8d 100644
--- a/src/app/dashboard/(user-dashboard)/settings/layout.tsx
+++ b/src/app/dashboard/(user-dashboard)/settings/layout.tsx
@@ -9,7 +9,9 @@ const layout: FC = ({ children }) => {
return (
-
{children}
+
+ {children}
+
);
};
diff --git a/src/app/dashboard/(user-dashboard)/settings/notification/_components/header.tsx b/src/app/dashboard/(user-dashboard)/settings/notification/_components/header.tsx
new file mode 100644
index 000000000..b66562fe0
--- /dev/null
+++ b/src/app/dashboard/(user-dashboard)/settings/notification/_components/header.tsx
@@ -0,0 +1,15 @@
+interface IProperties {
+ notificationTitle: string;
+}
+
+const NotificationHeader = ({ notificationTitle }: IProperties) => {
+ return (
+ {notificationTitle}
+ );
+};
+
+NotificationHeader.PreviewProps = {
+ notificationTitle: `Notification Alert`,
+} satisfies IProperties;
+
+export default NotificationHeader;
diff --git a/src/app/dashboard/(user-dashboard)/settings/notification/_components/notification-switch-box.tsx b/src/app/dashboard/(user-dashboard)/settings/notification/_components/notification-switch-box.tsx
new file mode 100644
index 000000000..9ef72b82d
--- /dev/null
+++ b/src/app/dashboard/(user-dashboard)/settings/notification/_components/notification-switch-box.tsx
@@ -0,0 +1,22 @@
+import { Switch } from "~/components/ui/switch";
+
+interface IProperties {
+ title: string;
+ description: string;
+}
+
+const NotificationSwitchBox = ({ title, description }: IProperties) => {
+ return (
+
+
+
{title}
+
{description}
+
+
+
+
+
+ );
+};
+
+export default NotificationSwitchBox;
diff --git a/src/app/dashboard/(user-dashboard)/settings/notification/page.tsx b/src/app/dashboard/(user-dashboard)/settings/notification/page.tsx
index db4d38750..e6f20ba1f 100644
--- a/src/app/dashboard/(user-dashboard)/settings/notification/page.tsx
+++ b/src/app/dashboard/(user-dashboard)/settings/notification/page.tsx
@@ -1,5 +1,127 @@
-const page = () => {
- return page
;
+"use client";
+
+import { Check } from "lucide-react";
+import { useState } from "react";
+
+import CustomButton from "~/components/common/common-button/common-button";
+import NotificationSettingSavedModal from "~/components/common/modals/notification-settings-saved";
+import NotificationHeader from "./_components/header";
+import NotificationSwitchBox from "./_components/notification-switch-box";
+
+const NotificationPage = () => {
+ const [isOpen, setOpen] = useState(false);
+
+ const handleOpenModal = () => {
+ setOpen(true);
+ };
+
+ return (
+
+ {/* NOTIFICATION ALERT */}
+
+ {/* EMAIL NOTIFICATION */}
+
+
+
+ {/* option 1 */}
+
+
+
+ {/* option 2 */}
+
+
+
+ {/* option 3 */}
+
+
+
+ {/* option 4 */}
+
+
+
+
+
+ {/* SLACK NOTIFICATIONS */}
+
+
+
+ {/* option 1 */}
+
+
+
+ {/* option 2 */}
+
+
+
+ {/* option 3 */}
+
+
+
+
+
+
+ }
+ isLeftIconVisible={true}
+ isLoading={false}
+ isDisabled={false}
+ onClick={handleOpenModal}
+ >
+ Save Changes
+
+
+
+
+ );
};
-export default page;
+export default NotificationPage;
diff --git a/src/app/globals.css b/src/app/globals.css
index 1296c9014..450351d4c 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -169,6 +169,10 @@
width: 14px; /* Width of the scrollbar */
}
+::selection {
+ @apply bg-primary;
+ color: white;
+}
::-webkit-scrollbar-thumb {
@apply bg-primary;
border: 4px solid transparent;
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index a8a667d28..38fe4626a 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -20,7 +20,7 @@ export default function RootLayout({
return (
-
+
{children}
diff --git a/src/components/common/modals/notification-settings-saved/index.tsx b/src/components/common/modals/notification-settings-saved/index.tsx
index b46652e1b..843b23578 100644
--- a/src/components/common/modals/notification-settings-saved/index.tsx
+++ b/src/components/common/modals/notification-settings-saved/index.tsx
@@ -1,5 +1,6 @@
"use client";
+import { usePathname } from "next/navigation";
import React from "react";
import CustomButton from "~/components/common/common-button/common-button";
@@ -20,11 +21,18 @@ const NotificationSettingSavedModal: React.FC
= ({
show,
onClose,
}) => {
+ const pathname = usePathname();
+ /**
+ * @kinxlo
+ * check if pathname == "notification", apply the blur effect
+ */
+ const isNotificationPath = pathname.includes("notification");
+
return (
{
+ return (
+
+ t{username}, your invoice reciept.
+
+
+
+
+
+
+
+
+ {title}
+
+
+
+
+ Hi {username},
+
+
+ {description}
+
+
+ {descriptionOne}
+
+
+
+
+
+
+
+
+
+ Activate Account
+
+
+
+
+
+
+
+ Regards,
+
+ Boilerplate
+
+
+
+
+
+ );
+};
+
+InvoiceTemplate.PreviewProps = {
+ title: "Invoice",
+ username: "John Doe",
+ image: "https://imgur.com/a4XiXxo.png",
+ link: "www.boilerplate.com",
+ description:
+ "We hope you are doing well. Thank you for your recent purchase from Boilerplate. Please find your invoice attached to this email.",
+ descriptionOne:
+ "To activate your account and secure it, please click the button below:",
+} satisfies InvoiceTemplateProperties;
+
+export default InvoiceTemplate;
diff --git a/tsconfig.json b/tsconfig.json
index 62e9bc113..5fc51ec50 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -35,7 +35,7 @@
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
- ],
+, "src/app/dashboard/(user-dashboard)/settings/notification/_components" ],
"exclude": [
"node_modules"
]