init
This commit is contained in:
86
src/app/(admin)/(ui-elements)/alerts/page.tsx
Normal file
86
src/app/(admin)/(ui-elements)/alerts/page.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import ComponentCard from "@/components/common/ComponentCard";
|
||||
import PageBreadcrumb from "@/components/common/PageBreadCrumb";
|
||||
import Alert from "@/components/ui/alert/Alert";
|
||||
import { Metadata } from "next";
|
||||
import React from "react";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Next.js Alerts | TailAdmin - Next.js Dashboard Template",
|
||||
description:
|
||||
"This is Next.js Alerts page for TailAdmin - Next.js Tailwind CSS Admin Dashboard Template",
|
||||
// other metadata
|
||||
};
|
||||
|
||||
export default function Alerts() {
|
||||
return (
|
||||
<div>
|
||||
<PageBreadcrumb pageTitle="Alerts" />
|
||||
<div className="space-y-5 sm:space-y-6">
|
||||
<ComponentCard title="Success Alert">
|
||||
<Alert
|
||||
variant="success"
|
||||
title="Success Message"
|
||||
message="Be cautious when performing this action."
|
||||
showLink={true}
|
||||
linkHref="/"
|
||||
linkText="Learn more"
|
||||
/>
|
||||
<Alert
|
||||
variant="success"
|
||||
title="Success Message"
|
||||
message="Be cautious when performing this action."
|
||||
showLink={false}
|
||||
/>
|
||||
</ComponentCard>
|
||||
<ComponentCard title="Warning Alert">
|
||||
<Alert
|
||||
variant="warning"
|
||||
title="Warning Message"
|
||||
message="Be cautious when performing this action."
|
||||
showLink={true}
|
||||
linkHref="/"
|
||||
linkText="Learn more"
|
||||
/>
|
||||
<Alert
|
||||
variant="warning"
|
||||
title="Warning Message"
|
||||
message="Be cautious when performing this action."
|
||||
showLink={false}
|
||||
/>
|
||||
</ComponentCard>{" "}
|
||||
<ComponentCard title="Error Alert">
|
||||
<Alert
|
||||
variant="error"
|
||||
title="Error Message"
|
||||
message="Be cautious when performing this action."
|
||||
showLink={true}
|
||||
linkHref="/"
|
||||
linkText="Learn more"
|
||||
/>
|
||||
<Alert
|
||||
variant="error"
|
||||
title="Error Message"
|
||||
message="Be cautious when performing this action."
|
||||
showLink={false}
|
||||
/>
|
||||
</ComponentCard>{" "}
|
||||
<ComponentCard title="Info Alert">
|
||||
<Alert
|
||||
variant="info"
|
||||
title="Info Message"
|
||||
message="Be cautious when performing this action."
|
||||
showLink={true}
|
||||
linkHref="/"
|
||||
linkText="Learn more"
|
||||
/>
|
||||
<Alert
|
||||
variant="info"
|
||||
title="Info Message"
|
||||
message="Be cautious when performing this action."
|
||||
showLink={false}
|
||||
/>
|
||||
</ComponentCard>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
126
src/app/(admin)/(ui-elements)/avatars/page.tsx
Normal file
126
src/app/(admin)/(ui-elements)/avatars/page.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
import ComponentCard from "@/components/common/ComponentCard";
|
||||
import PageBreadcrumb from "@/components/common/PageBreadCrumb";
|
||||
import Avatar from "@/components/ui/avatar/Avatar";
|
||||
import { Metadata } from "next";
|
||||
import React from "react";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Next.js Avatars | TailAdmin - Next.js Dashboard Template",
|
||||
description:
|
||||
"This is Next.js Avatars page for TailAdmin - Next.js Tailwind CSS Admin Dashboard Template",
|
||||
};
|
||||
|
||||
export default function AvatarPage() {
|
||||
return (
|
||||
<div>
|
||||
<PageBreadcrumb pageTitle="Avatar" />
|
||||
<div className="space-y-5 sm:space-y-6">
|
||||
<ComponentCard title="Default Avatar">
|
||||
{/* Default Avatar (No Status) */}
|
||||
<div className="flex flex-col items-center justify-center gap-5 sm:flex-row">
|
||||
<Avatar src="/images/user/user-01.jpg" size="xsmall" />
|
||||
<Avatar src="/images/user/user-01.jpg" size="small" />
|
||||
<Avatar src="/images/user/user-01.jpg" size="medium" />
|
||||
<Avatar src="/images/user/user-01.jpg" size="large" />
|
||||
<Avatar src="/images/user/user-01.jpg" size="xlarge" />
|
||||
<Avatar src="/images/user/user-01.jpg" size="xxlarge" />
|
||||
</div>
|
||||
</ComponentCard>
|
||||
<ComponentCard title="Avatar with online indicator">
|
||||
<div className="flex flex-col items-center justify-center gap-5 sm:flex-row">
|
||||
<Avatar
|
||||
src="/images/user/user-01.jpg"
|
||||
size="xsmall"
|
||||
status="online"
|
||||
/>
|
||||
<Avatar
|
||||
src="/images/user/user-01.jpg"
|
||||
size="small"
|
||||
status="online"
|
||||
/>
|
||||
<Avatar
|
||||
src="/images/user/user-01.jpg"
|
||||
size="medium"
|
||||
status="online"
|
||||
/>
|
||||
<Avatar
|
||||
src="/images/user/user-01.jpg"
|
||||
size="large"
|
||||
status="online"
|
||||
/>
|
||||
<Avatar
|
||||
src="/images/user/user-01.jpg"
|
||||
size="xlarge"
|
||||
status="online"
|
||||
/>
|
||||
<Avatar
|
||||
src="/images/user/user-01.jpg"
|
||||
size="xxlarge"
|
||||
status="online"
|
||||
/>
|
||||
</div>
|
||||
</ComponentCard>
|
||||
<ComponentCard title="Avatar with Offline indicator">
|
||||
<div className="flex flex-col items-center justify-center gap-5 sm:flex-row">
|
||||
<Avatar
|
||||
src="/images/user/user-01.jpg"
|
||||
size="xsmall"
|
||||
status="offline"
|
||||
/>
|
||||
<Avatar
|
||||
src="/images/user/user-01.jpg"
|
||||
size="small"
|
||||
status="offline"
|
||||
/>
|
||||
<Avatar
|
||||
src="/images/user/user-01.jpg"
|
||||
size="medium"
|
||||
status="offline"
|
||||
/>
|
||||
<Avatar
|
||||
src="/images/user/user-01.jpg"
|
||||
size="large"
|
||||
status="offline"
|
||||
/>
|
||||
<Avatar
|
||||
src="/images/user/user-01.jpg"
|
||||
size="xlarge"
|
||||
status="offline"
|
||||
/>
|
||||
<Avatar
|
||||
src="/images/user/user-01.jpg"
|
||||
size="xxlarge"
|
||||
status="offline"
|
||||
/>
|
||||
</div>
|
||||
</ComponentCard>{" "}
|
||||
<ComponentCard title="Avatar with busy indicator">
|
||||
<div className="flex flex-col items-center justify-center gap-5 sm:flex-row">
|
||||
<Avatar
|
||||
src="/images/user/user-01.jpg"
|
||||
size="xsmall"
|
||||
status="busy"
|
||||
/>
|
||||
<Avatar src="/images/user/user-01.jpg" size="small" status="busy" />
|
||||
<Avatar
|
||||
src="/images/user/user-01.jpg"
|
||||
size="medium"
|
||||
status="busy"
|
||||
/>
|
||||
<Avatar src="/images/user/user-01.jpg" size="large" status="busy" />
|
||||
<Avatar
|
||||
src="/images/user/user-01.jpg"
|
||||
size="xlarge"
|
||||
status="busy"
|
||||
/>
|
||||
<Avatar
|
||||
src="/images/user/user-01.jpg"
|
||||
size="xxlarge"
|
||||
status="busy"
|
||||
/>
|
||||
</div>
|
||||
</ComponentCard>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
221
src/app/(admin)/(ui-elements)/badge/page.tsx
Normal file
221
src/app/(admin)/(ui-elements)/badge/page.tsx
Normal file
@@ -0,0 +1,221 @@
|
||||
import PageBreadcrumb from "@/components/common/PageBreadCrumb";
|
||||
import Badge from "@/components/ui/badge/Badge";
|
||||
import { PlusIcon } from "@/icons";
|
||||
import { Metadata } from "next";
|
||||
import React from "react";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Next.js Badge | TailAdmin - Next.js Dashboard Template",
|
||||
description:
|
||||
"This is Next.js Badge page for TailAdmin - Next.js Tailwind CSS Admin Dashboard Template",
|
||||
// other metadata
|
||||
};
|
||||
|
||||
export default function BadgePage() {
|
||||
return (
|
||||
<div>
|
||||
<PageBreadcrumb pageTitle="Badges" />
|
||||
<div className="space-y-5 sm:space-y-6">
|
||||
<div className="rounded-2xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03]">
|
||||
<div className="px-6 py-5">
|
||||
<h3 className="text-base font-medium text-gray-800 dark:text-white/90">
|
||||
With Light Background
|
||||
</h3>
|
||||
</div>
|
||||
<div className="p-6 border-t border-gray-100 dark:border-gray-800 xl:p-10">
|
||||
<div className="flex flex-wrap gap-4 sm:items-center sm:justify-center">
|
||||
{/* Light Variant */}
|
||||
<Badge variant="light" color="primary">
|
||||
Primary
|
||||
</Badge>
|
||||
<Badge variant="light" color="success">
|
||||
Success
|
||||
</Badge>{" "}
|
||||
<Badge variant="light" color="error">
|
||||
Error
|
||||
</Badge>{" "}
|
||||
<Badge variant="light" color="warning">
|
||||
Warning
|
||||
</Badge>{" "}
|
||||
<Badge variant="light" color="info">
|
||||
Info
|
||||
</Badge>
|
||||
<Badge variant="light" color="light">
|
||||
Light
|
||||
</Badge>
|
||||
<Badge variant="light" color="dark">
|
||||
Dark
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-2xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03]">
|
||||
<div className="px-6 py-5">
|
||||
<h3 className="text-base font-medium text-gray-800 dark:text-white/90">
|
||||
With Solid Background
|
||||
</h3>
|
||||
</div>
|
||||
<div className="p-6 border-t border-gray-100 dark:border-gray-800 xl:p-10">
|
||||
<div className="flex flex-wrap gap-4 sm:items-center sm:justify-center">
|
||||
{/* Light Variant */}
|
||||
<Badge variant="solid" color="primary">
|
||||
Primary
|
||||
</Badge>
|
||||
<Badge variant="solid" color="success">
|
||||
Success
|
||||
</Badge>{" "}
|
||||
<Badge variant="solid" color="error">
|
||||
Error
|
||||
</Badge>{" "}
|
||||
<Badge variant="solid" color="warning">
|
||||
Warning
|
||||
</Badge>{" "}
|
||||
<Badge variant="solid" color="info">
|
||||
Info
|
||||
</Badge>
|
||||
<Badge variant="solid" color="light">
|
||||
Light
|
||||
</Badge>
|
||||
<Badge variant="solid" color="dark">
|
||||
Dark
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-2xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03]">
|
||||
<div className="px-6 py-5">
|
||||
<h3 className="text-base font-medium text-gray-800 dark:text-white/90">
|
||||
Light Background with Left Icon
|
||||
</h3>
|
||||
</div>
|
||||
<div className="p-6 border-t border-gray-100 dark:border-gray-800 xl:p-10">
|
||||
<div className="flex flex-wrap gap-4 sm:items-center sm:justify-center">
|
||||
<Badge variant="light" color="primary" startIcon={<PlusIcon />}>
|
||||
Primary
|
||||
</Badge>
|
||||
<Badge variant="light" color="success" startIcon={<PlusIcon />}>
|
||||
Success
|
||||
</Badge>{" "}
|
||||
<Badge variant="light" color="error" startIcon={<PlusIcon />}>
|
||||
Error
|
||||
</Badge>{" "}
|
||||
<Badge variant="light" color="warning" startIcon={<PlusIcon />}>
|
||||
Warning
|
||||
</Badge>{" "}
|
||||
<Badge variant="light" color="info" startIcon={<PlusIcon />}>
|
||||
Info
|
||||
</Badge>
|
||||
<Badge variant="light" color="light" startIcon={<PlusIcon />}>
|
||||
Light
|
||||
</Badge>
|
||||
<Badge variant="light" color="dark" startIcon={<PlusIcon />}>
|
||||
Dark
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-2xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03]">
|
||||
<div className="px-6 py-5">
|
||||
<h3 className="text-base font-medium text-gray-800 dark:text-white/90">
|
||||
Solid Background with Left Icon
|
||||
</h3>
|
||||
</div>
|
||||
<div className="p-6 border-t border-gray-100 dark:border-gray-800 xl:p-10">
|
||||
<div className="flex flex-wrap gap-4 sm:items-center sm:justify-center">
|
||||
<Badge variant="solid" color="primary" startIcon={<PlusIcon />}>
|
||||
Primary
|
||||
</Badge>
|
||||
<Badge variant="solid" color="success" startIcon={<PlusIcon />}>
|
||||
Success
|
||||
</Badge>{" "}
|
||||
<Badge variant="solid" color="error" startIcon={<PlusIcon />}>
|
||||
Error
|
||||
</Badge>{" "}
|
||||
<Badge variant="solid" color="warning" startIcon={<PlusIcon />}>
|
||||
Warning
|
||||
</Badge>{" "}
|
||||
<Badge variant="solid" color="info" startIcon={<PlusIcon />}>
|
||||
Info
|
||||
</Badge>
|
||||
<Badge variant="solid" color="light" startIcon={<PlusIcon />}>
|
||||
Light
|
||||
</Badge>
|
||||
<Badge variant="solid" color="dark" startIcon={<PlusIcon />}>
|
||||
Dark
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-2xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03]">
|
||||
<div className="px-6 py-5">
|
||||
<h3 className="text-base font-medium text-gray-800 dark:text-white/90">
|
||||
Light Background with Right Icon
|
||||
</h3>
|
||||
</div>
|
||||
<div className="p-6 border-t border-gray-100 dark:border-gray-800 xl:p-10">
|
||||
<div className="flex flex-wrap gap-4 sm:items-center sm:justify-center">
|
||||
<Badge variant="light" color="primary" endIcon={<PlusIcon />}>
|
||||
Primary
|
||||
</Badge>
|
||||
<Badge variant="light" color="success" endIcon={<PlusIcon />}>
|
||||
Success
|
||||
</Badge>{" "}
|
||||
<Badge variant="light" color="error" endIcon={<PlusIcon />}>
|
||||
Error
|
||||
</Badge>{" "}
|
||||
<Badge variant="light" color="warning" endIcon={<PlusIcon />}>
|
||||
Warning
|
||||
</Badge>{" "}
|
||||
<Badge variant="light" color="info" endIcon={<PlusIcon />}>
|
||||
Info
|
||||
</Badge>
|
||||
<Badge variant="light" color="light" endIcon={<PlusIcon />}>
|
||||
Light
|
||||
</Badge>
|
||||
<Badge variant="light" color="dark" endIcon={<PlusIcon />}>
|
||||
Dark
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-2xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03]">
|
||||
<div className="px-6 py-5">
|
||||
<h3 className="text-base font-medium text-gray-800 dark:text-white/90">
|
||||
Solid Background with Right Icon
|
||||
</h3>
|
||||
</div>
|
||||
<div className="p-6 border-t border-gray-100 dark:border-gray-800 xl:p-10">
|
||||
<div className="flex flex-wrap gap-4 sm:items-center sm:justify-center">
|
||||
<Badge variant="solid" color="primary" endIcon={<PlusIcon />}>
|
||||
Primary
|
||||
</Badge>
|
||||
<Badge variant="solid" color="success" endIcon={<PlusIcon />}>
|
||||
Success
|
||||
</Badge>{" "}
|
||||
<Badge variant="solid" color="error" endIcon={<PlusIcon />}>
|
||||
Error
|
||||
</Badge>{" "}
|
||||
<Badge variant="solid" color="warning" endIcon={<PlusIcon />}>
|
||||
Warning
|
||||
</Badge>{" "}
|
||||
<Badge variant="solid" color="info" endIcon={<PlusIcon />}>
|
||||
Info
|
||||
</Badge>
|
||||
<Badge variant="solid" color="light" endIcon={<PlusIcon />}>
|
||||
Light
|
||||
</Badge>
|
||||
<Badge variant="solid" color="dark" endIcon={<PlusIcon />}>
|
||||
Dark
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
89
src/app/(admin)/(ui-elements)/buttons/page.tsx
Normal file
89
src/app/(admin)/(ui-elements)/buttons/page.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import ComponentCard from "@/components/common/ComponentCard";
|
||||
import PageBreadcrumb from "@/components/common/PageBreadCrumb";
|
||||
import Button from "@/components/ui/button/Button";
|
||||
import { BoxIcon } from "@/icons";
|
||||
import { Metadata } from "next";
|
||||
import React from "react";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Next.js Buttons | TailAdmin - Next.js Dashboard Template",
|
||||
description:
|
||||
"This is Next.js Buttons page for TailAdmin - Next.js Tailwind CSS Admin Dashboard Template",
|
||||
};
|
||||
|
||||
export default function Buttons() {
|
||||
return (
|
||||
<div>
|
||||
<PageBreadcrumb pageTitle="Buttons" />
|
||||
<div className="space-y-5 sm:space-y-6">
|
||||
{/* Primary Button */}
|
||||
<ComponentCard title="Primary Button">
|
||||
<div className="flex items-center gap-5">
|
||||
<Button size="sm" variant="primary">
|
||||
Button Text
|
||||
</Button>
|
||||
<Button size="md" variant="primary">
|
||||
Button Text
|
||||
</Button>
|
||||
</div>
|
||||
</ComponentCard>
|
||||
{/* Primary Button with Start Icon */}
|
||||
<ComponentCard title="Primary Button with Left Icon">
|
||||
<div className="flex items-center gap-5">
|
||||
<Button size="sm" variant="primary" startIcon={<BoxIcon />}>
|
||||
Button Text
|
||||
</Button>
|
||||
<Button size="md" variant="primary" startIcon={<BoxIcon />}>
|
||||
Button Text
|
||||
</Button>
|
||||
</div>
|
||||
</ComponentCard>{" "}
|
||||
{/* Primary Button with Start Icon */}
|
||||
<ComponentCard title="Primary Button with Right Icon">
|
||||
<div className="flex items-center gap-5">
|
||||
<Button size="sm" variant="primary" endIcon={<BoxIcon />}>
|
||||
Button Text
|
||||
</Button>
|
||||
<Button size="md" variant="primary" endIcon={<BoxIcon />}>
|
||||
Button Text
|
||||
</Button>
|
||||
</div>
|
||||
</ComponentCard>
|
||||
{/* Outline Button */}
|
||||
<ComponentCard title="Secondary Button">
|
||||
<div className="flex items-center gap-5">
|
||||
{/* Outline Button */}
|
||||
<Button size="sm" variant="outline">
|
||||
Button Text
|
||||
</Button>
|
||||
<Button size="md" variant="outline">
|
||||
Button Text
|
||||
</Button>
|
||||
</div>
|
||||
</ComponentCard>
|
||||
{/* Outline Button with Start Icon */}
|
||||
<ComponentCard title="Outline Button with Left Icon">
|
||||
<div className="flex items-center gap-5">
|
||||
<Button size="sm" variant="outline" startIcon={<BoxIcon />}>
|
||||
Button Text
|
||||
</Button>
|
||||
<Button size="md" variant="outline" startIcon={<BoxIcon />}>
|
||||
Button Text
|
||||
</Button>
|
||||
</div>
|
||||
</ComponentCard>{" "}
|
||||
{/* Outline Button with Start Icon */}
|
||||
<ComponentCard title="Outline Button with Right Icon">
|
||||
<div className="flex items-center gap-5">
|
||||
<Button size="sm" variant="outline" endIcon={<BoxIcon />}>
|
||||
Button Text
|
||||
</Button>
|
||||
<Button size="md" variant="outline" endIcon={<BoxIcon />}>
|
||||
Button Text
|
||||
</Button>
|
||||
</div>
|
||||
</ComponentCard>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
33
src/app/(admin)/(ui-elements)/images/page.tsx
Normal file
33
src/app/(admin)/(ui-elements)/images/page.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import ComponentCard from "@/components/common/ComponentCard";
|
||||
import PageBreadcrumb from "@/components/common/PageBreadCrumb";
|
||||
import ResponsiveImage from "@/components/ui/images/ResponsiveImage";
|
||||
import ThreeColumnImageGrid from "@/components/ui/images/ThreeColumnImageGrid";
|
||||
import TwoColumnImageGrid from "@/components/ui/images/TwoColumnImageGrid";
|
||||
import { Metadata } from "next";
|
||||
import React from "react";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Next.js Images | TailAdmin - Next.js Dashboard Template",
|
||||
description:
|
||||
"This is Next.js Images page for TailAdmin - Next.js Tailwind CSS Admin Dashboard Template",
|
||||
// other metadata
|
||||
};
|
||||
|
||||
export default function Images() {
|
||||
return (
|
||||
<div>
|
||||
<PageBreadcrumb pageTitle="Images" />
|
||||
<div className="space-y-5 sm:space-y-6">
|
||||
<ComponentCard title="Responsive image">
|
||||
<ResponsiveImage />
|
||||
</ComponentCard>
|
||||
<ComponentCard title="Image in 2 Grid">
|
||||
<TwoColumnImageGrid />
|
||||
</ComponentCard>
|
||||
<ComponentCard title="Image in 3 Grid">
|
||||
<ThreeColumnImageGrid />
|
||||
</ComponentCard>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
30
src/app/(admin)/(ui-elements)/modals/page.tsx
Normal file
30
src/app/(admin)/(ui-elements)/modals/page.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import PageBreadcrumb from "@/components/common/PageBreadCrumb";
|
||||
import DefaultModal from "@/components/example/ModalExample/DefaultModal";
|
||||
import FormInModal from "@/components/example/ModalExample/FormInModal";
|
||||
import FullScreenModal from "@/components/example/ModalExample/FullScreenModal";
|
||||
import ModalBasedAlerts from "@/components/example/ModalExample/ModalBasedAlerts";
|
||||
import VerticallyCenteredModal from "@/components/example/ModalExample/VerticallyCenteredModal";
|
||||
import { Metadata } from "next";
|
||||
import React from "react";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Next.js Modals | TailAdmin - Next.js Dashboard Template",
|
||||
description:
|
||||
"This is Next.js Modals page for TailAdmin - Next.js Tailwind CSS Admin Dashboard Template",
|
||||
// other metadata
|
||||
};
|
||||
|
||||
export default function Modals() {
|
||||
return (
|
||||
<div>
|
||||
<PageBreadcrumb pageTitle="Modals" />
|
||||
<div className="grid grid-cols-1 gap-5 xl:grid-cols-2 xl:gap-6">
|
||||
<DefaultModal />
|
||||
<VerticallyCenteredModal />
|
||||
<FormInModal />
|
||||
<FullScreenModal />
|
||||
<ModalBasedAlerts />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
20
src/app/(admin)/(ui-elements)/videos/page.tsx
Normal file
20
src/app/(admin)/(ui-elements)/videos/page.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import PageBreadcrumb from "@/components/common/PageBreadCrumb";
|
||||
import VideosExample from "@/components/ui/video/VideosExample";
|
||||
import { Metadata } from "next";
|
||||
import React from "react";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Next.js Videos | TailAdmin - Next.js Dashboard Template",
|
||||
description:
|
||||
"This is Next.js Videos page for TailAdmin - Next.js Tailwind CSS Admin Dashboard Template",
|
||||
};
|
||||
|
||||
export default function VideoPage() {
|
||||
return (
|
||||
<div>
|
||||
<PageBreadcrumb pageTitle="Videos" />
|
||||
|
||||
<VideosExample />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
56
src/app/(admin)/layout.tsx
Normal file
56
src/app/(admin)/layout.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
"use client";
|
||||
|
||||
import { useSidebar } from "@/context/SidebarContext";
|
||||
import AppHeader from "@/layout/AppHeader";
|
||||
import AppSidebar from "@/layout/AppSidebar";
|
||||
import Backdrop from "@/layout/Backdrop";
|
||||
import { apiGetCurrentUser } from "@/service/auth";
|
||||
import { setUserData } from "@/store/features/userSlice";
|
||||
import React, { useEffect } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
|
||||
export default function AdminLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const { isExpanded, isHovered, isMobileOpen } = useSidebar();
|
||||
const dispatch = useDispatch()
|
||||
|
||||
useEffect(() => {
|
||||
const fetchUser = async () => {
|
||||
try {
|
||||
const userData = await apiGetCurrentUser();
|
||||
dispatch(setUserData(userData.data));
|
||||
} catch (err) {
|
||||
console.error("Lỗi:", err);
|
||||
}
|
||||
};
|
||||
fetchUser();
|
||||
}, [])
|
||||
|
||||
|
||||
// Dynamic class for main content margin based on sidebar state
|
||||
const mainContentMargin = isMobileOpen
|
||||
? "ml-0"
|
||||
: isExpanded || isHovered
|
||||
? "lg:ml-[290px]"
|
||||
: "lg:ml-[90px]";
|
||||
|
||||
return (
|
||||
<div className="min-h-screen xl:flex">
|
||||
{/* Sidebar and Backdrop */}
|
||||
<AppSidebar />
|
||||
<Backdrop />
|
||||
{/* Main Content Area */}
|
||||
<div
|
||||
className={`flex-1 transition-all duration-300 ease-in-out ${mainContentMargin}`}
|
||||
>
|
||||
{/* Header */}
|
||||
<AppHeader />
|
||||
{/* Page Content */}
|
||||
<div className="p-4 mx-auto max-w-(--breakpoint-2xl) md:p-6">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
42
src/app/(admin)/page.tsx
Normal file
42
src/app/(admin)/page.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import type { Metadata } from "next";
|
||||
import { EcommerceMetrics } from "@/components/ecommerce/EcommerceMetrics";
|
||||
import React from "react";
|
||||
import MonthlyTarget from "@/components/ecommerce/MonthlyTarget";
|
||||
import MonthlySalesChart from "@/components/ecommerce/MonthlySalesChart";
|
||||
import StatisticsChart from "@/components/ecommerce/StatisticsChart";
|
||||
import RecentOrders from "@/components/ecommerce/RecentOrders";
|
||||
import DemographicCard from "@/components/ecommerce/DemographicCard";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title:
|
||||
"Admin Dashboard",
|
||||
description: "This is Dashboard Home for History Web",
|
||||
};
|
||||
|
||||
export default function Ecommerce() {
|
||||
return (
|
||||
<div className="grid grid-cols-12 gap-4 md:gap-6">
|
||||
<div className="col-span-12 space-y-6 xl:col-span-7">
|
||||
<EcommerceMetrics />
|
||||
|
||||
<MonthlySalesChart />
|
||||
</div>
|
||||
|
||||
<div className="col-span-12 xl:col-span-5">
|
||||
<MonthlyTarget />
|
||||
</div>
|
||||
|
||||
<div className="col-span-12">
|
||||
<StatisticsChart />
|
||||
</div>
|
||||
|
||||
<div className="col-span-12 xl:col-span-5">
|
||||
<DemographicCard />
|
||||
</div>
|
||||
|
||||
<div className="col-span-12 xl:col-span-7">
|
||||
<RecentOrders />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
44
src/app/(full-width-pages)/(auth)/layout.tsx
Normal file
44
src/app/(full-width-pages)/(auth)/layout.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import GridShape from "@/components/common/GridShape";
|
||||
import ThemeTogglerTwo from "@/components/common/ThemeTogglerTwo";
|
||||
|
||||
import { ThemeProvider } from "@/context/ThemeContext";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import React from "react";
|
||||
|
||||
export default function AuthLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="relative p-6 bg-white z-1 dark:bg-gray-900 sm:p-0">
|
||||
<ThemeProvider>
|
||||
<div className="relative flex lg:flex-row w-full h-screen justify-center flex-col dark:bg-gray-900 sm:p-0">
|
||||
{children}
|
||||
{/* <div className="lg:w-1/2 w-full h-full bg-brand-950 dark:bg-white/5 lg:grid items-center hidden">
|
||||
<div className="relative items-center justify-center flex z-1">
|
||||
<GridShape />
|
||||
<div className="flex flex-col items-center max-w-xs">
|
||||
<Link href="/" className="block mb-4">
|
||||
<Image
|
||||
width={231}
|
||||
height={48}
|
||||
src="./images/logo/auth-logo.svg"
|
||||
alt="Logo"
|
||||
/>
|
||||
</Link>
|
||||
<p className="text-center text-gray-400 dark:text-white/60">
|
||||
Free and Open-Source Tailwind CSS Admin Dashboard Template
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
<div className="fixed bottom-6 right-6 z-50 hidden sm:block">
|
||||
<ThemeTogglerTwo />
|
||||
</div>
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
239
src/app/(full-width-pages)/(auth)/reset-password/page.tsx
Normal file
239
src/app/(full-width-pages)/(auth)/reset-password/page.tsx
Normal file
@@ -0,0 +1,239 @@
|
||||
"use client";
|
||||
import Input from "@/components/form/input/InputField";
|
||||
import Label from "@/components/form/Label";
|
||||
import { ChevronLeftIcon, EyeCloseIcon, EyeIcon } from "@/icons";
|
||||
import { apiCreateOTP, apiVerifyOTP, apiResetPassword } from "@/service/auth";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export default function ResetPasswordForm() {
|
||||
const [step, setStep] = useState(1);
|
||||
const [email, setEmail] = useState("");
|
||||
const [otp, setOtp] = useState("");
|
||||
const [newPassword, setNewPassword] = useState("");
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
const [errorMsg, setErrorMsg] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const isValidEmail = (email: string) => {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailRegex.test(email);
|
||||
};
|
||||
|
||||
const isValidPassword = (pass: string) => {
|
||||
const passwordRegex = /^(?=.*[A-Z])(?=.*\d)(?=.*[\W_]).{8,}$/;
|
||||
return passwordRegex.test(pass);
|
||||
};
|
||||
|
||||
const handleSendOtp = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setErrorMsg("");
|
||||
|
||||
if (!email.trim()) {
|
||||
setErrorMsg("Vui lòng nhập email.");
|
||||
return;
|
||||
}
|
||||
if (!isValidEmail(email)) {
|
||||
setErrorMsg("Email không đúng định dạng.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
await apiCreateOTP(email);
|
||||
toast.success("Mã OTP đã được gửi đến email của bạn!");
|
||||
setStep(2);
|
||||
} catch (error) {
|
||||
setErrorMsg("Lỗi khi gửi OTP. Vui lòng kiểm tra lại email.");
|
||||
toast.error("Gửi OTP thất bại.");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleResetPassword = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setErrorMsg("");
|
||||
|
||||
if (!otp.trim()) {
|
||||
setErrorMsg("Vui lòng nhập mã OTP.");
|
||||
return;
|
||||
}
|
||||
if (!isValidPassword(newPassword)) {
|
||||
setErrorMsg("Mật khẩu chưa đủ điều kiện bảo mật.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
const verifyRes = await apiVerifyOTP(email, otp);
|
||||
const tokenId = verifyRes?.data?.token_id;
|
||||
|
||||
if (!tokenId) {
|
||||
throw new Error("OTP không hợp lệ hoặc đã hết hạn.");
|
||||
}
|
||||
|
||||
const resetPayload = {
|
||||
email: email,
|
||||
new_password: newPassword,
|
||||
token_id: tokenId,
|
||||
};
|
||||
|
||||
await apiResetPassword(resetPayload);
|
||||
|
||||
toast.success("Đổi mật khẩu thành công!");
|
||||
window.location.href = "/signin";
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : "Đổi mật khẩu thất bại.";
|
||||
setErrorMsg(errorMessage);
|
||||
console.error("Reset password error:", error);
|
||||
toast.error("Vui lòng kiểm tra lại mã OTP.");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-1 w-full lg:w-1/2 overflow-y-auto no-scrollbar">
|
||||
<div className="w-full max-w-md mx-auto mb-5 sm:pt-10">
|
||||
<Link
|
||||
href="/signin"
|
||||
className="inline-flex items-center text-sm text-gray-500 transition-colors hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
|
||||
>
|
||||
<ChevronLeftIcon />
|
||||
Back to Sign In
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col justify-center flex-1 w-full max-w-md mx-auto">
|
||||
<div>
|
||||
<div className="mb-5 sm:mb-8">
|
||||
<h1 className="mb-2 font-semibold text-gray-800 text-title-sm dark:text-white/90 sm:text-title-md">
|
||||
{step === 1 ? "Forgot Password" : "Set New Password"}
|
||||
</h1>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{step === 1
|
||||
? "Enter your email address to receive an OTP code."
|
||||
: `We sent an OTP to ${email}. Please enter it along with your new password.`}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{errorMsg && (
|
||||
<div className="p-3 mb-4 text-sm rounded text-error-500 bg-error-50 dark:bg-error-500/10">
|
||||
{errorMsg}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 1 && (
|
||||
<form onSubmit={handleSendOtp}>
|
||||
<div className="space-y-5">
|
||||
<div>
|
||||
<Label>
|
||||
Email <span className="text-error-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
type="email"
|
||||
name="email"
|
||||
defaultValue={email}
|
||||
onChange={(e) => {
|
||||
setEmail(e.target.value);
|
||||
setErrorMsg("");
|
||||
}}
|
||||
placeholder="Enter your registered email"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading || !email.trim()}
|
||||
className={`flex items-center justify-center w-full px-4 py-3 text-sm font-medium text-white transition rounded-lg shadow-theme-xs
|
||||
${loading || !email.trim() ? "bg-gray-400 cursor-not-allowed" : "bg-brand-500 hover:bg-brand-600"}`}
|
||||
>
|
||||
{loading ? "Sending OTP..." : "Send Reset Code"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
|
||||
{step === 2 && (
|
||||
<form onSubmit={handleResetPassword}>
|
||||
<div className="space-y-5">
|
||||
<div>
|
||||
<Label>
|
||||
OTP Code <span className="text-error-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
type="text"
|
||||
name="otp"
|
||||
defaultValue={otp}
|
||||
onChange={(e) => {
|
||||
setOtp(e.target.value);
|
||||
setErrorMsg("");
|
||||
}}
|
||||
placeholder="Enter the 6-digit code"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>
|
||||
New Password <span className="text-error-500">*</span>
|
||||
</Label>
|
||||
<div
|
||||
className={`relative ${newPassword.length > 0 && !isValidPassword(newPassword) ? "border border-red-500 ring-1 ring-red-500 rounded-lg" : ""}`}
|
||||
>
|
||||
<Input
|
||||
name="newPassword"
|
||||
defaultValue={newPassword}
|
||||
onChange={(e) => {
|
||||
setNewPassword(e.target.value);
|
||||
setErrorMsg("");
|
||||
}}
|
||||
placeholder="Min. 8 characters"
|
||||
type={showPassword ? "text" : "password"}
|
||||
/>
|
||||
<span
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute z-30 -translate-y-1/2 cursor-pointer right-4 top-1/2"
|
||||
>
|
||||
{showPassword ? <EyeIcon /> : <EyeCloseIcon />}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p
|
||||
className={`mt-2 text-xs ${newPassword.length === 0 ? "text-gray-400" : isValidPassword(newPassword) ? "text-green-500" : "text-red-500"}`}
|
||||
>
|
||||
Mật khẩu phải chứa tối thiểu 8 ký tự, 1 chữ cái in hoa, 1 chữ số và 1 ký tự đặc biệt.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 pt-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setStep(1)}
|
||||
className="flex items-center justify-center w-1/3 px-4 py-3 text-sm font-medium text-gray-700 transition bg-gray-200 rounded-lg hover:bg-gray-300 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading || !isValidPassword(newPassword)}
|
||||
className={`flex items-center justify-center w-2/3 px-4 py-3 text-sm font-medium text-white transition rounded-lg shadow-theme-xs
|
||||
${loading || !isValidPassword(newPassword) ? "bg-brand-400 opacity-70 cursor-not-allowed" : "bg-brand-500 hover:bg-brand-600"}`}
|
||||
>
|
||||
{loading ? "Resetting..." : "Reset Password"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
11
src/app/(full-width-pages)/(auth)/signin/page.tsx
Normal file
11
src/app/(full-width-pages)/(auth)/signin/page.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import SignInForm from "@/components/auth/SignInForm";
|
||||
import { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Next.js SignIn Page | TailAdmin - Next.js Dashboard Template",
|
||||
description: "This is Next.js Signin Page TailAdmin Dashboard Template",
|
||||
};
|
||||
|
||||
export default function SignIn() {
|
||||
return <SignInForm />;
|
||||
}
|
||||
12
src/app/(full-width-pages)/(auth)/signup/page.tsx
Normal file
12
src/app/(full-width-pages)/(auth)/signup/page.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import SignUpForm from "@/components/auth/SignUpForm";
|
||||
import { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Next.js SignUp Page | TailAdmin - Next.js Dashboard Template",
|
||||
description: "This is Next.js SignUp Page TailAdmin Dashboard Template",
|
||||
// other metadata
|
||||
};
|
||||
|
||||
export default function SignUp() {
|
||||
return <SignUpForm />;
|
||||
}
|
||||
54
src/app/(full-width-pages)/(error-pages)/error-404/page.tsx
Normal file
54
src/app/(full-width-pages)/(error-pages)/error-404/page.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import GridShape from "@/components/common/GridShape";
|
||||
import { Metadata } from "next";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import React from "react";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Next.js Error 404 | TailAdmin - Next.js Dashboard Template",
|
||||
description:
|
||||
"This is Next.js Error 404 page for TailAdmin - Next.js Tailwind CSS Admin Dashboard Template",
|
||||
};
|
||||
|
||||
export default function Error404() {
|
||||
return (
|
||||
<div className="relative flex flex-col items-center justify-center min-h-screen p-6 overflow-hidden z-1">
|
||||
<GridShape />
|
||||
<div className="mx-auto w-full max-w-[242px] text-center sm:max-w-[472px]">
|
||||
<h1 className="mb-8 font-bold text-gray-800 text-title-md dark:text-white/90 xl:text-title-2xl">
|
||||
ERROR
|
||||
</h1>
|
||||
|
||||
<Image
|
||||
src="/images/error/404.svg"
|
||||
alt="404"
|
||||
className="dark:hidden"
|
||||
width={472}
|
||||
height={152}
|
||||
/>
|
||||
<Image
|
||||
src="/images/error/404-dark.svg"
|
||||
alt="404"
|
||||
className="hidden dark:block"
|
||||
width={472}
|
||||
height={152}
|
||||
/>
|
||||
|
||||
<p className="mt-10 mb-6 text-base text-gray-700 dark:text-gray-400 sm:text-lg">
|
||||
We can’t seem to find the page you are looking for!
|
||||
</p>
|
||||
|
||||
<Link
|
||||
href="/"
|
||||
className="inline-flex items-center justify-center rounded-lg border border-gray-300 bg-white px-5 py-3.5 text-sm font-medium text-gray-700 shadow-theme-xs hover:bg-gray-50 hover:text-gray-800 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03] dark:hover:text-gray-200"
|
||||
>
|
||||
Back to Home Page
|
||||
</Link>
|
||||
</div>
|
||||
{/* <!-- Footer --> */}
|
||||
<p className="absolute text-sm text-center text-gray-500 -translate-x-1/2 bottom-6 left-1/2 dark:text-gray-400">
|
||||
© {new Date().getFullYear()} - TailAdmin
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
7
src/app/(full-width-pages)/layout.tsx
Normal file
7
src/app/(full-width-pages)/layout.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
export default function FullWidthPageLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return <div>{children}</div>;
|
||||
}
|
||||
BIN
src/app/favicon.ico
Normal file
BIN
src/app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
822
src/app/globals.css
Normal file
822
src/app/globals.css
Normal file
@@ -0,0 +1,822 @@
|
||||
@import 'tailwindcss';
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@theme {
|
||||
--font-*: initial;
|
||||
--font-outfit: Outfit, sans-serif;
|
||||
--font-inter: "Inter", ui-sans-serif, system-ui, sans-serif;
|
||||
|
||||
--breakpoint-*: initial;
|
||||
--breakpoint-2xsm: 375px;
|
||||
--breakpoint-xsm: 425px;
|
||||
--breakpoint-3xl: 2000px;
|
||||
--breakpoint-sm: 640px;
|
||||
--breakpoint-md: 768px;
|
||||
--breakpoint-lg: 1024px;
|
||||
--breakpoint-xl: 1280px;
|
||||
--breakpoint-2xl: 1536px;
|
||||
|
||||
--text-title-2xl: 72px;
|
||||
--text-title-2xl--line-height: 90px;
|
||||
--text-title-xl: 60px;
|
||||
--text-title-xl--line-height: 72px;
|
||||
--text-title-lg: 48px;
|
||||
--text-title-lg--line-height: 60px;
|
||||
--text-title-md: 36px;
|
||||
--text-title-md--line-height: 44px;
|
||||
--text-title-sm: 30px;
|
||||
--text-title-sm--line-height: 38px;
|
||||
--text-theme-xl: 20px;
|
||||
--text-theme-xl--line-height: 30px;
|
||||
--text-theme-sm: 14px;
|
||||
--text-theme-sm--line-height: 20px;
|
||||
--text-theme-xs: 12px;
|
||||
--text-theme-xs--line-height: 18px;
|
||||
|
||||
--color-current: currentColor;
|
||||
--color-transparent: transparent;
|
||||
--color-white: #ffffff;
|
||||
--color-black: #101828;
|
||||
|
||||
--color-brand-25: #f2f7ff;
|
||||
--color-brand-50: #ecf3ff;
|
||||
--color-brand-100: #dde9ff;
|
||||
--color-brand-200: #c2d6ff;
|
||||
--color-brand-300: #9cb9ff;
|
||||
--color-brand-400: #7592ff;
|
||||
--color-brand-500: #465fff;
|
||||
--color-brand-600: #3641f5;
|
||||
--color-brand-700: #2a31d8;
|
||||
--color-brand-800: #252dae;
|
||||
--color-brand-900: #262e89;
|
||||
--color-brand-950: #161950;
|
||||
|
||||
--color-blue-light-25: #f5fbff;
|
||||
--color-blue-light-50: #f0f9ff;
|
||||
--color-blue-light-100: #e0f2fe;
|
||||
--color-blue-light-200: #b9e6fe;
|
||||
--color-blue-light-300: #7cd4fd;
|
||||
--color-blue-light-400: #36bffa;
|
||||
--color-blue-light-500: #0ba5ec;
|
||||
--color-blue-light-600: #0086c9;
|
||||
--color-blue-light-700: #026aa2;
|
||||
--color-blue-light-800: #065986;
|
||||
--color-blue-light-900: #0b4a6f;
|
||||
--color-blue-light-950: #062c41;
|
||||
|
||||
--color-gray-25: #fcfcfd;
|
||||
--color-gray-50: #f9fafb;
|
||||
--color-gray-100: #f2f4f7;
|
||||
--color-gray-200: #e4e7ec;
|
||||
--color-gray-300: #d0d5dd;
|
||||
--color-gray-400: #98a2b3;
|
||||
--color-gray-500: #667085;
|
||||
--color-gray-600: #475467;
|
||||
--color-gray-700: #344054;
|
||||
--color-gray-800: #1d2939;
|
||||
--color-gray-900: #101828;
|
||||
--color-gray-950: #0c111d;
|
||||
--color-gray-dark: #1a2231;
|
||||
|
||||
--color-orange-25: #fffaf5;
|
||||
--color-orange-50: #fff6ed;
|
||||
--color-orange-100: #ffead5;
|
||||
--color-orange-200: #fddcab;
|
||||
--color-orange-300: #feb273;
|
||||
--color-orange-400: #fd853a;
|
||||
--color-orange-500: #fb6514;
|
||||
--color-orange-600: #ec4a0a;
|
||||
--color-orange-700: #c4320a;
|
||||
--color-orange-800: #9c2a10;
|
||||
--color-orange-900: #7e2410;
|
||||
--color-orange-950: #511c10;
|
||||
|
||||
--color-success-25: #f6fef9;
|
||||
--color-success-50: #ecfdf3;
|
||||
--color-success-100: #d1fadf;
|
||||
--color-success-200: #a6f4c5;
|
||||
--color-success-300: #6ce9a6;
|
||||
--color-success-400: #32d583;
|
||||
--color-success-500: #12b76a;
|
||||
--color-success-600: #039855;
|
||||
--color-success-700: #027a48;
|
||||
--color-success-800: #05603a;
|
||||
--color-success-900: #054f31;
|
||||
--color-success-950: #053321;
|
||||
|
||||
--color-error-25: #fffbfa;
|
||||
--color-error-50: #fef3f2;
|
||||
--color-error-100: #fee4e2;
|
||||
--color-error-200: #fecdca;
|
||||
--color-error-300: #fda29b;
|
||||
--color-error-400: #f97066;
|
||||
--color-error-500: #f04438;
|
||||
--color-error-600: #d92d20;
|
||||
--color-error-700: #b42318;
|
||||
--color-error-800: #912018;
|
||||
--color-error-900: #7a271a;
|
||||
--color-error-950: #55160c;
|
||||
|
||||
--color-warning-25: #fffcf5;
|
||||
--color-warning-50: #fffaeb;
|
||||
--color-warning-100: #fef0c7;
|
||||
--color-warning-200: #fedf89;
|
||||
--color-warning-300: #fec84b;
|
||||
--color-warning-400: #fdb022;
|
||||
--color-warning-500: #f79009;
|
||||
--color-warning-600: #dc6803;
|
||||
--color-warning-700: #b54708;
|
||||
--color-warning-800: #93370d;
|
||||
--color-warning-900: #7a2e0e;
|
||||
--color-warning-950: #4e1d09;
|
||||
|
||||
--color-theme-pink-500: #ee46bc;
|
||||
|
||||
--color-theme-purple-500: #7a5af8;
|
||||
|
||||
--shadow-theme-md:
|
||||
0px 4px 8px -2px rgba(16, 24, 40, 0.1),
|
||||
0px 2px 4px -2px rgba(16, 24, 40, 0.06);
|
||||
--shadow-theme-lg:
|
||||
0px 12px 16px -4px rgba(16, 24, 40, 0.08),
|
||||
0px 4px 6px -2px rgba(16, 24, 40, 0.03);
|
||||
--shadow-theme-sm:
|
||||
0px 1px 3px 0px rgba(16, 24, 40, 0.1),
|
||||
0px 1px 2px 0px rgba(16, 24, 40, 0.06);
|
||||
--shadow-theme-xs: 0px 1px 2px 0px rgba(16, 24, 40, 0.05);
|
||||
--shadow-theme-xl:
|
||||
0px 20px 24px -4px rgba(16, 24, 40, 0.08),
|
||||
0px 8px 8px -4px rgba(16, 24, 40, 0.03);
|
||||
--shadow-datepicker: -5px 0 0 #262d3c, 5px 0 0 #262d3c;
|
||||
--shadow-focus-ring: 0px 0px 0px 4px rgba(70, 95, 255, 0.12);
|
||||
--shadow-slider-navigation:
|
||||
0px 1px 2px 0px rgba(16, 24, 40, 0.1), 0px 1px 3px 0px rgba(16, 24, 40, 0.1);
|
||||
--shadow-tooltip:
|
||||
0px 4px 6px -2px rgba(16, 24, 40, 0.05),
|
||||
-8px 0px 20px 8px rgba(16, 24, 40, 0.05);
|
||||
|
||||
--drop-shadow-4xl:
|
||||
0 35px 35px rgba(0, 0, 0, 0.25), 0 45px 65px rgba(0, 0, 0, 0.15);
|
||||
|
||||
--z-index-1: 1;
|
||||
--z-index-9: 9;
|
||||
--z-index-99: 99;
|
||||
--z-index-999: 999;
|
||||
--z-index-9999: 9999;
|
||||
--z-index-99999: 99999;
|
||||
--z-index-999999: 999999;
|
||||
}
|
||||
|
||||
/*
|
||||
The default border color has changed to `currentColor` in Tailwind CSS v4,
|
||||
so we've added these compatibility styles to make sure everything still
|
||||
looks the same as it did with Tailwind CSS v3.
|
||||
|
||||
If we ever want to remove these styles, we need to add an explicit border
|
||||
color utility to any element that depends on these defaults.
|
||||
*/
|
||||
@layer base {
|
||||
*,
|
||||
::after,
|
||||
::before,
|
||||
::backdrop,
|
||||
::file-selector-button {
|
||||
border-color: var(--color-gray-200, currentColor);
|
||||
}
|
||||
button:not(:disabled),
|
||||
[role="button"]:not(:disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
body {
|
||||
@apply relative font-normal font-inter z-1 bg-gray-50;
|
||||
}
|
||||
}
|
||||
|
||||
@utility menu-item {
|
||||
@apply relative flex items-center w-full gap-3 px-3 py-2 font-medium rounded-lg text-theme-sm;
|
||||
}
|
||||
|
||||
@utility menu-item-active {
|
||||
@apply bg-brand-50 text-brand-500 dark:bg-brand-500/[0.12] dark:text-brand-400;
|
||||
}
|
||||
|
||||
@utility menu-item-inactive {
|
||||
@apply text-gray-700 hover:bg-gray-100 group-hover:text-gray-700 dark:text-gray-300 dark:hover:bg-white/5 dark:hover:text-gray-300;
|
||||
}
|
||||
|
||||
@utility menu-item-icon {
|
||||
@apply text-gray-500 group-hover:text-gray-700 dark:text-gray-400;
|
||||
}
|
||||
|
||||
@utility menu-item-icon-active {
|
||||
@apply text-brand-500 dark:text-brand-400;
|
||||
}
|
||||
|
||||
@utility menu-item-icon-inactive {
|
||||
@apply text-gray-500 group-hover:text-gray-700 dark:text-gray-400 dark:group-hover:text-gray-300;
|
||||
}
|
||||
|
||||
@utility menu-item-arrow {
|
||||
@apply relative;
|
||||
}
|
||||
|
||||
@utility menu-item-arrow-active {
|
||||
@apply rotate-180 text-brand-500 dark:text-brand-400;
|
||||
}
|
||||
|
||||
@utility menu-item-arrow-inactive {
|
||||
@apply text-gray-500 group-hover:text-gray-700 dark:text-gray-400 dark:group-hover:text-gray-300;
|
||||
}
|
||||
|
||||
@utility menu-dropdown-item {
|
||||
@apply relative flex items-center gap-3 rounded-lg px-3 py-2.5 text-theme-sm font-medium;
|
||||
}
|
||||
|
||||
@utility menu-dropdown-item-active {
|
||||
@apply bg-brand-50 text-brand-500 dark:bg-brand-500/[0.12] dark:text-brand-400;
|
||||
}
|
||||
|
||||
@utility menu-dropdown-item-inactive {
|
||||
@apply text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-white/5;
|
||||
}
|
||||
|
||||
@utility menu-dropdown-badge {
|
||||
@apply block rounded-full px-2.5 py-0.5 text-xs font-medium uppercase text-brand-500 dark:text-brand-400;
|
||||
}
|
||||
|
||||
@utility menu-dropdown-badge-active {
|
||||
@apply bg-brand-100 dark:bg-brand-500/20;
|
||||
}
|
||||
|
||||
@utility menu-dropdown-badge-inactive {
|
||||
@apply bg-brand-50 group-hover:bg-brand-100 dark:bg-brand-500/15 dark:group-hover:bg-brand-500/20;
|
||||
}
|
||||
|
||||
@utility no-scrollbar {
|
||||
/* Chrome, Safari and Opera */
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
|
||||
@utility custom-scrollbar {
|
||||
&::-webkit-scrollbar {
|
||||
@apply size-1.5;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
@apply rounded-full;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
@apply bg-gray-200 rounded-full dark:bg-gray-700;
|
||||
}
|
||||
}
|
||||
|
||||
.dark .custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background-color: #344054;
|
||||
}
|
||||
|
||||
|
||||
/* third-party libraries CSS */
|
||||
.apexcharts-legend-text {
|
||||
@apply !text-gray-700 dark:!text-gray-400;
|
||||
}
|
||||
|
||||
.apexcharts-text {
|
||||
@apply !fill-gray-700 dark:!fill-gray-400;
|
||||
}
|
||||
|
||||
.apexcharts-tooltip.apexcharts-theme-light {
|
||||
@apply gap-1 !rounded-lg !border-gray-200 p-3 !shadow-theme-sm dark:!border-gray-800 dark:!bg-gray-900;
|
||||
}
|
||||
|
||||
.apexcharts-legend-text {
|
||||
@apply pl-5! !text-gray-700 dark:!text-gray-400;
|
||||
}
|
||||
.apexcharts-tooltip-series-group {
|
||||
@apply !p-0;
|
||||
}
|
||||
.apexcharts-tooltip-y-group {
|
||||
@apply !p-0;
|
||||
}
|
||||
.apexcharts-tooltip-title {
|
||||
@apply !mb-0 !border-b-0 !bg-transparent !p-0 !text-[10px] !leading-4 !text-gray-800 dark:!text-white/90;
|
||||
}
|
||||
.apexcharts-tooltip-text {
|
||||
@apply !text-theme-xs !text-gray-700 dark:!text-white/90;
|
||||
}
|
||||
.apexcharts-tooltip-text-y-value {
|
||||
@apply !font-medium;
|
||||
}
|
||||
|
||||
.apexcharts-gridline {
|
||||
@apply !stroke-gray-100 dark:!stroke-gray-800;
|
||||
}
|
||||
|
||||
.flatpickr-wrapper {
|
||||
@apply w-full;
|
||||
}
|
||||
.flatpickr-calendar {
|
||||
@apply mt-2 !bg-white !rounded-xl !p-5 !border !border-gray-200 dark:!border-gray-800 !text-gray-500 dark:!bg-gray-dark dark:!text-gray-400 dark:!shadow-theme-xl 2xsm:!w-auto;
|
||||
}
|
||||
|
||||
.flatpickr-months .flatpickr-prev-month:hover svg,
|
||||
.flatpickr-months .flatpickr-next-month:hover svg {
|
||||
@apply stroke-brand-500;
|
||||
}
|
||||
.flatpickr-calendar.arrowTop:before,
|
||||
.flatpickr-calendar.arrowTop:after {
|
||||
@apply hidden;
|
||||
}
|
||||
|
||||
.flatpickr-current-month {
|
||||
@apply !p-0;
|
||||
}
|
||||
.flatpickr-current-month .cur-month,
|
||||
.flatpickr-current-month input.cur-year {
|
||||
@apply !h-auto !pt-0 !text-lg !font-medium !text-gray-800 dark:!text-white/90;
|
||||
}
|
||||
|
||||
.flatpickr-prev-month,
|
||||
.flatpickr-next-month {
|
||||
@apply !p-0;
|
||||
}
|
||||
|
||||
.flatpickr-weekdays {
|
||||
@apply h-auto mt-6 mb-4 !bg-transparent;
|
||||
}
|
||||
|
||||
.flatpickr-calendar {
|
||||
@apply mt-2 !bg-white !rounded-xl !p-5 !shadow-none !border !border-gray-200 dark:!border-white/5 !text-gray-500 dark:!bg-gray-dark dark:!text-gray-400 dark:!shadow-theme-xl 2xsm:!w-auto;
|
||||
}
|
||||
|
||||
@media (max-width: 525px) {
|
||||
.flatpickr-calendar.static {
|
||||
left: auto !important;
|
||||
right: 0 !important;
|
||||
margin-right: -30px;
|
||||
}
|
||||
}
|
||||
|
||||
.flatpickr-weekday {
|
||||
@apply !text-theme-sm !font-medium !text-gray-500 dark:!text-gray-400 !bg-transparent;
|
||||
}
|
||||
|
||||
.flatpickr-day {
|
||||
@apply !flex !items-center !text-theme-sm !font-medium !text-gray-800 dark:!text-white/90 dark:hover:!border-gray-300 dark:hover:!bg-gray-900;
|
||||
}
|
||||
.flatpickr-day.nextMonthDay,
|
||||
.flatpickr-day.prevMonthDay {
|
||||
@apply !text-gray-400;
|
||||
}
|
||||
|
||||
.flatpickr-months > .flatpickr-month {
|
||||
background: none !important;
|
||||
}
|
||||
.flatpickr-month .flatpickr-current-month .flatpickr-monthDropdown-months {
|
||||
background: none !important;
|
||||
appearance: none;
|
||||
background-image: none !important;
|
||||
font-weight: 500;
|
||||
}
|
||||
.flatpickr-month
|
||||
.flatpickr-current-month
|
||||
.flatpickr-monthDropdown-months:focus {
|
||||
outline: none !important;
|
||||
border: 0 !important;
|
||||
}
|
||||
.flatpickr-months .flatpickr-prev-month,
|
||||
.flatpickr-months .flatpickr-next-month {
|
||||
@apply !top-7 dark:!fill-white dark:!text-white !bg-transparent;
|
||||
}
|
||||
.flatpickr-months .flatpickr-prev-month.flatpickr-prev-month,
|
||||
.flatpickr-months .flatpickr-next-month.flatpickr-prev-month {
|
||||
@apply !left-7;
|
||||
}
|
||||
.flatpickr-months .flatpickr-prev-month.flatpickr-next-month,
|
||||
.flatpickr-months .flatpickr-next-month.flatpickr-next-month {
|
||||
@apply !right-7;
|
||||
}
|
||||
.flatpickr-days {
|
||||
@apply !border-0;
|
||||
}
|
||||
span.flatpickr-weekday,
|
||||
.flatpickr-months .flatpickr-month {
|
||||
@apply dark:!fill-white dark:!text-white !bg-none;
|
||||
}
|
||||
.flatpickr-innerContainer {
|
||||
@apply !border-b-0;
|
||||
}
|
||||
.flatpickr-months .flatpickr-month {
|
||||
@apply !bg-none;
|
||||
}
|
||||
.flatpickr-day.inRange {
|
||||
box-shadow: -5px 0 0 #f9fafb, 5px 0 0 #f9fafb !important;
|
||||
@apply dark:!shadow-datepicker;
|
||||
}
|
||||
.flatpickr-day.inRange,
|
||||
.flatpickr-day.prevMonthDay.inRange,
|
||||
.flatpickr-day.nextMonthDay.inRange,
|
||||
.flatpickr-day.today.inRange,
|
||||
.flatpickr-day.prevMonthDay.today.inRange,
|
||||
.flatpickr-day.nextMonthDay.today.inRange,
|
||||
.flatpickr-day:hover,
|
||||
.flatpickr-day.prevMonthDay:hover,
|
||||
.flatpickr-day.nextMonthDay:hover,
|
||||
.flatpickr-day:focus,
|
||||
.flatpickr-day.prevMonthDay:focus,
|
||||
.flatpickr-day.nextMonthDay:focus {
|
||||
@apply !border-gray-50 !bg-gray-50 dark:!border-0 dark:!border-white/5 dark:!bg-white/5;
|
||||
}
|
||||
.flatpickr-day.selected,
|
||||
.flatpickr-day.startRange,
|
||||
.flatpickr-day.selected,
|
||||
.flatpickr-day.endRange {
|
||||
@apply !text-white dark:!text-white;
|
||||
}
|
||||
.flatpickr-day.selected,
|
||||
.flatpickr-day.startRange,
|
||||
.flatpickr-day.endRange,
|
||||
.flatpickr-day.selected.inRange,
|
||||
.flatpickr-day.startRange.inRange,
|
||||
.flatpickr-day.endRange.inRange,
|
||||
.flatpickr-day.selected:focus,
|
||||
.flatpickr-day.startRange:focus,
|
||||
.flatpickr-day.endRange:focus,
|
||||
.flatpickr-day.selected:hover,
|
||||
.flatpickr-day.startRange:hover,
|
||||
.flatpickr-day.endRange:hover,
|
||||
.flatpickr-day.selected.prevMonthDay,
|
||||
.flatpickr-day.startRange.prevMonthDay,
|
||||
.flatpickr-day.endRange.prevMonthDay,
|
||||
.flatpickr-day.selected.nextMonthDay,
|
||||
.flatpickr-day.startRange.nextMonthDay,
|
||||
.flatpickr-day.endRange.nextMonthDay {
|
||||
background: #465fff;
|
||||
@apply !border-brand-500 !bg-brand-500 hover:!border-brand-50 hover:!bg-brand-500;
|
||||
}
|
||||
.flatpickr-day.selected.startRange + .endRange:not(:nth-child(7n + 1)),
|
||||
.flatpickr-day.startRange.startRange + .endRange:not(:nth-child(7n + 1)),
|
||||
.flatpickr-day.endRange.startRange + .endRange:not(:nth-child(7n + 1)) {
|
||||
box-shadow: -10px 0 0 #465fff;
|
||||
}
|
||||
|
||||
.flatpickr-months .flatpickr-prev-month svg,
|
||||
.flatpickr-months .flatpickr-next-month svg,
|
||||
.flatpickr-months .flatpickr-prev-month,
|
||||
.flatpickr-months .flatpickr-next-month {
|
||||
@apply hover:!fill-none;
|
||||
}
|
||||
.flatpickr-months .flatpickr-prev-month:hover svg,
|
||||
.flatpickr-months .flatpickr-next-month:hover svg {
|
||||
fill: none !important;
|
||||
}
|
||||
|
||||
.flatpickr-calendar.static {
|
||||
@apply right-0;
|
||||
}
|
||||
|
||||
.fc .fc-view-harness {
|
||||
@apply max-w-full overflow-x-auto custom-scrollbar;
|
||||
}
|
||||
.fc-dayGridMonth-view.fc-view.fc-daygrid {
|
||||
@apply min-w-[718px];
|
||||
}
|
||||
.fc .fc-scrollgrid-section > * {
|
||||
border-right-width: 0;
|
||||
border-bottom-width: 0;
|
||||
}
|
||||
.fc .fc-scrollgrid {
|
||||
border-left-width: 0;
|
||||
}
|
||||
.fc .fc-toolbar.fc-header-toolbar {
|
||||
@apply flex-col gap-4 px-6 pt-6 sm:flex-row;
|
||||
}
|
||||
.fc-button-group {
|
||||
@apply gap-2;
|
||||
}
|
||||
.fc-button-group .fc-button {
|
||||
@apply flex h-10 w-10 items-center justify-center !rounded-lg border border-gray-200 bg-transparent hover:border-gray-200 hover:bg-gray-50 focus:shadow-none active:!border-gray-200 active:!bg-transparent active:!shadow-none dark:border-gray-800 dark:hover:border-gray-800 dark:hover:bg-gray-900 dark:active:border-gray-800!;
|
||||
}
|
||||
|
||||
.fc-button-group .fc-button.fc-prev-button:before {
|
||||
@apply inline-block mt-1;
|
||||
content: url("data:image/svg+xml,%3Csvg width='25' height='24' viewBox='0 0 25 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M16.0068 6L9.75684 12.25L16.0068 18.5' stroke='%23344054' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E%0A");
|
||||
}
|
||||
.fc-button-group .fc-button.fc-next-button:before {
|
||||
@apply inline-block mt-1;
|
||||
content: url("data:image/svg+xml,%3Csvg width='25' height='24' viewBox='0 0 25 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M9.50684 19L15.7568 12.75L9.50684 6.5' stroke='%23344054' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E%0A");
|
||||
}
|
||||
.dark .fc-button-group .fc-button.fc-prev-button:before {
|
||||
content: url("data:image/svg+xml,%3Csvg width='25' height='24' viewBox='0 0 25 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M16.0068 6L9.75684 12.25L16.0068 18.5' stroke='%2398A2B3' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E%0A");
|
||||
}
|
||||
.dark .fc-button-group .fc-button.fc-next-button:before {
|
||||
content: url("data:image/svg+xml,%3Csvg width='25' height='24' viewBox='0 0 25 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M9.50684 19L15.7568 12.75L9.50684 6.5' stroke='%2398A2B3' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E%0A");
|
||||
}
|
||||
.fc-button-group .fc-button .fc-icon {
|
||||
@apply hidden;
|
||||
}
|
||||
.fc-addEventButton-button {
|
||||
@apply !rounded-lg !border-0 !bg-brand-500 !px-4 !py-2.5 !text-sm !font-medium hover:!bg-brand-600 focus:!shadow-none;
|
||||
}
|
||||
.fc-toolbar-title {
|
||||
@apply !text-lg !font-medium text-gray-800 dark:text-white/90;
|
||||
}
|
||||
.fc-header-toolbar.fc-toolbar .fc-toolbar-chunk:last-child {
|
||||
@apply rounded-lg bg-gray-100 p-0.5 dark:bg-gray-900;
|
||||
}
|
||||
.fc-header-toolbar.fc-toolbar .fc-toolbar-chunk:last-child .fc-button {
|
||||
@apply !h-auto !w-auto rounded-md !border-0 bg-transparent !px-5 !py-2 text-sm font-medium text-gray-500 hover:text-gray-700 focus:shadow-none! dark:text-gray-400;
|
||||
}
|
||||
.fc-header-toolbar.fc-toolbar
|
||||
.fc-toolbar-chunk:last-child
|
||||
.fc-button.fc-button-active {
|
||||
@apply text-gray-900 bg-white dark:bg-gray-800 dark:text-white;
|
||||
}
|
||||
.fc-theme-standard th {
|
||||
@apply !border-x-0 border-t !border-gray-200 bg-gray-50 !text-left dark:!border-gray-800 dark:bg-gray-900;
|
||||
}
|
||||
.fc-theme-standard td,
|
||||
.fc-theme-standard .fc-scrollgrid {
|
||||
@apply !border-gray-200 dark:!border-gray-800;
|
||||
}
|
||||
.fc .fc-col-header-cell-cushion {
|
||||
@apply !px-5 !py-4 text-sm font-medium uppercase text-gray-400;
|
||||
}
|
||||
.fc .fc-daygrid-day.fc-day-today {
|
||||
@apply bg-transparent;
|
||||
}
|
||||
.fc .fc-daygrid-day {
|
||||
@apply p-2;
|
||||
}
|
||||
.fc .fc-daygrid-day.fc-day-today .fc-scrollgrid-sync-inner {
|
||||
@apply rounded-sm bg-gray-100 dark:bg-white/[0.03];
|
||||
}
|
||||
.fc .fc-daygrid-day-number {
|
||||
@apply !p-3 text-sm font-medium text-gray-700 dark:text-gray-400;
|
||||
}
|
||||
.fc .fc-daygrid-day-top {
|
||||
@apply !flex-row;
|
||||
}
|
||||
.fc .fc-day-other .fc-daygrid-day-top {
|
||||
opacity: 1;
|
||||
}
|
||||
.fc .fc-day-other .fc-daygrid-day-top .fc-daygrid-day-number {
|
||||
@apply text-gray-400 dark:text-white/30;
|
||||
}
|
||||
.event-fc-color {
|
||||
@apply rounded-lg py-2.5 pl-4 pr-3;
|
||||
}
|
||||
.event-fc-color .fc-event-title {
|
||||
@apply p-0 text-sm font-normal text-gray-700;
|
||||
}
|
||||
.fc-daygrid-event-dot {
|
||||
@apply w-1 h-5 ml-0 mr-3 border-none rounded-sm;
|
||||
}
|
||||
.fc-event {
|
||||
@apply focus:shadow-none;
|
||||
}
|
||||
.fc-daygrid-event.fc-event-start {
|
||||
@apply !ml-3;
|
||||
}
|
||||
.event-fc-color.fc-bg-success {
|
||||
@apply border-success-50 bg-success-50;
|
||||
}
|
||||
.event-fc-color.fc-bg-danger {
|
||||
@apply border-error-50 bg-error-50;
|
||||
}
|
||||
.event-fc-color.fc-bg-primary {
|
||||
@apply border-brand-50 bg-brand-50;
|
||||
}
|
||||
.event-fc-color.fc-bg-warning {
|
||||
@apply border-orange-50 bg-orange-50;
|
||||
}
|
||||
.event-fc-color.fc-bg-success .fc-daygrid-event-dot {
|
||||
@apply bg-success-500;
|
||||
}
|
||||
.event-fc-color.fc-bg-danger .fc-daygrid-event-dot {
|
||||
@apply bg-error-500;
|
||||
}
|
||||
.event-fc-color.fc-bg-primary .fc-daygrid-event-dot {
|
||||
@apply bg-brand-500;
|
||||
}
|
||||
.event-fc-color.fc-bg-warning .fc-daygrid-event-dot {
|
||||
@apply bg-orange-500;
|
||||
}
|
||||
.fc-direction-ltr .fc-timegrid-slot-label-frame {
|
||||
@apply px-3 py-1.5 text-left text-sm font-medium text-gray-500 dark:text-gray-400;
|
||||
}
|
||||
.fc .fc-timegrid-axis-cushion {
|
||||
@apply text-sm font-medium text-gray-500 dark:text-gray-400;
|
||||
}
|
||||
.custom-calendar .fc-h-event {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
color: black;
|
||||
}
|
||||
.fc.fc-media-screen {
|
||||
@apply min-h-screen;
|
||||
}
|
||||
|
||||
.input-date-icon::-webkit-inner-spin-button,
|
||||
.input-date-icon::-webkit-calendar-picker-indicator {
|
||||
opacity: 0;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
.stocks-slider-outer .swiper-button-next:after,
|
||||
.stocks-slider-outer .swiper-button-prev:after {
|
||||
@apply hidden;
|
||||
}
|
||||
|
||||
.stocks-slider-outer .swiper-button-next,
|
||||
.stocks-slider-outer .swiper-button-prev {
|
||||
@apply !static mt-0 h-8 w-9 rounded-full border dark:hover:bg-white/[0.05] border-gray-200 !text-gray-700 transition hover:bg-gray-100 dark:border-white/[0.03] dark:bg-gray-800 dark:!text-gray-400 dark:hover:!text-white/90;
|
||||
}
|
||||
|
||||
.stocks-slider-outer .swiper-button-next.swiper-button-disabled,
|
||||
.stocks-slider-outer .swiper-button-prev.swiper-button-disabled {
|
||||
@apply bg-white opacity-50 dark:bg-gray-900;
|
||||
}
|
||||
|
||||
.stocks-slider-outer .swiper-button-next svg,
|
||||
.stocks-slider-outer .swiper-button-prev svg {
|
||||
@apply !h-auto !w-auto;
|
||||
}
|
||||
|
||||
.swiper-button-prev svg,
|
||||
.swiper-button-next svg {
|
||||
@apply !h-auto !w-auto;
|
||||
}
|
||||
|
||||
.carouselTwo .swiper-button-next:after,
|
||||
.carouselTwo .swiper-button-prev:after,
|
||||
.carouselFour .swiper-button-next:after,
|
||||
.carouselFour .swiper-button-prev:after {
|
||||
@apply hidden;
|
||||
}
|
||||
.carouselTwo .swiper-button-next.swiper-button-disabled,
|
||||
.carouselTwo .swiper-button-prev.swiper-button-disabled,
|
||||
.carouselFour .swiper-button-next.swiper-button-disabled,
|
||||
.carouselFour .swiper-button-prev.swiper-button-disabled {
|
||||
@apply bg-white/60 !opacity-100;
|
||||
}
|
||||
.carouselTwo .swiper-button-next,
|
||||
.carouselTwo .swiper-button-prev,
|
||||
.carouselFour .swiper-button-next,
|
||||
.carouselFour .swiper-button-prev {
|
||||
@apply h-10 w-10 rounded-full border-[0.5px] border-white/10 bg-white/90 !text-gray-700 shadow-slider-navigation backdrop-blur-[10px];
|
||||
}
|
||||
|
||||
.carouselTwo .swiper-button-prev,
|
||||
.carouselFour .swiper-button-prev {
|
||||
@apply !left-3 sm:!left-4;
|
||||
}
|
||||
|
||||
.carouselTwo .swiper-button-next,
|
||||
.carouselFour .swiper-button-next {
|
||||
@apply !right-3 sm:!right-4;
|
||||
}
|
||||
|
||||
.carouselThree .swiper-pagination,
|
||||
.carouselFour .swiper-pagination {
|
||||
@apply !bottom-3 !left-1/2 inline-flex !w-auto -translate-x-1/2 items-center gap-1.5 rounded-[40px] border-[0.5px] border-white/10 bg-white/60 px-2 py-1.5 shadow-slider-navigation backdrop-blur-[10px] sm:!bottom-5;
|
||||
}
|
||||
|
||||
.carouselThree .swiper-pagination-bullet,
|
||||
.carouselFour .swiper-pagination-bullet {
|
||||
@apply !m-0 h-2.5 w-2.5 bg-white opacity-100 shadow-theme-xs duration-200 ease-in-out;
|
||||
}
|
||||
|
||||
.carouselThree .swiper-pagination-bullet-active,
|
||||
.carouselFour .swiper-pagination-bullet-active {
|
||||
@apply w-6.5 rounded-xl;
|
||||
}
|
||||
|
||||
.jvectormap-container {
|
||||
@apply !bg-gray-50 dark:!bg-gray-900;
|
||||
}
|
||||
.jvectormap-region.jvectormap-element {
|
||||
@apply !fill-gray-300 hover:!fill-brand-500 dark:!fill-gray-700 dark:hover:!fill-brand-500;
|
||||
}
|
||||
.jvectormap-marker.jvectormap-element {
|
||||
@apply !stroke-gray-200 dark:!stroke-gray-800;
|
||||
}
|
||||
.jvectormap-tip {
|
||||
@apply !bg-brand-500 !border-none !px-2 !py-1;
|
||||
}
|
||||
.jvectormap-zoomin,
|
||||
.jvectormap-zoomout {
|
||||
@apply !hidden;
|
||||
}
|
||||
|
||||
.form-check-input:checked ~ span {
|
||||
@apply border-[6px] border-brand-500 bg-brand-500 dark:border-brand-500;
|
||||
}
|
||||
|
||||
.taskCheckbox:checked ~ .box span {
|
||||
@apply opacity-100 bg-brand-500;
|
||||
}
|
||||
.taskCheckbox:checked ~ p {
|
||||
@apply text-gray-400 line-through;
|
||||
}
|
||||
.taskCheckbox:checked ~ .box {
|
||||
@apply border-brand-500 bg-brand-500 dark:border-brand-500;
|
||||
}
|
||||
|
||||
.task {
|
||||
transition: all 0.2s ease; /* Smooth transition for visual effects */
|
||||
}
|
||||
|
||||
.task {
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 1px 3px 0 rgba(16, 24, 40, 0.1),
|
||||
0 1px 2px 0 rgba(16, 24, 40, 0.06);
|
||||
opacity: 0.8;
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
html {
|
||||
scrollbar-gutter: stable;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
html {
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--color-gray-50);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
border-radius: 10px;
|
||||
background-color: var(--color-gray-300);
|
||||
border: 2px solid var(--color-gray-50);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background-color: var(--color-gray-400);
|
||||
}
|
||||
|
||||
.dark ::-webkit-scrollbar-track {
|
||||
background: var(--color-gray-950);
|
||||
}
|
||||
|
||||
.dark ::-webkit-scrollbar-thumb {
|
||||
background-color: var(--color-gray-700);
|
||||
border: 2px solid var(--color-gray-950);
|
||||
}
|
||||
|
||||
.dark ::-webkit-scrollbar-thumb:hover {
|
||||
background-color: var(--color-gray-600);
|
||||
}
|
||||
|
||||
.ql-editor pre.ql-syntax {
|
||||
background-color: #23241f;
|
||||
color: #f8f8f2;
|
||||
overflow: visible;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.ql-toolbar.ql-snow {
|
||||
border: none !important;
|
||||
border-bottom: 1px solid #e5e7eb !important;
|
||||
background-color: #f9fafb;
|
||||
padding: 12px !important;
|
||||
}
|
||||
|
||||
.ql-container.ql-snow {
|
||||
border: none !important;
|
||||
font-size: 16px;
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
.ql-editor {
|
||||
min-height: 400px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.dark .ql-toolbar.ql-snow {
|
||||
background-color: #1f2937;
|
||||
border-bottom: 1px solid #374151 !important;
|
||||
color: #f3f4f6;
|
||||
}
|
||||
|
||||
.dark .ql-snow .ql-stroke {
|
||||
stroke: #d1d5db;
|
||||
}
|
||||
|
||||
.dark .ql-editor {
|
||||
color: #f3f4f6;
|
||||
}
|
||||
29
src/app/layout.tsx
Normal file
29
src/app/layout.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Inter } from 'next/font/google';
|
||||
import './globals.css';
|
||||
import "flatpickr/dist/flatpickr.css";
|
||||
import { SidebarProvider } from '@/context/SidebarContext';
|
||||
import { ThemeProvider } from '@/context/ThemeContext';
|
||||
import { Toaster } from 'sonner';
|
||||
import StoreProvider from '@/store/StoreProvider';
|
||||
|
||||
const inter = Inter({
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={`${inter.className} dark:bg-gray-900`}>
|
||||
<StoreProvider>
|
||||
<ThemeProvider>
|
||||
<SidebarProvider>{children} <Toaster closeButton richColors position="top-right" /> </SidebarProvider>
|
||||
</ThemeProvider>
|
||||
</StoreProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
47
src/app/not-found.tsx
Normal file
47
src/app/not-found.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import GridShape from "@/components/common/GridShape";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import React from "react";
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<div className="relative flex flex-col items-center justify-center min-h-screen p-6 overflow-hidden z-1">
|
||||
<GridShape />
|
||||
<div className="mx-auto w-full max-w-[242px] text-center sm:max-w-[472px]">
|
||||
<h1 className="mb-8 font-bold text-gray-800 text-title-md dark:text-white/90 xl:text-title-2xl">
|
||||
ERROR
|
||||
</h1>
|
||||
|
||||
<Image
|
||||
src="/images/error/404.svg"
|
||||
alt="404"
|
||||
className="dark:hidden"
|
||||
width={472}
|
||||
height={152}
|
||||
/>
|
||||
<Image
|
||||
src="/images/error/404-dark.svg"
|
||||
alt="404"
|
||||
className="hidden dark:block"
|
||||
width={472}
|
||||
height={152}
|
||||
/>
|
||||
|
||||
<p className="mt-10 mb-6 text-base text-gray-700 dark:text-gray-400 sm:text-lg">
|
||||
We can’t seem to find the page you are looking for!
|
||||
</p>
|
||||
|
||||
<Link
|
||||
href="/"
|
||||
className="inline-flex items-center justify-center rounded-lg border border-gray-300 bg-white px-5 py-3.5 text-sm font-medium text-gray-700 shadow-theme-xs hover:bg-gray-50 hover:text-gray-800 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03] dark:hover:text-gray-200"
|
||||
>
|
||||
Back to Home Page
|
||||
</Link>
|
||||
</div>
|
||||
{/* <!-- Footer --> */}
|
||||
<p className="absolute text-sm text-center text-gray-500 -translate-x-1/2 bottom-6 left-1/2 dark:text-gray-400">
|
||||
© {new Date().getFullYear()} - TailAdmin
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
37
src/app/testing/page.tsx
Normal file
37
src/app/testing/page.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { apiGetCurrentUser } from "@/service/auth";
|
||||
|
||||
export default function GetUser() {
|
||||
const [user, setUser] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchUser = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const result = await apiGetCurrentUser();
|
||||
// console.log("Current User from useEffect:", result);
|
||||
setUser(result);
|
||||
} catch (err) {
|
||||
// console.error("Lỗi 401 hoặc lỗi kết nối:", err);
|
||||
// setError(err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchUser();
|
||||
}, []);
|
||||
|
||||
if (loading) return <div>Đang tải thông tin...</div>;
|
||||
if (error) return <div>Bạn chưa đăng nhập (Lỗi 401)</div>;
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
<h1>Thông tin người dùng hiện tại:</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
24
src/app/user/(chart)/bar-chart/page.tsx
Normal file
24
src/app/user/(chart)/bar-chart/page.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import BarChartOne from "@/components/charts/bar/BarChartOne";
|
||||
import ComponentCard from "@/components/common/ComponentCard";
|
||||
import PageBreadcrumb from "@/components/common/PageBreadCrumb";
|
||||
import { Metadata } from "next";
|
||||
import React from "react";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Next.js Bar Chart | TailAdmin - Next.js Dashboard Template",
|
||||
description:
|
||||
"This is Next.js Bar Chart page for TailAdmin - Next.js Tailwind CSS Admin Dashboard Template",
|
||||
};
|
||||
|
||||
export default function page() {
|
||||
return (
|
||||
<div>
|
||||
<PageBreadcrumb pageTitle="Bar Chart" />
|
||||
<div className="space-y-6">
|
||||
<ComponentCard title="Bar Chart 1">
|
||||
<BarChartOne />
|
||||
</ComponentCard>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
23
src/app/user/(chart)/line-chart/page.tsx
Normal file
23
src/app/user/(chart)/line-chart/page.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import LineChartOne from "@/components/charts/line/LineChartOne";
|
||||
import ComponentCard from "@/components/common/ComponentCard";
|
||||
import PageBreadcrumb from "@/components/common/PageBreadCrumb";
|
||||
import { Metadata } from "next";
|
||||
import React from "react";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Next.js Line Chart | TailAdmin - Next.js Dashboard Template",
|
||||
description:
|
||||
"This is Next.js Line Chart page for TailAdmin - Next.js Tailwind CSS Admin Dashboard Template",
|
||||
};
|
||||
export default function LineChart() {
|
||||
return (
|
||||
<div>
|
||||
<PageBreadcrumb pageTitle="Line Chart" />
|
||||
<div className="space-y-6">
|
||||
<ComponentCard title="Line Chart 1">
|
||||
<LineChartOne />
|
||||
</ComponentCard>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
295
src/app/user/account/applications/page.tsx
Normal file
295
src/app/user/account/applications/page.tsx
Normal file
@@ -0,0 +1,295 @@
|
||||
"use client";
|
||||
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState } from "@/store/store";
|
||||
import { SafeHTMLRenderer } from "@/components/ui/parse/SafeHTMLRenderer";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { apiDeleteHistorianCV } from "@/service/historianService";
|
||||
import Swal from "sweetalert2";
|
||||
import { statusConfig } from "@/service/handler";
|
||||
|
||||
import Lightbox from "yet-another-react-lightbox";
|
||||
import Zoom from "yet-another-react-lightbox/plugins/zoom";
|
||||
import Captions from "yet-another-react-lightbox/plugins/captions";
|
||||
import "yet-another-react-lightbox/styles.css";
|
||||
import "yet-another-react-lightbox/plugins/captions.css";
|
||||
import Image from "next/image";
|
||||
import { URL_MEDIA } from "../../../../../api";
|
||||
|
||||
export default function ApplicationDetailPage() {
|
||||
const application = useSelector(
|
||||
(state: RootState) => state.user.selectedApplication,
|
||||
);
|
||||
const router = useRouter();
|
||||
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [errMessage, setErrMessage] = useState<string>(
|
||||
"Không thể xóa đơn đăng ký này.",
|
||||
);
|
||||
const [index, setIndex] = useState(-1);
|
||||
|
||||
if (!application) {
|
||||
return (
|
||||
<div className="p-10 text-center text-zinc-500 font-medium">
|
||||
Đang tải hoặc không có dữ liệu...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const config = statusConfig[application.status] || statusConfig.PENDING;
|
||||
|
||||
const isImageFile = (file: any) => {
|
||||
const isImageMime = file.mime_type?.startsWith("image/");
|
||||
const isImageExt = /\.(jpg|jpeg|png|webp|gif)$/i.test(file.storage_key);
|
||||
return isImageMime || isImageExt;
|
||||
};
|
||||
|
||||
const imageMediaOnly = (application.media || []).filter(isImageFile);
|
||||
|
||||
const imageSlides = imageMediaOnly.map((item: any) => ({
|
||||
src: `${URL_MEDIA}${item.storage_key}`,
|
||||
title: item.original_name,
|
||||
description: `Dung lượng: ${(item.size / 1024).toFixed(2)} KB`,
|
||||
}));
|
||||
|
||||
const handleMediaClick = (item: any) => {
|
||||
const fileUrl = `${URL_MEDIA}${item.storage_key}`;
|
||||
if (isImageFile(item)) {
|
||||
const photoIndex = imageMediaOnly.findIndex(
|
||||
(img: any) => img.id === item.id,
|
||||
);
|
||||
setIndex(photoIndex);
|
||||
} else {
|
||||
const googleDocsUrl = `https://docs.google.com/viewer?url=${encodeURIComponent(fileUrl)}&embedded=true`;
|
||||
window.open(googleDocsUrl, "_blank");
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
const isDarkMode = document.documentElement.classList.contains("dark");
|
||||
const result = await Swal.fire({
|
||||
title: "Xác nhận xóa?",
|
||||
text: "Bạn sẽ không thể khôi phục lại đơn đăng ký này!",
|
||||
icon: "warning",
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: "#ef4444",
|
||||
cancelButtonColor: "#71717a",
|
||||
confirmButtonText: "Xóa ngay",
|
||||
cancelButtonText: "Hủy",
|
||||
background: isDarkMode ? "#18181b" : "#fff",
|
||||
color: isDarkMode ? "#fff" : "#000",
|
||||
});
|
||||
|
||||
if (result.isConfirmed) {
|
||||
try {
|
||||
setIsDeleting(true);
|
||||
await apiDeleteHistorianCV(application.id);
|
||||
|
||||
// Thành công (200 OK)
|
||||
await Swal.fire({
|
||||
title: "Đã xóa!",
|
||||
icon: "success",
|
||||
timer: 1500,
|
||||
showConfirmButton: false,
|
||||
background: isDarkMode ? "#18181b" : "#fff",
|
||||
color: isDarkMode ? "#fff" : "#000",
|
||||
});
|
||||
router.push("/account");
|
||||
} catch (error: any) {
|
||||
setErrMessage(
|
||||
error.response?.data?.message || "Có lỗi xảy ra khi xóa!",
|
||||
);
|
||||
if (
|
||||
error.response?.data?.message ===
|
||||
"You don't have permission to access this resource."
|
||||
) {
|
||||
setErrMessage("Bạn không có quyền xóa đơn đăng ký này.");
|
||||
}
|
||||
|
||||
Swal.fire({
|
||||
title: "Lỗi!",
|
||||
text: errMessage,
|
||||
icon: "error",
|
||||
background: isDarkMode ? "#18181b" : "#fff",
|
||||
color: isDarkMode ? "#fff" : "#000",
|
||||
});
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
const formatDate = (dateString: string | null | undefined) => {
|
||||
if (!dateString) return "-";
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString("vi-VN", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
};
|
||||
|
||||
// console.log("Application Detail:", application);
|
||||
|
||||
return (
|
||||
<div className="max-w-5xl mx-auto p-6 bg-white dark:bg-zinc-900 rounded-2xl shadow-sm border dark:border-zinc-800">
|
||||
<div className="flex justify-between items-center mb-8 border-b dark:border-zinc-800 pb-6">
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold dark:text-zinc-50 tracking-tight">
|
||||
Chi tiết đơn đăng ký
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`flex items-center gap-1.5 px-2 py-1 rounded-md backdrop-blur-md border ${config.container}`}
|
||||
>
|
||||
<span className={`text-[12px] font-bold uppercase tracking-wider`}>
|
||||
{application.status}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-10">
|
||||
<section>
|
||||
<label className="text-[11px] font-black text-zinc-400 uppercase tracking-widest mb-4 block">
|
||||
Nội dung hiển thị (CV)
|
||||
</label>
|
||||
<div className="rounded-2xl border-2 border-zinc-50 dark:border-zinc-800 p-6 bg-zinc-50/50 dark:bg-zinc-950/30">
|
||||
<SafeHTMLRenderer html={application.content} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{application.media && application.media.length > 0 && (
|
||||
<section>
|
||||
<label className="text-[11px] font-black text-zinc-400 uppercase tracking-widest mb-4 block">
|
||||
Tài liệu đính kèm ({application.media.length})
|
||||
</label>
|
||||
<div className="flex flex-row gap-4 overflow-x-auto pb-4 scrollbar-hide">
|
||||
{application.media.map((item: any) => {
|
||||
const isImg = isImageFile(item);
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
onClick={() => handleMediaClick(item)}
|
||||
className="group relative min-w-[160px] h-[160px] overflow-hidden rounded-2xl border-2 border-zinc-100 dark:border-zinc-800 cursor-pointer bg-zinc-100 dark:bg-zinc-900 flex flex-col items-center justify-center transition-all hover:border-blue-500"
|
||||
>
|
||||
{isImg ? (
|
||||
<>
|
||||
<img
|
||||
src={`${URL_MEDIA}${item.storage_key}`}
|
||||
alt={item.original_name}
|
||||
className="h-full w-full object-cover transition-transform duration-500 group-hover:scale-110"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-end p-3">
|
||||
<p className="text-[10px] text-white font-bold truncate">
|
||||
Xem ảnh
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex flex-col items-center p-4">
|
||||
<div className="w-12 h-12 mb-3 flex items-center justify-center bg-white dark:bg-zinc-800 rounded-xl shadow-inner text-2xl">
|
||||
📄
|
||||
</div>
|
||||
<p className="text-[10px] font-black text-zinc-700 dark:text-zinc-200 uppercase tracking-tighter">
|
||||
.{item.storage_key.split(".").pop()}
|
||||
</p>
|
||||
<p className="text-[9px] text-zinc-400 truncate w-28 mt-1 font-medium italic">
|
||||
{item.original_name}
|
||||
</p>
|
||||
<span className="mt-3 text-[9px] bg-blue-600 text-white px-3 py-1 rounded-full font-black uppercase opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
Preview
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{application.status !== "APPROVED" &&
|
||||
application.status !== "REJECTED" && (
|
||||
<div className="pt-8 border-t border-zinc-200 dark:border-zinc-800">
|
||||
<div className="flex justify-end w-full">
|
||||
<button
|
||||
onClick={handleDelete}
|
||||
disabled={isDeleting}
|
||||
className="group flex items-center gap-2 px-8 py-3 bg-red-100 hover:bg-red-500 text-red-600 hover:text-white dark:bg-red-950/20 dark:hover:bg-red-600 dark:text-red-400 dark:hover:text-white text-sm font-black rounded-xl transition-all duration-300 disabled:opacity-50"
|
||||
>
|
||||
{isDeleting ? "ĐANG XỬ LÝ..." : "XÓA"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{application?.reviewer && (
|
||||
<section className="pt-8 border-t border-zinc-200 dark:border-zinc-800">
|
||||
<label className="text-[11px] font-bold text-zinc-500 dark:text-zinc-400 uppercase tracking-wider mb-3 block">
|
||||
Phản hồi từ người kiểm duyệt
|
||||
</label>
|
||||
|
||||
<div className="p-5 rounded-xl border border-zinc-200 dark:border-zinc-800 bg-zinc-50/50 dark:bg-zinc-900/50">
|
||||
<div className="flex items-start justify-between mb-5">
|
||||
<div className="flex items-center gap-3">
|
||||
{application?.reviewer?.avatar_url ? (
|
||||
<Image
|
||||
src={application.reviewer.avatar_url}
|
||||
alt={application.reviewer.display_name || "Avatar"}
|
||||
width={36}
|
||||
height={36}
|
||||
className="w-9 h-9 rounded-full object-cover border border-zinc-200 dark:border-zinc-700 bg-white dark:bg-zinc-800"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-9 h-9 rounded-full bg-zinc-200 dark:bg-zinc-800 flex items-center justify-center text-zinc-500 font-medium text-sm">
|
||||
{application?.reviewer?.display_name?.charAt(0) || "R"}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-zinc-900 dark:text-zinc-100">
|
||||
{application?.reviewer?.display_name}
|
||||
</h4>
|
||||
<p className="text-[11px] text-zinc-500 dark:text-zinc-400">
|
||||
{application?.reviewer?.email}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-[11px] text-zinc-400 dark:text-zinc-500 tabular-nums">
|
||||
{formatDate(application?.reviewed_at)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="relative ml-4 pl-4 border-l border-zinc-300 dark:border-zinc-700">
|
||||
<div className="text-sm text-zinc-700 dark:text-zinc-300 leading-relaxed">
|
||||
{application?.review_note ? (
|
||||
<p>{application.review_note}</p>
|
||||
) : (
|
||||
<p className="italic text-zinc-500">Không có ghi chú bổ sung.</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Lightbox
|
||||
index={index}
|
||||
open={index >= 0}
|
||||
close={() => setIndex(-1)}
|
||||
slides={imageSlides}
|
||||
plugins={[Zoom, Captions]}
|
||||
styles={{
|
||||
root: {
|
||||
zIndex: 99999,
|
||||
"--yarl__color_backdrop": "rgba(0, 0, 0, 0.95)",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
46
src/app/user/account/page.tsx
Normal file
46
src/app/user/account/page.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
"use client";
|
||||
|
||||
import AccountDetails from "@/components/user-profile/AccountDetails";
|
||||
import UserInfoCard from "@/components/user-profile/UserInfoCard";
|
||||
import UserMetaCard from "@/components/user-profile/UserMetaCard";
|
||||
import { UserMetaCardProps } from "@/interface/user";
|
||||
import { apiGetCurrentUser } from "@/service/auth";
|
||||
import { setUserData } from "@/store/features/userSlice";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
|
||||
export default function Profile() {
|
||||
const [user, setUser] = useState<UserMetaCardProps | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
const fetchUser = async () => {
|
||||
try {
|
||||
const userData = await apiGetCurrentUser();
|
||||
dispatch(setUserData(userData.data));
|
||||
setUser(userData);
|
||||
} catch (err) {
|
||||
console.error("Lỗi:", err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
fetchUser();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="rounded-2xl border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-white/[0.03] lg:p-6">
|
||||
<h3 className="mb-5 text-lg font-semibold text-gray-800 dark:text-white/90 lg:mb-7">
|
||||
Profile
|
||||
</h3>
|
||||
<div className="space-y-6">
|
||||
<UserMetaCard data={user ?? {}} />
|
||||
<UserInfoCard data={{ ...user, openEdit: true }} />
|
||||
<AccountDetails data={user ?? {}} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
56
src/app/user/layout.tsx
Normal file
56
src/app/user/layout.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
"use client";
|
||||
|
||||
import { useSidebar } from "@/context/SidebarContext";
|
||||
import AppHeader from "@/layout/AppHeader";
|
||||
import AppSidebar from "@/layout/AppSidebar";
|
||||
import Backdrop from "@/layout/Backdrop";
|
||||
import { apiGetCurrentUser } from "@/service/auth";
|
||||
import { setUserData } from "@/store/features/userSlice";
|
||||
import React, { useEffect } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
|
||||
export default function AdminLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const { isExpanded, isHovered, isMobileOpen } = useSidebar();
|
||||
const dispatch = useDispatch()
|
||||
|
||||
useEffect(() => {
|
||||
const fetchUser = async () => {
|
||||
try {
|
||||
const userData = await apiGetCurrentUser();
|
||||
dispatch(setUserData(userData.data));
|
||||
} catch (err) {
|
||||
console.error("Lỗi:", err);
|
||||
}
|
||||
};
|
||||
fetchUser();
|
||||
}, [])
|
||||
|
||||
|
||||
// Dynamic class for main content margin based on sidebar state
|
||||
const mainContentMargin = isMobileOpen
|
||||
? "ml-0"
|
||||
: isExpanded || isHovered
|
||||
? "lg:ml-[290px]"
|
||||
: "lg:ml-[90px]";
|
||||
|
||||
return (
|
||||
<div className="min-h-screen xl:flex">
|
||||
{/* Sidebar and Backdrop */}
|
||||
<AppSidebar />
|
||||
<Backdrop />
|
||||
{/* Main Content Area */}
|
||||
<div
|
||||
className={`flex-1 transition-all duration-300 ease-in-out ${mainContentMargin}`}
|
||||
>
|
||||
{/* Header */}
|
||||
<AppHeader />
|
||||
{/* Page Content */}
|
||||
<div className="p-4 mx-auto max-w-(--breakpoint-2xl) md:p-6">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
74
src/app/user/library/page.tsx
Normal file
74
src/app/user/library/page.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import ApplicationLibrary from "@/components/user-profile/ApplicationList";
|
||||
import { MediaDto } from "@/interface/media";
|
||||
import { apiGetCurrentUserApplications, apiGetCurrentUserMedia } from "@/service/userService";
|
||||
import MediaLibrary from "@/components/user-profile/Media";
|
||||
|
||||
export default function LibraryPage() {
|
||||
const [mediaData, setMediaData] = useState<MediaDto | null>(null);
|
||||
const [applications, setApplications] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchLibraryContent = async () => {
|
||||
try {
|
||||
const [mediaResponse, userApplications] = await Promise.all([
|
||||
apiGetCurrentUserMedia(),
|
||||
apiGetCurrentUserApplications()
|
||||
]);
|
||||
|
||||
if (userApplications?.data) setApplications(userApplications.data);
|
||||
setMediaData(mediaResponse);
|
||||
} catch (err) {
|
||||
console.error("Lỗi khi tải thư viện:", err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
fetchLibraryContent();
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex h-[50vh] items-center justify-center">
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-4 border-blue-500 border-t-transparent"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const hasNoData = (!mediaData?.data || mediaData.data.length === 0) && applications.length === 0;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50/50 p-4 dark:bg-zinc-950 lg:p-8">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold tracking-tight text-gray-900 dark:text-white">
|
||||
Thư viện
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{hasNoData ? (
|
||||
<div className="flex flex-col items-center justify-center rounded-xl border-2 border-dashed border-gray-200 py-24 text-center dark:border-zinc-800">
|
||||
<p className="text-lg font-medium text-gray-500 dark:text-gray-400">
|
||||
Chưa có nội dung nào trong thư viện
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-12">
|
||||
{(mediaData?.data?.length ?? 0) > 0 && (
|
||||
<section>
|
||||
<MediaLibrary data={mediaData ?? {}} />
|
||||
</section>
|
||||
)}
|
||||
|
||||
{applications.length > 0 && (
|
||||
<section>
|
||||
<ApplicationLibrary applications={applications} />
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
42
src/app/user/page.tsx
Normal file
42
src/app/user/page.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import type { Metadata } from "next";
|
||||
import { EcommerceMetrics } from "@/components/ecommerce/EcommerceMetrics";
|
||||
import React from "react";
|
||||
import MonthlyTarget from "@/components/ecommerce/MonthlyTarget";
|
||||
import MonthlySalesChart from "@/components/ecommerce/MonthlySalesChart";
|
||||
import StatisticsChart from "@/components/ecommerce/StatisticsChart";
|
||||
import RecentOrders from "@/components/ecommerce/RecentOrders";
|
||||
import DemographicCard from "@/components/ecommerce/DemographicCard";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title:
|
||||
"Admin Dashboard",
|
||||
description: "This is Dashboard Home for History Web",
|
||||
};
|
||||
|
||||
export default function Ecommerce() {
|
||||
return (
|
||||
<div className="grid grid-cols-12 gap-4 md:gap-6">
|
||||
<div className="col-span-12 space-y-6 xl:col-span-7">
|
||||
<EcommerceMetrics />
|
||||
|
||||
<MonthlySalesChart />
|
||||
</div>
|
||||
|
||||
<div className="col-span-12 xl:col-span-5">
|
||||
<MonthlyTarget />
|
||||
</div>
|
||||
|
||||
<div className="col-span-12">
|
||||
<StatisticsChart />
|
||||
</div>
|
||||
|
||||
<div className="col-span-12 xl:col-span-5">
|
||||
<DemographicCard />
|
||||
</div>
|
||||
|
||||
<div className="col-span-12 xl:col-span-7">
|
||||
<RecentOrders />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
362
src/app/user/role-upgrade/page.tsx
Normal file
362
src/app/user/role-upgrade/page.tsx
Normal file
@@ -0,0 +1,362 @@
|
||||
"use client";
|
||||
|
||||
import PageBreadcrumb from "@/components/common/PageBreadCrumb";
|
||||
import RichTextEditor from "@/components/form/role-upgrade/Editor";
|
||||
import {
|
||||
confirmUpload,
|
||||
getPresignedUrl,
|
||||
uploadFileToS3,
|
||||
} from "@/service/mediaService";
|
||||
import React, { useRef, useState } from "react";
|
||||
import Image from "next/image";
|
||||
import Lightbox from "yet-another-react-lightbox";
|
||||
import Zoom from "yet-another-react-lightbox/plugins/zoom";
|
||||
import Captions from "yet-another-react-lightbox/plugins/captions";
|
||||
import "yet-another-react-lightbox/styles.css";
|
||||
import "yet-another-react-lightbox/plugins/captions.css";
|
||||
import { createHistorianCV } from "@/service/historianService";
|
||||
import { toast } from "sonner";
|
||||
import Swal from "sweetalert2";
|
||||
|
||||
type PendingFile = {
|
||||
id: string;
|
||||
file: File;
|
||||
previewUrl: string;
|
||||
name: string;
|
||||
size: number;
|
||||
type: "image" | "document";
|
||||
extension: string;
|
||||
presigned?: any;
|
||||
};
|
||||
|
||||
export default function RoleUpgrade() {
|
||||
const [content, setContent] = useState<string>("");
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [verifyType, setVerifyType] = useState<string>("OTHER");
|
||||
const [pendingFiles, setPendingFiles] = useState<PendingFile[]>([]);
|
||||
const [lightboxIndex, setLightboxIndex] = useState(-1);
|
||||
const [isPreparingFiles, setIsPreparingFiles] = useState(false);
|
||||
const [showPreview, setShowPreview] = useState(false);
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||
|
||||
const handleIframeLoad = () => {
|
||||
const iframe = iframeRef.current;
|
||||
|
||||
if (!iframe || !iframe.contentDocument) return;
|
||||
|
||||
const updateHeight = () => {
|
||||
if (iframe.contentDocument) {
|
||||
iframe.style.height = "auto";
|
||||
const scrollHeight =
|
||||
iframe.contentDocument.documentElement.scrollHeight;
|
||||
iframe.style.height = `${scrollHeight}px`;
|
||||
}
|
||||
};
|
||||
|
||||
updateHeight();
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
updateHeight();
|
||||
});
|
||||
|
||||
if (iframe.contentDocument.body) {
|
||||
resizeObserver.observe(iframe.contentDocument.body);
|
||||
}
|
||||
};
|
||||
|
||||
const cleanHTMLContent = (rawHtml: string) => {
|
||||
if (!rawHtml) return "";
|
||||
|
||||
const doc = new DOMParser().parseFromString(rawHtml, "text/html");
|
||||
let decoded = doc.documentElement.textContent || rawHtml;
|
||||
|
||||
decoded = decoded.replace(/<pre[^>]*>/g, "").replace(/<\/pre>/g, "");
|
||||
|
||||
return decoded;
|
||||
};
|
||||
|
||||
const handleContentChange = (value: string) => {
|
||||
setContent(value);
|
||||
};
|
||||
|
||||
const handleFileChange = async (
|
||||
event: React.ChangeEvent<HTMLInputElement>,
|
||||
) => {
|
||||
const files = event.target.files;
|
||||
if (!files || files.length === 0) return;
|
||||
|
||||
setIsPreparingFiles(true);
|
||||
try {
|
||||
const newFilesPromises = Array.from(files).map(async (file) => {
|
||||
const isImage = file.type.startsWith("image/");
|
||||
const extension = file.name.split(".").pop()?.toLowerCase() || "";
|
||||
const presigned = await getPresignedUrl(file);
|
||||
|
||||
return {
|
||||
id: Math.random().toString(36).substring(7),
|
||||
file: file,
|
||||
previewUrl: isImage ? URL.createObjectURL(file) : "",
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
type: isImage ? "image" : "document",
|
||||
extension: extension,
|
||||
presigned: presigned,
|
||||
} as PendingFile;
|
||||
});
|
||||
|
||||
const newPendingFiles = await Promise.all(newFilesPromises);
|
||||
setPendingFiles((prev) => [...prev, ...newPendingFiles]);
|
||||
} catch (error) {
|
||||
console.error("Lỗi khi chuẩn bị file:", error);
|
||||
toast.error("Lỗi khi chuẩn bị kết nối upload.");
|
||||
} finally {
|
||||
setIsPreparingFiles(false);
|
||||
if (event.target) event.target.value = "";
|
||||
}
|
||||
};
|
||||
|
||||
const removePendingFile = (idToRemove: string, e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
setPendingFiles((prev) => prev.filter((item) => item.id !== idToRemove));
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!content.trim()) {
|
||||
toast.warning("Vui lòng nhập nội dung hồ sơ!");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
|
||||
const uploadPromises = pendingFiles.map(async (item) => {
|
||||
await uploadFileToS3(item.file, item.presigned);
|
||||
const confirmRes = await confirmUpload(item.presigned.token_id);
|
||||
return confirmRes?.data?.id || confirmRes?.id;
|
||||
});
|
||||
const uploadedMediaIds = await Promise.all(uploadPromises);
|
||||
|
||||
const cleanPayloadContent = cleanHTMLContent(content);
|
||||
|
||||
const payload = {
|
||||
content: cleanPayloadContent,
|
||||
media_ids: uploadedMediaIds,
|
||||
verify_type: verifyType,
|
||||
};
|
||||
|
||||
await createHistorianCV(payload);
|
||||
|
||||
Swal.fire({
|
||||
title: "Gửi yêu cầu thành công!",
|
||||
text: "Hồ sơ của bạn đã được ghi nhận và đang chờ duyệt.",
|
||||
icon: "success",
|
||||
});
|
||||
|
||||
setContent("");
|
||||
setPendingFiles([]);
|
||||
setVerifyType("OTHER");
|
||||
} catch (error) {
|
||||
console.error("Lỗi submit:", error);
|
||||
toast.error("Có lỗi xảy ra khi gửi yêu cầu.");
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const imageFiles = pendingFiles.filter((f) => f.type === "image");
|
||||
const slides = imageFiles.map((item) => ({
|
||||
src: item.previewUrl,
|
||||
title: item.name,
|
||||
description: `Size: ${(item.size / 1024).toFixed(2)} KB`,
|
||||
}));
|
||||
|
||||
const handleItemClick = (item: PendingFile) => {
|
||||
if (item.type === "image") {
|
||||
const index = imageFiles.findIndex((img) => img.id === item.id);
|
||||
setLightboxIndex(index);
|
||||
} else {
|
||||
const fileUrl = URL.createObjectURL(item.file);
|
||||
window.open(fileUrl, "_blank");
|
||||
}
|
||||
};
|
||||
|
||||
const getDocumentStyle = (ext: string) => {
|
||||
if (["pdf"].includes(ext))
|
||||
return { color: "text-red-500", bg: "bg-red-50" };
|
||||
if (["doc", "docx"].includes(ext))
|
||||
return { color: "text-blue-500", bg: "bg-blue-50" };
|
||||
if (["xls", "xlsx"].includes(ext))
|
||||
return { color: "text-emerald-500", bg: "bg-emerald-50" };
|
||||
return { color: "text-gray-500", bg: "bg-gray-100" };
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto pb-20">
|
||||
<PageBreadcrumb pageTitle="Đăng ký trở thành Nhà sử học" />
|
||||
|
||||
<div className="flex items-center justify-between bg-white dark:bg-gray-900 p-2 rounded-xl border border-gray-200 dark:border-gray-800 mb-6">
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPreview(false)}
|
||||
className={`px-4 py-2 text-sm font-bold rounded-lg transition-all ${!showPreview ? "bg-blue-600 text-white" : "text-gray-500 hover:bg-gray-100"}`}
|
||||
>
|
||||
Soạn thảo
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPreview(true)}
|
||||
className={`px-4 py-2 text-sm font-bold rounded-lg transition-all ${showPreview ? "bg-blue-600 text-white" : "text-gray-500 hover:bg-gray-100"}`}
|
||||
>
|
||||
Xem trước giao diện
|
||||
</button>
|
||||
</div>
|
||||
<span className="text-[10px] font-black uppercase text-gray-400 mr-2 tracking-widest">
|
||||
{showPreview ? "Preview Mode" : "Edit Mode"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="relative min-h-[400px]">
|
||||
{!showPreview ? (
|
||||
<RichTextEditor value={content} onChange={handleContentChange} />
|
||||
) : (
|
||||
<div className="bg-white border border-zinc-200 rounded-xl shadow-inner overflow-hidden min-h-[400px]">
|
||||
{content ? (
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
onLoad={handleIframeLoad}
|
||||
srcDoc={`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
body {
|
||||
font-family: system-ui, sans-serif;
|
||||
padding: 20px;
|
||||
margin: 0;
|
||||
color: black;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
img { max-width: 100%; height: auto; display: block; margin-top: 10px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
${cleanHTMLContent(content)}
|
||||
</body>
|
||||
</html>
|
||||
`}
|
||||
title="Preview"
|
||||
className="w-full border-none flex-1 transition-all duration-300"
|
||||
sandbox="allow-same-origin"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center h-[400px] text-zinc-400 italic bg-gray-50 dark:bg-gray-900">
|
||||
<p>Chưa có nội dung để xem trước</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="p-5 bg-white border border-gray-200 rounded-2xl dark:border-gray-800 dark:bg-gray-900">
|
||||
<label className="block mb-3 text-sm font-semibold text-gray-700 dark:text-white">
|
||||
Loại hồ sơ xác minh <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<select
|
||||
value={verifyType}
|
||||
onChange={(e) => setVerifyType(e.target.value)}
|
||||
className="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl outline-none focus:border-blue-500 dark:bg-gray-800 dark:border-gray-700"
|
||||
>
|
||||
<option value="OTHER">Tài liệu khác (Other)</option>
|
||||
<option value="ID_CARD">Thẻ nhận dạng (ID Card)</option>
|
||||
<option value="EDUCATION">Bằng cấp giáo dục (Education)</option>
|
||||
<option value="EXPERT">Chứng nhận chuyên gia (Expert)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Upload Files */}
|
||||
<div className="p-5 bg-white border border-gray-200 rounded-2xl dark:border-gray-800 dark:bg-gray-900">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold">Tài liệu đính kèm</h3>
|
||||
<label className="cursor-pointer px-4 py-2 rounded-lg border border-dashed border-blue-500 text-blue-500 hover:bg-blue-50 transition">
|
||||
<input
|
||||
type="file"
|
||||
multiple
|
||||
className="hidden"
|
||||
accept="image/*,.pdf,.doc,.docx"
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
<span>+ Thêm tệp</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{pendingFiles.length > 0 && (
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 md:grid-cols-5 gap-4">
|
||||
{pendingFiles.map((item) => {
|
||||
const docStyle = getDocumentStyle(item.extension);
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
className="relative group aspect-square border rounded-xl overflow-hidden bg-gray-50"
|
||||
>
|
||||
{item.type === "image" ? (
|
||||
<Image
|
||||
src={item.previewUrl}
|
||||
alt="preview"
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className={`flex flex-col items-center justify-center h-full ${docStyle.bg} p-2`}
|
||||
>
|
||||
<span className={`text-xs font-bold ${docStyle.color}`}>
|
||||
{item.extension.toUpperCase()}
|
||||
</span>
|
||||
<span className="text-[10px] truncate w-full text-center mt-1">
|
||||
{item.name}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
onClick={(e) => removePendingFile(item.id, e)}
|
||||
className="absolute top-1 right-1 bg-red-500 text-white rounded-full p-1 opacity-0 group-hover:opacity-100 transition"
|
||||
>
|
||||
<svg
|
||||
className="w-3 h-3"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
disabled={isSubmitting || isPreparingFiles}
|
||||
className={`w-full py-4 text-white font-bold rounded-2xl shadow-lg transition-all ${isSubmitting || isPreparingFiles ? "bg-gray-400" : "bg-blue-600 hover:bg-blue-700"}`}
|
||||
>
|
||||
{isSubmitting ? "Đang xử lý..." : "Gửi yêu cầu nâng cấp"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Lightbox
|
||||
index={lightboxIndex}
|
||||
open={lightboxIndex >= 0}
|
||||
close={() => setLightboxIndex(-1)}
|
||||
slides={slides}
|
||||
plugins={[Zoom, Captions]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
271
src/components/auth/SignInForm.tsx
Normal file
271
src/components/auth/SignInForm.tsx
Normal file
@@ -0,0 +1,271 @@
|
||||
"use client";
|
||||
|
||||
import Checkbox from "@/components/form/input/Checkbox";
|
||||
import Input from "@/components/form/input/InputField";
|
||||
import Label from "@/components/form/Label";
|
||||
import { ChevronLeftIcon, EyeCloseIcon, EyeIcon } from "@/icons";
|
||||
import { apiGetCurrentUser, apiSignIn } from "@/service/auth";
|
||||
import Link from "next/link";
|
||||
import React, { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { API, HOME_URL } from "../../../api";
|
||||
import { setUserData } from "@/store/features/userSlice";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export default function SignInForm() {
|
||||
const router = useRouter();
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const dispatch = useDispatch();
|
||||
const [isChecked, setIsChecked] = useState(false);
|
||||
const [errorMsg, setErrorMsg] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
email: "",
|
||||
password: "",
|
||||
});
|
||||
|
||||
const isFormEmpty = !formData.email.trim() || !formData.password.trim();
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setFormData({ ...formData, [e.target.name]: e.target.value });
|
||||
setErrorMsg("");
|
||||
};
|
||||
|
||||
const isValidEmail = (email: string) => {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailRegex.test(email);
|
||||
};
|
||||
|
||||
const isValidPassword = (pass: string) => {
|
||||
const passwordRegex = /^(?=.*[A-Z])(?=.*\d)(?=.*[\W_]).{8,}$/;
|
||||
return passwordRegex.test(pass);
|
||||
};
|
||||
|
||||
const handleSignInClick = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
if (loading || isFormEmpty) return;
|
||||
|
||||
setErrorMsg("");
|
||||
|
||||
if (!isValidEmail(formData.email)) {
|
||||
setErrorMsg("Email không đúng định dạng.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isValidPassword(formData.password)) {
|
||||
setErrorMsg(
|
||||
"Mật khẩu tối thiểu 8 ký tự, 1 in hoa, 1 số và 1 ký tự đặc biệt.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
const res = await apiSignIn(formData);
|
||||
// console.log("API Sign In Response:", res);
|
||||
|
||||
if (res.status === true) {
|
||||
toast.success("Đăng nhập thành công!");
|
||||
const data = await apiGetCurrentUser();
|
||||
|
||||
// console.log("Current User Data:", data);
|
||||
if (data?.data) {
|
||||
dispatch(setUserData(data.data));
|
||||
router.push("/");
|
||||
}
|
||||
} else {
|
||||
toast.error("Email hoặc mật khẩu không đúng.");
|
||||
}
|
||||
} catch (error) {
|
||||
setErrorMsg("Lỗi khi đăng nhập. Vui lòng thử lại.");
|
||||
toast.error("Đăng nhập thất bại. Vui lòng kiểm tra lại thông tin.");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-1 lg:w-1/2 w-full">
|
||||
<div className="w-full max-w-md sm:pt-10 mx-auto mb-5">
|
||||
<Link
|
||||
href="/"
|
||||
className="inline-flex items-center text-sm text-gray-500 transition-colors hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
|
||||
>
|
||||
<ChevronLeftIcon />
|
||||
Back to dashboard
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex flex-col justify-center flex-1 w-full max-w-md mx-auto">
|
||||
<div>
|
||||
<div className="mb-5 sm:mb-8">
|
||||
<h1 className="mb-2 font-semibold text-gray-800 text-title-sm dark:text-white/90 sm:text-title-md">
|
||||
Sign In
|
||||
</h1>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Enter your email and password to sign in!
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<div className="grid grid-cols-1 gap-3 sm:gap-5">
|
||||
<button
|
||||
onClick={() => {
|
||||
const redirectUrl = HOME_URL;
|
||||
window.location.href = `${API.Auth.GOOGLE_LOGIN}?redirect=${redirectUrl}`;
|
||||
}}
|
||||
className="inline-flex items-center justify-center gap-3 py-3 text-sm font-normal text-gray-700 transition-colors bg-gray-100 rounded-lg px-7 hover:bg-gray-200 hover:text-gray-800 dark:bg-white/5 dark:text-white/90 dark:hover:bg-white/10"
|
||||
>
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M18.7511 10.1944C18.7511 9.47495 18.6915 8.94995 18.5626 8.40552H10.1797V11.6527H15.1003C15.0011 12.4597 14.4654 13.675 13.2749 14.4916L13.2582 14.6003L15.9087 16.6126L16.0924 16.6305C17.7788 15.1041 18.7511 12.8583 18.7511 10.1944Z"
|
||||
fill="#4285F4"
|
||||
/>
|
||||
<path
|
||||
d="M10.1788 18.75C12.5895 18.75 14.6133 17.9722 16.0915 16.6305L13.274 14.4916C12.5201 15.0068 11.5081 15.3666 10.1788 15.3666C7.81773 15.3666 5.81379 13.8402 5.09944 11.7305L4.99473 11.7392L2.23868 13.8295L2.20264 13.9277C3.67087 16.786 6.68674 18.75 10.1788 18.75Z"
|
||||
fill="#34A853"
|
||||
/>
|
||||
<path
|
||||
d="M5.10014 11.7305C4.91165 11.186 4.80257 10.6027 4.80257 9.99992C4.80257 9.3971 4.91165 8.81379 5.09022 8.26935L5.08523 8.1534L2.29464 6.02954L2.20333 6.0721C1.5982 7.25823 1.25098 8.5902 1.25098 9.99992C1.25098 11.4096 1.5982 12.7415 2.20333 13.9277L5.10014 11.7305Z"
|
||||
fill="#FBBC05"
|
||||
/>
|
||||
<path
|
||||
d="M10.1789 4.63331C11.8554 4.63331 12.9864 5.34303 13.6312 5.93612L16.1511 3.525C14.6035 2.11528 12.5895 1.25 10.1789 1.25C6.68676 1.25 3.67088 3.21387 2.20264 6.07218L5.08953 8.26943C5.81381 6.15972 7.81776 4.63331 10.1789 4.63331Z"
|
||||
fill="#EB4335"
|
||||
/>
|
||||
</svg>
|
||||
Sign in with Google
|
||||
</button>
|
||||
</div>
|
||||
<div className="relative py-3 sm:py-5">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<div className="w-full border-t border-gray-200 dark:border-gray-800"></div>
|
||||
</div>
|
||||
<div className="relative flex justify-center text-sm">
|
||||
<span className="p-2 text-gray-400 bg-white dark:bg-gray-900 sm:px-5 sm:py-2">
|
||||
Or
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<form onSubmit={handleSignInClick}>
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<Label>
|
||||
Email <span className="text-error-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
name="email"
|
||||
placeholder="info@gmail.com"
|
||||
type="email"
|
||||
onChange={handleChange}
|
||||
defaultValue={formData.email}
|
||||
autoComplete="username"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>
|
||||
Password <span className="text-error-500">*</span>
|
||||
</Label>
|
||||
<div
|
||||
className={`relative ${formData.password.length > 0 && !isValidPassword(formData.password) ? "border border-red-500 ring-1 ring-red-500 rounded-lg" : ""}`}
|
||||
>
|
||||
<Input
|
||||
name="password"
|
||||
type={showPassword ? "text" : "password"}
|
||||
placeholder="Min. 8 characters"
|
||||
onChange={handleChange}
|
||||
defaultValue={formData.password}
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
<span
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute z-30 -translate-y-1/2 cursor-pointer right-4 top-1/2"
|
||||
>
|
||||
{showPassword ? <EyeIcon /> : <EyeCloseIcon />}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{errorMsg && (
|
||||
<p className="text-sm text-red-500 font-medium">{errorMsg}</p>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Checkbox checked={isChecked} onChange={setIsChecked} />
|
||||
<span className="block font-normal text-gray-700 text-theme-sm dark:text-gray-400">
|
||||
Keep me logged in
|
||||
</span>
|
||||
</div>
|
||||
<Link
|
||||
href="/reset-password"
|
||||
className="text-sm text-brand-500 hover:text-brand-600 dark:text-brand-400"
|
||||
>
|
||||
Forgot password?
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
disabled={loading || isFormEmpty}
|
||||
type="submit"
|
||||
className={`w-full flex items-center justify-center px-4 py-3 text-sm font-medium text-white transition rounded-lg shadow-theme-xs
|
||||
${loading || isFormEmpty ? "bg-gray-400 cursor-not-allowed" : "bg-brand-500 hover:bg-brand-600"}`}
|
||||
>
|
||||
{loading ? (
|
||||
<span className="flex items-center gap-2">
|
||||
<svg
|
||||
className="animate-spin h-5 w-5 text-white"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
className="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="4"
|
||||
></circle>
|
||||
<path
|
||||
className="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
></path>
|
||||
</svg>
|
||||
Signing in...
|
||||
</span>
|
||||
) : (
|
||||
"Sign in"
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className="mt-5">
|
||||
<p className="text-sm font-normal text-center text-gray-700 dark:text-gray-400 sm:text-start">
|
||||
Don't have an account? {""}
|
||||
<Link
|
||||
href="/signup"
|
||||
className="text-brand-500 hover:text-brand-600 dark:text-brand-400"
|
||||
>
|
||||
Sign Up
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
384
src/components/auth/SignUpForm.tsx
Normal file
384
src/components/auth/SignUpForm.tsx
Normal file
@@ -0,0 +1,384 @@
|
||||
"use client";
|
||||
import Checkbox from "@/components/form/input/Checkbox";
|
||||
import Input from "@/components/form/input/InputField";
|
||||
import Label from "@/components/form/Label";
|
||||
import { ChevronLeftIcon, EyeCloseIcon, EyeIcon } from "@/icons";
|
||||
import { apiCreateOTP, apiSignUp, apiVerifyOTP } from "@/service/auth";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { API, HOME_URL } from "../../../api";
|
||||
import Swal from "sweetalert2";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
|
||||
export default function SignUpForm() {
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [isChecked, setIsChecked] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const [step, setStep] = useState(1);
|
||||
const [formData, setFormData] = useState({
|
||||
fname: "",
|
||||
lname: "",
|
||||
email: "",
|
||||
password: "",
|
||||
});
|
||||
const [otp, setOtp] = useState("");
|
||||
const [errorMsg, setErrorMsg] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setFormData({ ...formData, [e.target.name]: e.target.value });
|
||||
setErrorMsg("");
|
||||
};
|
||||
|
||||
const isValidEmail = (email: string) => {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailRegex.test(email);
|
||||
};
|
||||
|
||||
const isValidPassword = (pass: string) => {
|
||||
const passwordRegex = /^(?=.*[A-Z])(?=.*\d)(?=.*[\W_]).{8,}$/;
|
||||
return passwordRegex.test(pass);
|
||||
};
|
||||
|
||||
const handleSignUpClick = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setErrorMsg("");
|
||||
|
||||
if (
|
||||
!formData.fname ||
|
||||
!formData.lname ||
|
||||
!formData.email ||
|
||||
!formData.password
|
||||
) {
|
||||
setErrorMsg("Vui lòng điền đầy đủ thông tin.");
|
||||
return;
|
||||
}
|
||||
if (!isValidEmail(formData.email)) {
|
||||
setErrorMsg("Email không đúng định dạng.");
|
||||
return;
|
||||
}
|
||||
if (!isValidPassword(formData.password)) {
|
||||
setErrorMsg(
|
||||
"Mật khẩu chưa đủ điều kiện. Vui lòng nhập tối thiểu 8 ký tự, 1 in hoa, 1 số và 1 ký tự đặc biệt.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
await apiCreateOTP(formData.email);
|
||||
setStep(2);
|
||||
} catch (error) {
|
||||
setErrorMsg("Lỗi khi tạo OTP. Vui lòng thử lại.");
|
||||
toast.error("Tạo OTP thất bại. Vui lòng kiểm tra lại thông tin.");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleVerifyOtpSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setErrorMsg("");
|
||||
|
||||
if (!otp) {
|
||||
setErrorMsg("Vui lòng nhập OTP.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
const verifyRes = await apiVerifyOTP(formData.email, otp);
|
||||
|
||||
const tokenId = verifyRes?.data?.token_id;
|
||||
if (!tokenId) {
|
||||
throw new Error("OTP không hợp lệ hoặc không có token_id");
|
||||
}
|
||||
|
||||
const signupPayload = {
|
||||
display_name: `${formData.fname} ${formData.lname}`.trim(),
|
||||
email: formData.email,
|
||||
password: formData.password,
|
||||
token_id: tokenId,
|
||||
};
|
||||
|
||||
const signupRes = await apiSignUp(signupPayload);
|
||||
|
||||
await Swal.fire({
|
||||
title: "Tạo tài khoản thành công!. Quay về trang đăng nhập để tiếp tục",
|
||||
icon: "success",
|
||||
timer: 1500,
|
||||
showConfirmButton: false,
|
||||
});
|
||||
router.push("/signin");
|
||||
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Xác thực OTP hoặc đăng ký thất bại.";
|
||||
setErrorMsg(errorMessage || "Xác thực OTP hoặc đăng ký thất bại.");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-1 lg:w-1/2 w-full overflow-y-auto no-scrollbar">
|
||||
<div className="w-full max-w-md sm:pt-10 mx-auto mb-5">
|
||||
<Link
|
||||
href="/"
|
||||
className="inline-flex items-center text-sm text-gray-500 transition-colors hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
|
||||
>
|
||||
<ChevronLeftIcon />
|
||||
Back to dashboard
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col justify-center flex-1 w-full max-w-md mx-auto">
|
||||
<div>
|
||||
<div className="mb-5 sm:mb-8">
|
||||
<h1 className="mb-2 font-semibold text-gray-800 text-title-sm dark:text-white/90 sm:text-title-md">
|
||||
{step === 1 ? "Sign Up" : "Verify OTP"}
|
||||
</h1>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{step === 1
|
||||
? "Enter your email and password to sign up!"
|
||||
: `We sent an OTP to ${formData.email}. Please check your email and enter it below.`}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{errorMsg && (
|
||||
<div className="mb-4 text-sm text-error-500 bg-error-50 p-3 rounded">
|
||||
{errorMsg}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 1 && (
|
||||
<>
|
||||
<div className="grid grid-cols-1 gap-3 sm:gap-5">
|
||||
<button
|
||||
onClick={() => {
|
||||
const redirectUrl = HOME_URL;
|
||||
window.location.href = `${API.Auth.GOOGLE_LOGIN}?redirect=${redirectUrl}`;
|
||||
}}
|
||||
className="inline-flex items-center justify-center gap-3 py-3 text-sm font-normal text-gray-700 transition-colors bg-gray-100 rounded-lg px-7 hover:bg-gray-200 hover:text-gray-800 dark:bg-white/5 dark:text-white/90 dark:hover:bg-white/10"
|
||||
>
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M18.7511 10.1944C18.7511 9.47495 18.6915 8.94995 18.5626 8.40552H10.1797V11.6527H15.1003C15.0011 12.4597 14.4654 13.675 13.2749 14.4916L13.2582 14.6003L15.9087 16.6126L16.0924 16.6305C17.7788 15.1041 18.7511 12.8583 18.7511 10.1944Z"
|
||||
fill="#4285F4"
|
||||
/>
|
||||
<path
|
||||
d="M10.1788 18.75C12.5895 18.75 14.6133 17.9722 16.0915 16.6305L13.274 14.4916C12.5201 15.0068 11.5081 15.3666 10.1788 15.3666C7.81773 15.3666 5.81379 13.8402 5.09944 11.7305L4.99473 11.7392L2.23868 13.8295L2.20264 13.9277C3.67087 16.786 6.68674 18.75 10.1788 18.75Z"
|
||||
fill="#34A853"
|
||||
/>
|
||||
<path
|
||||
d="M5.10014 11.7305C4.91165 11.186 4.80257 10.6027 4.80257 9.99992C4.80257 9.3971 4.91165 8.81379 5.09022 8.26935L5.08523 8.1534L2.29464 6.02954L2.20333 6.0721C1.5982 7.25823 1.25098 8.5902 1.25098 9.99992C1.25098 11.4096 1.5982 12.7415 2.20333 13.9277L5.10014 11.7305Z"
|
||||
fill="#FBBC05"
|
||||
/>
|
||||
<path
|
||||
d="M10.1789 4.63331C11.8554 4.63331 12.9864 5.34303 13.6312 5.93612L16.1511 3.525C14.6035 2.11528 12.5895 1.25 10.1789 1.25C6.68676 1.25 3.67088 3.21387 2.20264 6.07218L5.08953 8.26943C5.81381 6.15972 7.81776 4.63331 10.1789 4.63331Z"
|
||||
fill="#EB4335"
|
||||
/>
|
||||
</svg>
|
||||
Sign up with Google
|
||||
</button>
|
||||
</div>
|
||||
<div className="relative py-3 sm:py-5">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<div className="w-full border-t border-gray-200 dark:border-gray-800"></div>
|
||||
</div>
|
||||
<div className="relative flex justify-center text-sm">
|
||||
<span className="p-2 text-gray-400 bg-white dark:bg-gray-900 sm:px-5 sm:py-2">
|
||||
Or
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSignUpClick}>
|
||||
<div className="space-y-5">
|
||||
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2">
|
||||
<div className="sm:col-span-1">
|
||||
<Label>
|
||||
First Name<span className="text-error-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
type="text"
|
||||
name="fname"
|
||||
defaultValue={formData.fname}
|
||||
onChange={handleChange}
|
||||
placeholder="Enter your first name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="sm:col-span-1">
|
||||
<Label>
|
||||
Last Name<span className="text-error-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
type="text"
|
||||
name="lname"
|
||||
defaultValue={formData.lname}
|
||||
onChange={handleChange}
|
||||
placeholder="Enter your last name"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>
|
||||
Email<span className="text-error-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
type="email"
|
||||
name="email"
|
||||
defaultValue={formData.email}
|
||||
onChange={handleChange}
|
||||
placeholder="Enter your email"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>
|
||||
Password<span className="text-error-500">*</span>
|
||||
</Label>
|
||||
|
||||
<div
|
||||
className={`relative ${formData.password.length > 0 && !isValidPassword(formData.password) ? "border border-red-500 ring-1 ring-red-500 rounded-lg" : ""}`}
|
||||
>
|
||||
<Input
|
||||
name="password"
|
||||
defaultValue={formData.password}
|
||||
onChange={handleChange}
|
||||
placeholder="Min. 8 characters"
|
||||
type={showPassword ? "text" : "password"}
|
||||
/>
|
||||
<span
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute z-30 -translate-y-1/2 cursor-pointer right-4 top-1/2"
|
||||
>
|
||||
{showPassword ? <EyeIcon /> : <EyeCloseIcon />}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p
|
||||
className={`mt-2 text-xs ${formData.password.length === 0 ? "text-gray-400" : isValidPassword(formData.password) ? "text-green-500" : "text-red-500"}`}
|
||||
>
|
||||
Mật khẩu phải chứa tối thiểu 8 ký tự, 1 chữ cái in hoa, 1
|
||||
chữ số và 1 ký tự đặc biệt.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<div
|
||||
className={
|
||||
formData.password.length > 0 &&
|
||||
!isValidPassword(formData.password)
|
||||
? "opacity-50 cursor-not-allowed"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
<Checkbox
|
||||
className="w-5 h-5"
|
||||
checked={isChecked}
|
||||
onChange={(val) => {
|
||||
if (isValidPassword(formData.password))
|
||||
setIsChecked(val);
|
||||
}}
|
||||
disabled={!isValidPassword(formData.password)}
|
||||
/>
|
||||
</div>
|
||||
<p className="inline-block font-normal text-gray-500 dark:text-gray-400">
|
||||
By creating an account means you agree to the{" "}
|
||||
<span className="text-gray-800 dark:text-white/90">
|
||||
Terms and Conditions,
|
||||
</span>{" "}
|
||||
and our{" "}
|
||||
<span className="text-gray-800 dark:text-white">
|
||||
Privacy Policy
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={
|
||||
!isChecked ||
|
||||
loading ||
|
||||
!isValidPassword(formData.password)
|
||||
}
|
||||
className={`flex items-center justify-center w-full px-4 py-3 text-sm font-medium text-white transition rounded-lg bg-brand-500 shadow-theme-xs
|
||||
${!isChecked || !isValidPassword(formData.password) ? "opacity-50 cursor-not-allowed" : "hover:bg-brand-600"}`}
|
||||
>
|
||||
{loading ? "Processing..." : "Sign Up"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className="mt-5">
|
||||
<p className="text-sm font-normal text-center text-gray-700 dark:text-gray-400 sm:text-start">
|
||||
Already have an account?{" "}
|
||||
<Link
|
||||
href="/signin"
|
||||
className="text-brand-500 hover:text-brand-600 dark:text-brand-400"
|
||||
>
|
||||
Sign In
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{step === 2 && (
|
||||
<form onSubmit={handleVerifyOtpSubmit}>
|
||||
<div className="space-y-5">
|
||||
<div>
|
||||
<Label>
|
||||
OTP Code<span className="text-error-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
type="text"
|
||||
name="otp"
|
||||
defaultValue={otp}
|
||||
onChange={(e) => setOtp(e.target.value)}
|
||||
placeholder="Enter the 6-digit code"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setStep(1)}
|
||||
className="flex items-center justify-center w-1/3 px-4 py-3 text-sm font-medium text-gray-700 transition rounded-lg bg-gray-200 hover:bg-gray-300"
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="flex items-center justify-center w-2/3 px-4 py-3 text-sm font-medium text-white transition rounded-lg bg-brand-500 shadow-theme-xs hover:bg-brand-600"
|
||||
>
|
||||
{loading ? "Verifying..." : "Verify & Register"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
283
src/components/calendar/Calendar.tsx
Normal file
283
src/components/calendar/Calendar.tsx
Normal file
@@ -0,0 +1,283 @@
|
||||
"use client";
|
||||
import React, { useState, useRef, useEffect } from "react";
|
||||
import FullCalendar from "@fullcalendar/react";
|
||||
import dayGridPlugin from "@fullcalendar/daygrid";
|
||||
import timeGridPlugin from "@fullcalendar/timegrid";
|
||||
import interactionPlugin from "@fullcalendar/interaction";
|
||||
import {
|
||||
EventInput,
|
||||
DateSelectArg,
|
||||
EventClickArg,
|
||||
EventContentArg,
|
||||
} from "@fullcalendar/core";
|
||||
import { useModal } from "@/hooks/useModal";
|
||||
import { Modal } from "@/components/ui/modal";
|
||||
|
||||
interface CalendarEvent extends EventInput {
|
||||
extendedProps: {
|
||||
calendar: string;
|
||||
};
|
||||
}
|
||||
|
||||
const Calendar: React.FC = () => {
|
||||
const [selectedEvent, setSelectedEvent] = useState<CalendarEvent | null>(
|
||||
null
|
||||
);
|
||||
const [eventTitle, setEventTitle] = useState("");
|
||||
const [eventStartDate, setEventStartDate] = useState("");
|
||||
const [eventEndDate, setEventEndDate] = useState("");
|
||||
const [eventLevel, setEventLevel] = useState("");
|
||||
const [events, setEvents] = useState<CalendarEvent[]>([]);
|
||||
const calendarRef = useRef<FullCalendar>(null);
|
||||
const { isOpen, openModal, closeModal } = useModal();
|
||||
|
||||
const calendarsEvents = {
|
||||
Danger: "danger",
|
||||
Success: "success",
|
||||
Primary: "primary",
|
||||
Warning: "warning",
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Initialize with some events
|
||||
setEvents([
|
||||
{
|
||||
id: "1",
|
||||
title: "Event Conf.",
|
||||
start: new Date().toISOString().split("T")[0],
|
||||
extendedProps: { calendar: "Danger" },
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
title: "Meeting",
|
||||
start: new Date(Date.now() + 86400000).toISOString().split("T")[0],
|
||||
extendedProps: { calendar: "Success" },
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
title: "Workshop",
|
||||
start: new Date(Date.now() + 172800000).toISOString().split("T")[0],
|
||||
end: new Date(Date.now() + 259200000).toISOString().split("T")[0],
|
||||
extendedProps: { calendar: "Primary" },
|
||||
},
|
||||
]);
|
||||
}, []);
|
||||
|
||||
const handleDateSelect = (selectInfo: DateSelectArg) => {
|
||||
resetModalFields();
|
||||
setEventStartDate(selectInfo.startStr);
|
||||
setEventEndDate(selectInfo.endStr || selectInfo.startStr);
|
||||
openModal();
|
||||
};
|
||||
|
||||
const handleEventClick = (clickInfo: EventClickArg) => {
|
||||
const event = clickInfo.event;
|
||||
setSelectedEvent(event as unknown as CalendarEvent);
|
||||
setEventTitle(event.title);
|
||||
setEventStartDate(event.start?.toISOString().split("T")[0] || "");
|
||||
setEventEndDate(event.end?.toISOString().split("T")[0] || "");
|
||||
setEventLevel(event.extendedProps.calendar);
|
||||
openModal();
|
||||
};
|
||||
|
||||
const handleAddOrUpdateEvent = () => {
|
||||
if (selectedEvent) {
|
||||
// Update existing event
|
||||
setEvents((prevEvents) =>
|
||||
prevEvents.map((event) =>
|
||||
event.id === selectedEvent.id
|
||||
? {
|
||||
...event,
|
||||
title: eventTitle,
|
||||
start: eventStartDate,
|
||||
end: eventEndDate,
|
||||
extendedProps: { calendar: eventLevel },
|
||||
}
|
||||
: event
|
||||
)
|
||||
);
|
||||
} else {
|
||||
// Add new event
|
||||
const newEvent: CalendarEvent = {
|
||||
id: Date.now().toString(),
|
||||
title: eventTitle,
|
||||
start: eventStartDate,
|
||||
end: eventEndDate,
|
||||
allDay: true,
|
||||
extendedProps: { calendar: eventLevel },
|
||||
};
|
||||
setEvents((prevEvents) => [...prevEvents, newEvent]);
|
||||
}
|
||||
closeModal();
|
||||
resetModalFields();
|
||||
};
|
||||
|
||||
const resetModalFields = () => {
|
||||
setEventTitle("");
|
||||
setEventStartDate("");
|
||||
setEventEndDate("");
|
||||
setEventLevel("");
|
||||
setSelectedEvent(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="rounded-2xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03]">
|
||||
<div className="custom-calendar">
|
||||
<FullCalendar
|
||||
ref={calendarRef}
|
||||
plugins={[dayGridPlugin, timeGridPlugin, interactionPlugin]}
|
||||
initialView="dayGridMonth"
|
||||
headerToolbar={{
|
||||
left: "prev,next addEventButton",
|
||||
center: "title",
|
||||
right: "dayGridMonth,timeGridWeek,timeGridDay",
|
||||
}}
|
||||
events={events}
|
||||
selectable={true}
|
||||
select={handleDateSelect}
|
||||
eventClick={handleEventClick}
|
||||
eventContent={renderEventContent}
|
||||
customButtons={{
|
||||
addEventButton: {
|
||||
text: "Add Event +",
|
||||
click: openModal,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={closeModal}
|
||||
className="max-w-[700px] p-6 lg:p-10"
|
||||
>
|
||||
<div className="flex flex-col px-2 overflow-y-auto custom-scrollbar">
|
||||
<div>
|
||||
<h5 className="mb-2 font-semibold text-gray-800 modal-title text-theme-xl dark:text-white/90 lg:text-2xl">
|
||||
{selectedEvent ? "Edit Event" : "Add Event"}
|
||||
</h5>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Plan your next big moment: schedule or edit an event to stay on
|
||||
track
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-8">
|
||||
<div>
|
||||
<div>
|
||||
<label className="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
|
||||
Event Title
|
||||
</label>
|
||||
<input
|
||||
id="event-title"
|
||||
type="text"
|
||||
value={eventTitle}
|
||||
onChange={(e) => setEventTitle(e.target.value)}
|
||||
className="dark:bg-dark-900 h-11 w-full rounded-lg border border-gray-300 bg-transparent px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<label className="block mb-4 text-sm font-medium text-gray-700 dark:text-gray-400">
|
||||
Event Color
|
||||
</label>
|
||||
<div className="flex flex-wrap items-center gap-4 sm:gap-5">
|
||||
{Object.entries(calendarsEvents).map(([key, value]) => (
|
||||
<div key={key} className="n-chk">
|
||||
<div
|
||||
className={`form-check form-check-${value} form-check-inline`}
|
||||
>
|
||||
<label
|
||||
className="flex items-center text-sm text-gray-700 form-check-label dark:text-gray-400"
|
||||
htmlFor={`modal${key}`}
|
||||
>
|
||||
<span className="relative">
|
||||
<input
|
||||
className="sr-only form-check-input"
|
||||
type="radio"
|
||||
name="event-level"
|
||||
value={key}
|
||||
id={`modal${key}`}
|
||||
checked={eventLevel === key}
|
||||
onChange={() => setEventLevel(key)}
|
||||
/>
|
||||
<span className="flex items-center justify-center w-5 h-5 mr-2 border border-gray-300 rounded-full box dark:border-gray-700">
|
||||
<span
|
||||
className={`h-2 w-2 rounded-full bg-white ${
|
||||
eventLevel === key ? "block" : "hidden"
|
||||
}`}
|
||||
></span>
|
||||
</span>
|
||||
</span>
|
||||
{key}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<label className="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
|
||||
Enter Start Date
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
id="event-start-date"
|
||||
type="date"
|
||||
value={eventStartDate}
|
||||
onChange={(e) => setEventStartDate(e.target.value)}
|
||||
className="dark:bg-dark-900 h-11 w-full appearance-none rounded-lg border border-gray-300 bg-transparent bg-none px-4 py-2.5 pl-4 pr-11 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<label className="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
|
||||
Enter End Date
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
id="event-end-date"
|
||||
type="date"
|
||||
value={eventEndDate}
|
||||
onChange={(e) => setEventEndDate(e.target.value)}
|
||||
className="dark:bg-dark-900 h-11 w-full appearance-none rounded-lg border border-gray-300 bg-transparent bg-none px-4 py-2.5 pl-4 pr-11 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 mt-6 modal-footer sm:justify-end">
|
||||
<button
|
||||
onClick={closeModal}
|
||||
type="button"
|
||||
className="flex w-full justify-center rounded-lg border border-gray-300 bg-white px-4 py-2.5 text-sm font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03] sm:w-auto"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
<button
|
||||
onClick={handleAddOrUpdateEvent}
|
||||
type="button"
|
||||
className="btn btn-success btn-update-event flex w-full justify-center rounded-lg bg-brand-500 px-4 py-2.5 text-sm font-medium text-white hover:bg-brand-600 sm:w-auto"
|
||||
>
|
||||
{selectedEvent ? "Update Changes" : "Add Event"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderEventContent = (eventInfo: EventContentArg) => {
|
||||
const colorClass = `fc-bg-${eventInfo.event.extendedProps.calendar.toLowerCase()}`;
|
||||
return (
|
||||
<div
|
||||
className={`event-fc-color flex fc-event-main ${colorClass} p-1 rounded-sm`}
|
||||
>
|
||||
<div className="fc-daygrid-event-dot"></div>
|
||||
<div className="fc-event-time">{eventInfo.timeText}</div>
|
||||
<div className="fc-event-title">{eventInfo.event.title}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Calendar;
|
||||
110
src/components/charts/bar/BarChartOne.tsx
Normal file
110
src/components/charts/bar/BarChartOne.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
|
||||
import { ApexOptions } from "apexcharts";
|
||||
|
||||
import dynamic from "next/dynamic";
|
||||
// Dynamically import the ReactApexChart component
|
||||
const ReactApexChart = dynamic(() => import("react-apexcharts"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
export default function BarChartOne() {
|
||||
const options: ApexOptions = {
|
||||
colors: ["#465fff"],
|
||||
chart: {
|
||||
fontFamily: "Outfit, sans-serif",
|
||||
type: "bar",
|
||||
height: 180,
|
||||
toolbar: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
plotOptions: {
|
||||
bar: {
|
||||
horizontal: false,
|
||||
columnWidth: "39%",
|
||||
borderRadius: 5,
|
||||
borderRadiusApplication: "end",
|
||||
},
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false,
|
||||
},
|
||||
stroke: {
|
||||
show: true,
|
||||
width: 4,
|
||||
colors: ["transparent"],
|
||||
},
|
||||
xaxis: {
|
||||
categories: [
|
||||
"Jan",
|
||||
"Feb",
|
||||
"Mar",
|
||||
"Apr",
|
||||
"May",
|
||||
"Jun",
|
||||
"Jul",
|
||||
"Aug",
|
||||
"Sep",
|
||||
"Oct",
|
||||
"Nov",
|
||||
"Dec",
|
||||
],
|
||||
axisBorder: {
|
||||
show: false,
|
||||
},
|
||||
axisTicks: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
show: true,
|
||||
position: "top",
|
||||
horizontalAlign: "left",
|
||||
fontFamily: "Outfit",
|
||||
},
|
||||
yaxis: {
|
||||
title: {
|
||||
text: undefined,
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
yaxis: {
|
||||
lines: {
|
||||
show: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
fill: {
|
||||
opacity: 1,
|
||||
},
|
||||
|
||||
tooltip: {
|
||||
x: {
|
||||
show: false,
|
||||
},
|
||||
y: {
|
||||
formatter: (val: number) => `${val}`,
|
||||
},
|
||||
},
|
||||
};
|
||||
const series = [
|
||||
{
|
||||
name: "Sales",
|
||||
data: [168, 385, 201, 298, 187, 195, 291, 110, 215, 390, 280, 112],
|
||||
},
|
||||
];
|
||||
return (
|
||||
<div className="max-w-full overflow-x-auto custom-scrollbar">
|
||||
<div id="chartOne" className="min-w-[1000px]">
|
||||
<ReactApexChart
|
||||
options={options}
|
||||
series={series}
|
||||
type="bar"
|
||||
height={180}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
133
src/components/charts/line/LineChartOne.tsx
Normal file
133
src/components/charts/line/LineChartOne.tsx
Normal file
@@ -0,0 +1,133 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
|
||||
import { ApexOptions } from "apexcharts";
|
||||
|
||||
import dynamic from "next/dynamic";
|
||||
// Dynamically import the ReactApexChart component
|
||||
const ReactApexChart = dynamic(() => import("react-apexcharts"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
export default function LineChartOne() {
|
||||
const options: ApexOptions = {
|
||||
legend: {
|
||||
show: false, // Hide legend
|
||||
position: "top",
|
||||
horizontalAlign: "left",
|
||||
},
|
||||
colors: ["#465FFF", "#9CB9FF"], // Define line colors
|
||||
chart: {
|
||||
fontFamily: "Outfit, sans-serif",
|
||||
height: 310,
|
||||
type: "line", // Set the chart type to 'line'
|
||||
toolbar: {
|
||||
show: false, // Hide chart toolbar
|
||||
},
|
||||
},
|
||||
stroke: {
|
||||
curve: "straight", // Define the line style (straight, smooth, or step)
|
||||
width: [2, 2], // Line width for each dataset
|
||||
},
|
||||
|
||||
fill: {
|
||||
type: "gradient",
|
||||
gradient: {
|
||||
opacityFrom: 0.55,
|
||||
opacityTo: 0,
|
||||
},
|
||||
},
|
||||
markers: {
|
||||
size: 0, // Size of the marker points
|
||||
strokeColors: "#fff", // Marker border color
|
||||
strokeWidth: 2,
|
||||
hover: {
|
||||
size: 6, // Marker size on hover
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
xaxis: {
|
||||
lines: {
|
||||
show: false, // Hide grid lines on x-axis
|
||||
},
|
||||
},
|
||||
yaxis: {
|
||||
lines: {
|
||||
show: true, // Show grid lines on y-axis
|
||||
},
|
||||
},
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false, // Disable data labels
|
||||
},
|
||||
tooltip: {
|
||||
enabled: true, // Enable tooltip
|
||||
x: {
|
||||
format: "dd MMM yyyy", // Format for x-axis tooltip
|
||||
},
|
||||
},
|
||||
xaxis: {
|
||||
type: "category", // Category-based x-axis
|
||||
categories: [
|
||||
"Jan",
|
||||
"Feb",
|
||||
"Mar",
|
||||
"Apr",
|
||||
"May",
|
||||
"Jun",
|
||||
"Jul",
|
||||
"Aug",
|
||||
"Sep",
|
||||
"Oct",
|
||||
"Nov",
|
||||
"Dec",
|
||||
],
|
||||
axisBorder: {
|
||||
show: false, // Hide x-axis border
|
||||
},
|
||||
axisTicks: {
|
||||
show: false, // Hide x-axis ticks
|
||||
},
|
||||
tooltip: {
|
||||
enabled: false, // Disable tooltip for x-axis points
|
||||
},
|
||||
},
|
||||
yaxis: {
|
||||
labels: {
|
||||
style: {
|
||||
fontSize: "12px", // Adjust font size for y-axis labels
|
||||
colors: ["#6B7280"], // Color of the labels
|
||||
},
|
||||
},
|
||||
title: {
|
||||
text: "", // Remove y-axis title
|
||||
style: {
|
||||
fontSize: "0px",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const series = [
|
||||
{
|
||||
name: "Sales",
|
||||
data: [180, 190, 170, 160, 175, 165, 170, 205, 230, 210, 240, 235],
|
||||
},
|
||||
{
|
||||
name: "Revenue",
|
||||
data: [40, 30, 50, 40, 55, 40, 70, 100, 110, 120, 150, 140],
|
||||
},
|
||||
];
|
||||
return (
|
||||
<div className="max-w-full overflow-x-auto custom-scrollbar">
|
||||
<div id="chartEight" className="min-w-[1000px]">
|
||||
<ReactApexChart
|
||||
options={options}
|
||||
series={series}
|
||||
type="area"
|
||||
height={310}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
45
src/components/common/ChartTab.tsx
Normal file
45
src/components/common/ChartTab.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import React, { useState } from "react";
|
||||
|
||||
const ChartTab: React.FC = () => {
|
||||
const [selected, setSelected] = useState<
|
||||
"optionOne" | "optionTwo" | "optionThree"
|
||||
>("optionOne");
|
||||
|
||||
const getButtonClass = (option: "optionOne" | "optionTwo" | "optionThree") =>
|
||||
selected === option
|
||||
? "shadow-theme-xs text-gray-900 dark:text-white bg-white dark:bg-gray-800"
|
||||
: "text-gray-500 dark:text-gray-400";
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-0.5 rounded-lg bg-gray-100 p-0.5 dark:bg-gray-900">
|
||||
<button
|
||||
onClick={() => setSelected("optionOne")}
|
||||
className={`px-3 py-2 font-medium w-full rounded-md text-theme-sm hover:text-gray-900 dark:hover:text-white ${getButtonClass(
|
||||
"optionOne"
|
||||
)}`}
|
||||
>
|
||||
Monthly
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setSelected("optionTwo")}
|
||||
className={`px-3 py-2 font-medium w-full rounded-md text-theme-sm hover:text-gray-900 dark:hover:text-white ${getButtonClass(
|
||||
"optionTwo"
|
||||
)}`}
|
||||
>
|
||||
Quarterly
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setSelected("optionThree")}
|
||||
className={`px-3 py-2 font-medium w-full rounded-md text-theme-sm hover:text-gray-900 dark:hover:text-white ${getButtonClass(
|
||||
"optionThree"
|
||||
)}`}
|
||||
>
|
||||
Annually
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChartTab;
|
||||
43
src/components/common/ComponentCard.tsx
Normal file
43
src/components/common/ComponentCard.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import React from "react";
|
||||
|
||||
interface ComponentCardProps {
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
className?: string; // Additional custom classes for styling
|
||||
desc?: string; // Description text
|
||||
headerAction?: React.ReactNode;
|
||||
}
|
||||
|
||||
const ComponentCard: React.FC<ComponentCardProps> = ({
|
||||
title,
|
||||
children,
|
||||
className = "",
|
||||
desc = "",
|
||||
headerAction,
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={`rounded-2xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03] ${className}`}
|
||||
>
|
||||
{/* Card Header */}
|
||||
<div className="px-6 py-5 flex items-center justify-between">
|
||||
<h3 className="text-base font-medium text-gray-800 dark:text-white/90">
|
||||
{title}
|
||||
</h3>
|
||||
{desc && (
|
||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
{desc}
|
||||
</p>
|
||||
)}
|
||||
{headerAction && <div>{headerAction}</div>}
|
||||
</div>
|
||||
|
||||
{/* Card Body */}
|
||||
<div className="p-4 border-t border-gray-100 dark:border-gray-800 sm:p-6">
|
||||
<div className="space-y-6">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ComponentCard;
|
||||
25
src/components/common/GridShape.tsx
Normal file
25
src/components/common/GridShape.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import Image from "next/image";
|
||||
import React from "react";
|
||||
|
||||
export default function GridShape() {
|
||||
return (
|
||||
<>
|
||||
<div className="absolute right-0 top-0 -z-1 w-full max-w-[250px] xl:max-w-[450px]">
|
||||
<Image
|
||||
width={540}
|
||||
height={254}
|
||||
src="/images/shape/grid-01.svg"
|
||||
alt="grid"
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute bottom-0 left-0 -z-1 w-full max-w-[250px] rotate-180 xl:max-w-[450px]">
|
||||
<Image
|
||||
width={540}
|
||||
height={254}
|
||||
src="/images/shape/grid-01.svg"
|
||||
alt="grid"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
52
src/components/common/PageBreadCrumb.tsx
Normal file
52
src/components/common/PageBreadCrumb.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import Link from "next/link";
|
||||
import React from "react";
|
||||
|
||||
interface BreadcrumbProps {
|
||||
pageTitle: string;
|
||||
}
|
||||
|
||||
const PageBreadcrumb: React.FC<BreadcrumbProps> = ({ pageTitle }) => {
|
||||
return (
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 mb-6">
|
||||
<h2
|
||||
className="text-xl font-semibold text-gray-800 dark:text-white/90"
|
||||
x-text="pageName"
|
||||
>
|
||||
{pageTitle}
|
||||
</h2>
|
||||
<nav>
|
||||
<ol className="flex items-center gap-1.5">
|
||||
<li>
|
||||
<Link
|
||||
className="inline-flex items-center gap-1.5 text-sm text-gray-500 dark:text-gray-400"
|
||||
href="/"
|
||||
>
|
||||
Home
|
||||
<svg
|
||||
className="stroke-current"
|
||||
width="17"
|
||||
height="16"
|
||||
viewBox="0 0 17 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6.0765 12.667L10.2432 8.50033L6.0765 4.33366"
|
||||
stroke=""
|
||||
strokeWidth="1.2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</Link>
|
||||
</li>
|
||||
<li className="text-sm text-gray-800 dark:text-white/90">
|
||||
{pageTitle}
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PageBreadcrumb;
|
||||
42
src/components/common/ThemeToggleButton.tsx
Normal file
42
src/components/common/ThemeToggleButton.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import React from "react";
|
||||
import { useTheme } from "../../context/ThemeContext";
|
||||
|
||||
export const ThemeToggleButton: React.FC = () => {
|
||||
const { toggleTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={toggleTheme}
|
||||
className="relative flex items-center justify-center text-gray-500 transition-colors bg-white border border-gray-200 rounded-full hover:text-dark-900 h-11 w-11 hover:bg-gray-100 hover:text-gray-700 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-white"
|
||||
>
|
||||
<svg
|
||||
className="hidden dark:block"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M9.99998 1.5415C10.4142 1.5415 10.75 1.87729 10.75 2.2915V3.5415C10.75 3.95572 10.4142 4.2915 9.99998 4.2915C9.58577 4.2915 9.24998 3.95572 9.24998 3.5415V2.2915C9.24998 1.87729 9.58577 1.5415 9.99998 1.5415ZM10.0009 6.79327C8.22978 6.79327 6.79402 8.22904 6.79402 10.0001C6.79402 11.7712 8.22978 13.207 10.0009 13.207C11.772 13.207 13.2078 11.7712 13.2078 10.0001C13.2078 8.22904 11.772 6.79327 10.0009 6.79327ZM5.29402 10.0001C5.29402 7.40061 7.40135 5.29327 10.0009 5.29327C12.6004 5.29327 14.7078 7.40061 14.7078 10.0001C14.7078 12.5997 12.6004 14.707 10.0009 14.707C7.40135 14.707 5.29402 12.5997 5.29402 10.0001ZM15.9813 5.08035C16.2742 4.78746 16.2742 4.31258 15.9813 4.01969C15.6884 3.7268 15.2135 3.7268 14.9207 4.01969L14.0368 4.90357C13.7439 5.19647 13.7439 5.67134 14.0368 5.96423C14.3297 6.25713 14.8045 6.25713 15.0974 5.96423L15.9813 5.08035ZM18.4577 10.0001C18.4577 10.4143 18.1219 10.7501 17.7077 10.7501H16.4577C16.0435 10.7501 15.7077 10.4143 15.7077 10.0001C15.7077 9.58592 16.0435 9.25013 16.4577 9.25013H17.7077C18.1219 9.25013 18.4577 9.58592 18.4577 10.0001ZM14.9207 15.9806C15.2135 16.2735 15.6884 16.2735 15.9813 15.9806C16.2742 15.6877 16.2742 15.2128 15.9813 14.9199L15.0974 14.036C14.8045 13.7431 14.3297 13.7431 14.0368 14.036C13.7439 14.3289 13.7439 14.8038 14.0368 15.0967L14.9207 15.9806ZM9.99998 15.7088C10.4142 15.7088 10.75 16.0445 10.75 16.4588V17.7088C10.75 18.123 10.4142 18.4588 9.99998 18.4588C9.58577 18.4588 9.24998 18.123 9.24998 17.7088V16.4588C9.24998 16.0445 9.58577 15.7088 9.99998 15.7088ZM5.96356 15.0972C6.25646 14.8043 6.25646 14.3295 5.96356 14.0366C5.67067 13.7437 5.1958 13.7437 4.9029 14.0366L4.01902 14.9204C3.72613 15.2133 3.72613 15.6882 4.01902 15.9811C4.31191 16.274 4.78679 16.274 5.07968 15.9811L5.96356 15.0972ZM4.29224 10.0001C4.29224 10.4143 3.95645 10.7501 3.54224 10.7501H2.29224C1.87802 10.7501 1.54224 10.4143 1.54224 10.0001C1.54224 9.58592 1.87802 9.25013 2.29224 9.25013H3.54224C3.95645 9.25013 4.29224 9.58592 4.29224 10.0001ZM4.9029 5.9637C5.1958 6.25659 5.67067 6.25659 5.96356 5.9637C6.25646 5.6708 6.25646 5.19593 5.96356 4.90303L5.07968 4.01915C4.78679 3.72626 4.31191 3.72626 4.01902 4.01915C3.72613 4.31204 3.72613 4.78692 4.01902 5.07981L4.9029 5.9637Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
className="dark:hidden"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M17.4547 11.97L18.1799 12.1611C18.265 11.8383 18.1265 11.4982 17.8401 11.3266C17.5538 11.1551 17.1885 11.1934 16.944 11.4207L17.4547 11.97ZM8.0306 2.5459L8.57989 3.05657C8.80718 2.81209 8.84554 2.44682 8.67398 2.16046C8.50243 1.8741 8.16227 1.73559 7.83948 1.82066L8.0306 2.5459ZM12.9154 13.0035C9.64678 13.0035 6.99707 10.3538 6.99707 7.08524H5.49707C5.49707 11.1823 8.81835 14.5035 12.9154 14.5035V13.0035ZM16.944 11.4207C15.8869 12.4035 14.4721 13.0035 12.9154 13.0035V14.5035C14.8657 14.5035 16.6418 13.7499 17.9654 12.5193L16.944 11.4207ZM16.7295 11.7789C15.9437 14.7607 13.2277 16.9586 10.0003 16.9586V18.4586C13.9257 18.4586 17.2249 15.7853 18.1799 12.1611L16.7295 11.7789ZM10.0003 16.9586C6.15734 16.9586 3.04199 13.8433 3.04199 10.0003H1.54199C1.54199 14.6717 5.32892 18.4586 10.0003 18.4586V16.9586ZM3.04199 10.0003C3.04199 6.77289 5.23988 4.05695 8.22173 3.27114L7.83948 1.82066C4.21532 2.77574 1.54199 6.07486 1.54199 10.0003H3.04199ZM6.99707 7.08524C6.99707 5.52854 7.5971 4.11366 8.57989 3.05657L7.48132 2.03522C6.25073 3.35885 5.49707 5.13487 5.49707 7.08524H6.99707Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
42
src/components/common/ThemeTogglerTwo.tsx
Normal file
42
src/components/common/ThemeTogglerTwo.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
"use client";
|
||||
import { useTheme } from "@/context/ThemeContext";
|
||||
import React from "react";
|
||||
|
||||
export default function ThemeTogglerTwo() {
|
||||
const { toggleTheme } = useTheme();
|
||||
return (
|
||||
<button
|
||||
onClick={toggleTheme}
|
||||
className="inline-flex size-14 items-center justify-center rounded-full bg-brand-500 text-white transition-colors hover:bg-brand-600"
|
||||
>
|
||||
<svg
|
||||
className="hidden dark:block"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M9.99998 1.5415C10.4142 1.5415 10.75 1.87729 10.75 2.2915V3.5415C10.75 3.95572 10.4142 4.2915 9.99998 4.2915C9.58577 4.2915 9.24998 3.95572 9.24998 3.5415V2.2915C9.24998 1.87729 9.58577 1.5415 9.99998 1.5415ZM10.0009 6.79327C8.22978 6.79327 6.79402 8.22904 6.79402 10.0001C6.79402 11.7712 8.22978 13.207 10.0009 13.207C11.772 13.207 13.2078 11.7712 13.2078 10.0001C13.2078 8.22904 11.772 6.79327 10.0009 6.79327ZM5.29402 10.0001C5.29402 7.40061 7.40135 5.29327 10.0009 5.29327C12.6004 5.29327 14.7078 7.40061 14.7078 10.0001C14.7078 12.5997 12.6004 14.707 10.0009 14.707C7.40135 14.707 5.29402 12.5997 5.29402 10.0001ZM15.9813 5.08035C16.2742 4.78746 16.2742 4.31258 15.9813 4.01969C15.6884 3.7268 15.2135 3.7268 14.9207 4.01969L14.0368 4.90357C13.7439 5.19647 13.7439 5.67134 14.0368 5.96423C14.3297 6.25713 14.8045 6.25713 15.0974 5.96423L15.9813 5.08035ZM18.4577 10.0001C18.4577 10.4143 18.1219 10.7501 17.7077 10.7501H16.4577C16.0435 10.7501 15.7077 10.4143 15.7077 10.0001C15.7077 9.58592 16.0435 9.25013 16.4577 9.25013H17.7077C18.1219 9.25013 18.4577 9.58592 18.4577 10.0001ZM14.9207 15.9806C15.2135 16.2735 15.6884 16.2735 15.9813 15.9806C16.2742 15.6877 16.2742 15.2128 15.9813 14.9199L15.0974 14.036C14.8045 13.7431 14.3297 13.7431 14.0368 14.036C13.7439 14.3289 13.7439 14.8038 14.0368 15.0967L14.9207 15.9806ZM9.99998 15.7088C10.4142 15.7088 10.75 16.0445 10.75 16.4588V17.7088C10.75 18.123 10.4142 18.4588 9.99998 18.4588C9.58577 18.4588 9.24998 18.123 9.24998 17.7088V16.4588C9.24998 16.0445 9.58577 15.7088 9.99998 15.7088ZM5.96356 15.0972C6.25646 14.8043 6.25646 14.3295 5.96356 14.0366C5.67067 13.7437 5.1958 13.7437 4.9029 14.0366L4.01902 14.9204C3.72613 15.2133 3.72613 15.6882 4.01902 15.9811C4.31191 16.274 4.78679 16.274 5.07968 15.9811L5.96356 15.0972ZM4.29224 10.0001C4.29224 10.4143 3.95645 10.7501 3.54224 10.7501H2.29224C1.87802 10.7501 1.54224 10.4143 1.54224 10.0001C1.54224 9.58592 1.87802 9.25013 2.29224 9.25013H3.54224C3.95645 9.25013 4.29224 9.58592 4.29224 10.0001ZM4.9029 5.9637C5.1958 6.25659 5.67067 6.25659 5.96356 5.9637C6.25646 5.6708 6.25646 5.19593 5.96356 4.90303L5.07968 4.01915C4.78679 3.72626 4.31191 3.72626 4.01902 4.01915C3.72613 4.31204 3.72613 4.78692 4.01902 5.07981L4.9029 5.9637Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
className="dark:hidden"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M17.4547 11.97L18.1799 12.1611C18.265 11.8383 18.1265 11.4982 17.8401 11.3266C17.5538 11.1551 17.1885 11.1934 16.944 11.4207L17.4547 11.97ZM8.0306 2.5459L8.57989 3.05657C8.80718 2.81209 8.84554 2.44682 8.67398 2.16046C8.50243 1.8741 8.16227 1.73559 7.83948 1.82066L8.0306 2.5459ZM12.9154 13.0035C9.64678 13.0035 6.99707 10.3538 6.99707 7.08524H5.49707C5.49707 11.1823 8.81835 14.5035 12.9154 14.5035V13.0035ZM16.944 11.4207C15.8869 12.4035 14.4721 13.0035 12.9154 13.0035V14.5035C14.8657 14.5035 16.6418 13.7499 17.9654 12.5193L16.944 11.4207ZM16.7295 11.7789C15.9437 14.7607 13.2277 16.9586 10.0003 16.9586V18.4586C13.9257 18.4586 17.2249 15.7853 18.1799 12.1611L16.7295 11.7789ZM10.0003 16.9586C6.15734 16.9586 3.04199 13.8433 3.04199 10.0003H1.54199C1.54199 14.6717 5.32892 18.4586 10.0003 18.4586V16.9586ZM3.04199 10.0003C3.04199 6.77289 5.23988 4.05695 8.22173 3.27114L7.83948 1.82066C4.21532 2.77574 1.54199 6.07486 1.54199 10.0003H3.04199ZM6.99707 7.08524C6.99707 5.52854 7.5971 4.11366 8.57989 3.05657L7.48132 2.03522C6.25073 3.35885 5.49707 5.13487 5.49707 7.08524H6.99707Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
123
src/components/ecommerce/CountryMap.tsx
Normal file
123
src/components/ecommerce/CountryMap.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
import React from "react";
|
||||
// import { VectorMap } from "@react-jvectormap/core";
|
||||
import { worldMill } from "@react-jvectormap/world";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
const VectorMap = dynamic(
|
||||
() => import("@react-jvectormap/core").then((mod) => mod.VectorMap),
|
||||
{ ssr: false }
|
||||
);
|
||||
|
||||
// Define the component props
|
||||
interface CountryMapProps {
|
||||
mapColor?: string;
|
||||
}
|
||||
|
||||
type MarkerStyle = {
|
||||
initial: {
|
||||
fill: string;
|
||||
r: number; // Radius for markers
|
||||
};
|
||||
};
|
||||
|
||||
type Marker = {
|
||||
latLng: [number, number];
|
||||
name: string;
|
||||
style?: {
|
||||
fill: string;
|
||||
borderWidth: number;
|
||||
borderColor: string;
|
||||
stroke?: string;
|
||||
strokeOpacity?: number;
|
||||
};
|
||||
};
|
||||
|
||||
const CountryMap: React.FC<CountryMapProps> = ({ mapColor }) => {
|
||||
return (
|
||||
<VectorMap
|
||||
map={worldMill}
|
||||
backgroundColor="transparent"
|
||||
markerStyle={
|
||||
{
|
||||
initial: {
|
||||
fill: "#465FFF",
|
||||
r: 4, // Custom radius for markers
|
||||
}, // Type assertion to bypass strict CSS property checks
|
||||
} as MarkerStyle
|
||||
}
|
||||
markersSelectable={true}
|
||||
markers={
|
||||
[
|
||||
{
|
||||
latLng: [37.2580397, -104.657039],
|
||||
name: "United States",
|
||||
style: {
|
||||
fill: "#465FFF",
|
||||
borderWidth: 1,
|
||||
borderColor: "white",
|
||||
stroke: "#383f47",
|
||||
},
|
||||
},
|
||||
{
|
||||
latLng: [20.7504374, 73.7276105],
|
||||
name: "India",
|
||||
style: { fill: "#465FFF", borderWidth: 1, borderColor: "white" },
|
||||
},
|
||||
{
|
||||
latLng: [53.613, -11.6368],
|
||||
name: "United Kingdom",
|
||||
style: { fill: "#465FFF", borderWidth: 1, borderColor: "white" },
|
||||
},
|
||||
{
|
||||
latLng: [-25.0304388, 115.2092761],
|
||||
name: "Sweden",
|
||||
style: {
|
||||
fill: "#465FFF",
|
||||
borderWidth: 1,
|
||||
borderColor: "white",
|
||||
strokeOpacity: 0,
|
||||
},
|
||||
},
|
||||
] as Marker[]
|
||||
}
|
||||
zoomOnScroll={false}
|
||||
zoomMax={12}
|
||||
zoomMin={1}
|
||||
zoomAnimate={true}
|
||||
zoomStep={1.5}
|
||||
regionStyle={{
|
||||
initial: {
|
||||
fill: mapColor || "#D0D5DD",
|
||||
fillOpacity: 1,
|
||||
fontFamily: "Outfit",
|
||||
stroke: "none",
|
||||
strokeWidth: 0,
|
||||
strokeOpacity: 0,
|
||||
},
|
||||
hover: {
|
||||
fillOpacity: 0.7,
|
||||
cursor: "pointer",
|
||||
fill: "#465fff",
|
||||
stroke: "none",
|
||||
},
|
||||
selected: {
|
||||
fill: "#465FFF",
|
||||
},
|
||||
selectedHover: {},
|
||||
}}
|
||||
regionLabelStyle={{
|
||||
initial: {
|
||||
fill: "#35373e",
|
||||
fontWeight: 500,
|
||||
fontSize: "13px",
|
||||
stroke: "none",
|
||||
},
|
||||
hover: {},
|
||||
selected: {},
|
||||
selectedHover: {},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default CountryMap;
|
||||
131
src/components/ecommerce/DemographicCard.tsx
Normal file
131
src/components/ecommerce/DemographicCard.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
"use client";
|
||||
import Image from "next/image";
|
||||
|
||||
import CountryMap from "./CountryMap";
|
||||
import { useState } from "react";
|
||||
import { MoreDotIcon } from "@/icons";
|
||||
import { Dropdown } from "../ui/dropdown/Dropdown";
|
||||
import { DropdownItem } from "../ui/dropdown/DropdownItem";
|
||||
|
||||
export default function DemographicCard() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
function toggleDropdown() {
|
||||
setIsOpen(!isOpen);
|
||||
}
|
||||
|
||||
function closeDropdown() {
|
||||
setIsOpen(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rounded-2xl border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-white/[0.03] sm:p-6">
|
||||
<div className="flex justify-between">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-800 dark:text-white/90">
|
||||
Customers Demographic
|
||||
</h3>
|
||||
<p className="mt-1 text-gray-500 text-theme-sm dark:text-gray-400">
|
||||
Number of customer based on country
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="relative inline-block">
|
||||
<button onClick={toggleDropdown} className="dropdown-toggle">
|
||||
<MoreDotIcon className="text-gray-400 hover:text-gray-700 dark:hover:text-gray-300" />
|
||||
</button>
|
||||
<Dropdown
|
||||
isOpen={isOpen}
|
||||
onClose={closeDropdown}
|
||||
className="w-40 p-2"
|
||||
>
|
||||
<DropdownItem
|
||||
onItemClick={closeDropdown}
|
||||
className="flex w-full font-normal text-left text-gray-500 rounded-lg hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300"
|
||||
>
|
||||
View More
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
onItemClick={closeDropdown}
|
||||
className="flex w-full font-normal text-left text-gray-500 rounded-lg hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300"
|
||||
>
|
||||
Delete
|
||||
</DropdownItem>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-4 py-6 my-6 overflow-hidden border border-gary-200 rounded-2xl bg-gray-50 dark:border-gray-800 dark:bg-gray-900 sm:px-6">
|
||||
<div
|
||||
id="mapOne"
|
||||
className="mapOne map-btn -mx-4 -my-6 h-[212px] w-[252px] 2xsm:w-[307px] xsm:w-[358px] sm:-mx-6 md:w-[668px] lg:w-[634px] xl:w-[393px] 2xl:w-[554px]"
|
||||
>
|
||||
<CountryMap />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-5">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="items-center w-full rounded-full max-w-8">
|
||||
<Image
|
||||
width={48}
|
||||
height={48}
|
||||
src="/images/country/country-01.svg"
|
||||
alt="usa"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-semibold text-gray-800 text-theme-sm dark:text-white/90">
|
||||
USA
|
||||
</p>
|
||||
<span className="block text-gray-500 text-theme-xs dark:text-gray-400">
|
||||
2,379 Customers
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex w-full max-w-[140px] items-center gap-3">
|
||||
<div className="relative block h-2 w-full max-w-[100px] rounded-sm bg-gray-200 dark:bg-gray-800">
|
||||
<div className="absolute left-0 top-0 flex h-full w-[79%] items-center justify-center rounded-sm bg-brand-500 text-xs font-medium text-white"></div>
|
||||
</div>
|
||||
<p className="font-medium text-gray-800 text-theme-sm dark:text-white/90">
|
||||
79%
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="items-center w-full rounded-full max-w-8">
|
||||
<Image
|
||||
width={48}
|
||||
height={48}
|
||||
className="w-full"
|
||||
src="/images/country/country-02.svg"
|
||||
alt="france"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-semibold text-gray-800 text-theme-sm dark:text-white/90">
|
||||
France
|
||||
</p>
|
||||
<span className="block text-gray-500 text-theme-xs dark:text-gray-400">
|
||||
589 Customers
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex w-full max-w-[140px] items-center gap-3">
|
||||
<div className="relative block h-2 w-full max-w-[100px] rounded-sm bg-gray-200 dark:bg-gray-800">
|
||||
<div className="absolute left-0 top-0 flex h-full w-[23%] items-center justify-center rounded-sm bg-brand-500 text-xs font-medium text-white"></div>
|
||||
</div>
|
||||
<p className="font-medium text-gray-800 text-theme-sm dark:text-white/90">
|
||||
23%
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
56
src/components/ecommerce/EcommerceMetrics.tsx
Normal file
56
src/components/ecommerce/EcommerceMetrics.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
import Badge from "../ui/badge/Badge";
|
||||
import { ArrowDownIcon, ArrowUpIcon, BoxIconLine, GroupIcon } from "@/icons";
|
||||
|
||||
export const EcommerceMetrics = () => {
|
||||
return (
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:gap-6">
|
||||
{/* <!-- Metric Item Start --> */}
|
||||
<div className="rounded-2xl border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-white/[0.03] md:p-6">
|
||||
<div className="flex items-center justify-center w-12 h-12 bg-gray-100 rounded-xl dark:bg-gray-800">
|
||||
<GroupIcon className="text-gray-800 size-6 dark:text-white/90" />
|
||||
</div>
|
||||
|
||||
<div className="flex items-end justify-between mt-5">
|
||||
<div>
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Customers
|
||||
</span>
|
||||
<h4 className="mt-2 font-bold text-gray-800 text-title-sm dark:text-white/90">
|
||||
3,782
|
||||
</h4>
|
||||
</div>
|
||||
<Badge color="success">
|
||||
<ArrowUpIcon />
|
||||
11.01%
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
{/* <!-- Metric Item End --> */}
|
||||
|
||||
{/* <!-- Metric Item Start --> */}
|
||||
<div className="rounded-2xl border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-white/[0.03] md:p-6">
|
||||
<div className="flex items-center justify-center w-12 h-12 bg-gray-100 rounded-xl dark:bg-gray-800">
|
||||
<BoxIconLine className="text-gray-800 dark:text-white/90" />
|
||||
</div>
|
||||
<div className="flex items-end justify-between mt-5">
|
||||
<div>
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Orders
|
||||
</span>
|
||||
<h4 className="mt-2 font-bold text-gray-800 text-title-sm dark:text-white/90">
|
||||
5,359
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<Badge color="error">
|
||||
<ArrowDownIcon className="text-error-500" />
|
||||
9.05%
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
{/* <!-- Metric Item End --> */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
154
src/components/ecommerce/MonthlySalesChart.tsx
Normal file
154
src/components/ecommerce/MonthlySalesChart.tsx
Normal file
@@ -0,0 +1,154 @@
|
||||
"use client";
|
||||
import { ApexOptions } from "apexcharts";
|
||||
import dynamic from "next/dynamic";
|
||||
import { MoreDotIcon } from "@/icons";
|
||||
import { DropdownItem } from "../ui/dropdown/DropdownItem";
|
||||
import { useState } from "react";
|
||||
import { Dropdown } from "../ui/dropdown/Dropdown";
|
||||
|
||||
// Dynamically import the ReactApexChart component
|
||||
const ReactApexChart = dynamic(() => import("react-apexcharts"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
export default function MonthlySalesChart() {
|
||||
const options: ApexOptions = {
|
||||
colors: ["#465fff"],
|
||||
chart: {
|
||||
fontFamily: "Outfit, sans-serif",
|
||||
type: "bar",
|
||||
height: 180,
|
||||
toolbar: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
plotOptions: {
|
||||
bar: {
|
||||
horizontal: false,
|
||||
columnWidth: "39%",
|
||||
borderRadius: 5,
|
||||
borderRadiusApplication: "end",
|
||||
},
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false,
|
||||
},
|
||||
stroke: {
|
||||
show: true,
|
||||
width: 4,
|
||||
colors: ["transparent"],
|
||||
},
|
||||
xaxis: {
|
||||
categories: [
|
||||
"Jan",
|
||||
"Feb",
|
||||
"Mar",
|
||||
"Apr",
|
||||
"May",
|
||||
"Jun",
|
||||
"Jul",
|
||||
"Aug",
|
||||
"Sep",
|
||||
"Oct",
|
||||
"Nov",
|
||||
"Dec",
|
||||
],
|
||||
axisBorder: {
|
||||
show: false,
|
||||
},
|
||||
axisTicks: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
show: true,
|
||||
position: "top",
|
||||
horizontalAlign: "left",
|
||||
fontFamily: "Outfit",
|
||||
},
|
||||
yaxis: {
|
||||
title: {
|
||||
text: undefined,
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
yaxis: {
|
||||
lines: {
|
||||
show: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
fill: {
|
||||
opacity: 1,
|
||||
},
|
||||
|
||||
tooltip: {
|
||||
x: {
|
||||
show: false,
|
||||
},
|
||||
y: {
|
||||
formatter: (val: number) => `${val}`,
|
||||
},
|
||||
},
|
||||
};
|
||||
const series = [
|
||||
{
|
||||
name: "Sales",
|
||||
data: [168, 385, 201, 298, 187, 195, 291, 110, 215, 390, 280, 112],
|
||||
},
|
||||
];
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
function toggleDropdown() {
|
||||
setIsOpen(!isOpen);
|
||||
}
|
||||
|
||||
function closeDropdown() {
|
||||
setIsOpen(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden rounded-2xl border border-gray-200 bg-white px-5 pt-5 dark:border-gray-800 dark:bg-white/[0.03] sm:px-6 sm:pt-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-lg font-semibold text-gray-800 dark:text-white/90">
|
||||
Monthly Sales
|
||||
</h3>
|
||||
|
||||
<div className="relative inline-block">
|
||||
<button onClick={toggleDropdown} className="dropdown-toggle">
|
||||
<MoreDotIcon className="text-gray-400 hover:text-gray-700 dark:hover:text-gray-300" />
|
||||
</button>
|
||||
<Dropdown
|
||||
isOpen={isOpen}
|
||||
onClose={closeDropdown}
|
||||
className="w-40 p-2"
|
||||
>
|
||||
<DropdownItem
|
||||
onItemClick={closeDropdown}
|
||||
className="flex w-full font-normal text-left text-gray-500 rounded-lg hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300"
|
||||
>
|
||||
View More
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
onItemClick={closeDropdown}
|
||||
className="flex w-full font-normal text-left text-gray-500 rounded-lg hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300"
|
||||
>
|
||||
Delete
|
||||
</DropdownItem>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="max-w-full overflow-x-auto custom-scrollbar">
|
||||
<div className="-ml-5 min-w-[650px] xl:min-w-full pl-2">
|
||||
<ReactApexChart
|
||||
options={options}
|
||||
series={series}
|
||||
type="bar"
|
||||
height={180}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
209
src/components/ecommerce/MonthlyTarget.tsx
Normal file
209
src/components/ecommerce/MonthlyTarget.tsx
Normal file
@@ -0,0 +1,209 @@
|
||||
"use client";
|
||||
// import Chart from "react-apexcharts";
|
||||
import { ApexOptions } from "apexcharts";
|
||||
|
||||
import dynamic from "next/dynamic";
|
||||
import { Dropdown } from "../ui/dropdown/Dropdown";
|
||||
import { MoreDotIcon } from "@/icons";
|
||||
import { useState } from "react";
|
||||
import { DropdownItem } from "../ui/dropdown/DropdownItem";
|
||||
// Dynamically import the ReactApexChart component
|
||||
const ReactApexChart = dynamic(() => import("react-apexcharts"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
export default function MonthlyTarget() {
|
||||
const series = [75.55];
|
||||
const options: ApexOptions = {
|
||||
colors: ["#465FFF"],
|
||||
chart: {
|
||||
fontFamily: "Outfit, sans-serif",
|
||||
type: "radialBar",
|
||||
height: 330,
|
||||
sparkline: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
plotOptions: {
|
||||
radialBar: {
|
||||
startAngle: -85,
|
||||
endAngle: 85,
|
||||
hollow: {
|
||||
size: "80%",
|
||||
},
|
||||
track: {
|
||||
background: "#E4E7EC",
|
||||
strokeWidth: "100%",
|
||||
margin: 5, // margin is in pixels
|
||||
},
|
||||
dataLabels: {
|
||||
name: {
|
||||
show: false,
|
||||
},
|
||||
value: {
|
||||
fontSize: "36px",
|
||||
fontWeight: "600",
|
||||
offsetY: -40,
|
||||
color: "#1D2939",
|
||||
formatter: function (val) {
|
||||
return val + "%";
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
fill: {
|
||||
type: "solid",
|
||||
colors: ["#465FFF"],
|
||||
},
|
||||
stroke: {
|
||||
lineCap: "round",
|
||||
},
|
||||
labels: ["Progress"],
|
||||
};
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
function toggleDropdown() {
|
||||
setIsOpen(!isOpen);
|
||||
}
|
||||
|
||||
function closeDropdown() {
|
||||
setIsOpen(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rounded-2xl border border-gray-200 bg-gray-100 dark:border-gray-800 dark:bg-white/[0.03]">
|
||||
<div className="px-5 pt-5 bg-white shadow-default rounded-2xl pb-11 dark:bg-gray-900 sm:px-6 sm:pt-6">
|
||||
<div className="flex justify-between">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-800 dark:text-white/90">
|
||||
Monthly Target
|
||||
</h3>
|
||||
<p className="mt-1 font-normal text-gray-500 text-theme-sm dark:text-gray-400">
|
||||
Target you’ve set for each month
|
||||
</p>
|
||||
</div>
|
||||
<div className="relative inline-block">
|
||||
<button onClick={toggleDropdown} className="dropdown-toggle">
|
||||
<MoreDotIcon className="text-gray-400 hover:text-gray-700 dark:hover:text-gray-300" />
|
||||
</button>
|
||||
<Dropdown
|
||||
isOpen={isOpen}
|
||||
onClose={closeDropdown}
|
||||
className="w-40 p-2"
|
||||
>
|
||||
<DropdownItem
|
||||
tag="a"
|
||||
onItemClick={closeDropdown}
|
||||
className="flex w-full font-normal text-left text-gray-500 rounded-lg hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300"
|
||||
>
|
||||
View More
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
tag="a"
|
||||
onItemClick={closeDropdown}
|
||||
className="flex w-full font-normal text-left text-gray-500 rounded-lg hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300"
|
||||
>
|
||||
Delete
|
||||
</DropdownItem>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative ">
|
||||
<div className="max-h-[330px]">
|
||||
<ReactApexChart
|
||||
options={options}
|
||||
series={series}
|
||||
type="radialBar"
|
||||
height={330}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<span className="absolute left-1/2 top-full -translate-x-1/2 -translate-y-[95%] rounded-full bg-success-50 px-3 py-1 text-xs font-medium text-success-600 dark:bg-success-500/15 dark:text-success-500">
|
||||
+10%
|
||||
</span>
|
||||
</div>
|
||||
<p className="mx-auto mt-10 w-full max-w-[380px] text-center text-sm text-gray-500 sm:text-base">
|
||||
You earn $3287 today, it's higher than last month. Keep up your
|
||||
good work!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center gap-5 px-6 py-3.5 sm:gap-8 sm:py-5">
|
||||
<div>
|
||||
<p className="mb-1 text-center text-gray-500 text-theme-xs dark:text-gray-400 sm:text-sm">
|
||||
Target
|
||||
</p>
|
||||
<p className="flex items-center justify-center gap-1 text-base font-semibold text-gray-800 dark:text-white/90 sm:text-lg">
|
||||
$20K
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M7.26816 13.6632C7.4056 13.8192 7.60686 13.9176 7.8311 13.9176C7.83148 13.9176 7.83187 13.9176 7.83226 13.9176C8.02445 13.9178 8.21671 13.8447 8.36339 13.6981L12.3635 9.70076C12.6565 9.40797 12.6567 8.9331 12.3639 8.6401C12.0711 8.34711 11.5962 8.34694 11.3032 8.63973L8.5811 11.36L8.5811 2.5C8.5811 2.08579 8.24531 1.75 7.8311 1.75C7.41688 1.75 7.0811 2.08579 7.0811 2.5L7.0811 11.3556L4.36354 8.63975C4.07055 8.34695 3.59568 8.3471 3.30288 8.64009C3.01008 8.93307 3.01023 9.40794 3.30321 9.70075L7.26816 13.6632Z"
|
||||
fill="#D92D20"
|
||||
/>
|
||||
</svg>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="w-px bg-gray-200 h-7 dark:bg-gray-800"></div>
|
||||
|
||||
<div>
|
||||
<p className="mb-1 text-center text-gray-500 text-theme-xs dark:text-gray-400 sm:text-sm">
|
||||
Revenue
|
||||
</p>
|
||||
<p className="flex items-center justify-center gap-1 text-base font-semibold text-gray-800 dark:text-white/90 sm:text-lg">
|
||||
$20K
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M7.60141 2.33683C7.73885 2.18084 7.9401 2.08243 8.16435 2.08243C8.16475 2.08243 8.16516 2.08243 8.16556 2.08243C8.35773 2.08219 8.54998 2.15535 8.69664 2.30191L12.6968 6.29924C12.9898 6.59203 12.9899 7.0669 12.6971 7.3599C12.4044 7.6529 11.9295 7.65306 11.6365 7.36027L8.91435 4.64004L8.91435 13.5C8.91435 13.9142 8.57856 14.25 8.16435 14.25C7.75013 14.25 7.41435 13.9142 7.41435 13.5L7.41435 4.64442L4.69679 7.36025C4.4038 7.65305 3.92893 7.6529 3.63613 7.35992C3.34333 7.06693 3.34348 6.59206 3.63646 6.29926L7.60141 2.33683Z"
|
||||
fill="#039855"
|
||||
/>
|
||||
</svg>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="w-px bg-gray-200 h-7 dark:bg-gray-800"></div>
|
||||
|
||||
<div>
|
||||
<p className="mb-1 text-center text-gray-500 text-theme-xs dark:text-gray-400 sm:text-sm">
|
||||
Today
|
||||
</p>
|
||||
<p className="flex items-center justify-center gap-1 text-base font-semibold text-gray-800 dark:text-white/90 sm:text-lg">
|
||||
$20K
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M7.60141 2.33683C7.73885 2.18084 7.9401 2.08243 8.16435 2.08243C8.16475 2.08243 8.16516 2.08243 8.16556 2.08243C8.35773 2.08219 8.54998 2.15535 8.69664 2.30191L12.6968 6.29924C12.9898 6.59203 12.9899 7.0669 12.6971 7.3599C12.4044 7.6529 11.9295 7.65306 11.6365 7.36027L8.91435 4.64004L8.91435 13.5C8.91435 13.9142 8.57856 14.25 8.16435 14.25C7.75013 14.25 7.41435 13.9142 7.41435 13.5L7.41435 4.64442L4.69679 7.36025C4.4038 7.65305 3.92893 7.6529 3.63613 7.35992C3.34333 7.06693 3.34348 6.59206 3.63646 6.29926L7.60141 2.33683Z"
|
||||
fill="#039855"
|
||||
/>
|
||||
</svg>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
211
src/components/ecommerce/RecentOrders.tsx
Normal file
211
src/components/ecommerce/RecentOrders.tsx
Normal file
@@ -0,0 +1,211 @@
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "../ui/table";
|
||||
import Badge from "../ui/badge/Badge";
|
||||
import Image from "next/image";
|
||||
|
||||
// Define the TypeScript interface for the table rows
|
||||
interface Product {
|
||||
id: number; // Unique identifier for each product
|
||||
name: string; // Product name
|
||||
variants: string; // Number of variants (e.g., "1 Variant", "2 Variants")
|
||||
category: string; // Category of the product
|
||||
price: string; // Price of the product (as a string with currency symbol)
|
||||
// status: string; // Status of the product
|
||||
image: string; // URL or path to the product image
|
||||
status: "Delivered" | "Pending" | "Canceled"; // Status of the product
|
||||
}
|
||||
|
||||
// Define the table data using the interface
|
||||
const tableData: Product[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: "MacBook Pro 13”",
|
||||
variants: "2 Variants",
|
||||
category: "Laptop",
|
||||
price: "$2399.00",
|
||||
status: "Delivered",
|
||||
image: "/images/product/product-01.jpg", // Replace with actual image URL
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Apple Watch Ultra",
|
||||
variants: "1 Variant",
|
||||
category: "Watch",
|
||||
price: "$879.00",
|
||||
status: "Pending",
|
||||
image: "/images/product/product-02.jpg", // Replace with actual image URL
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "iPhone 15 Pro Max",
|
||||
variants: "2 Variants",
|
||||
category: "SmartPhone",
|
||||
price: "$1869.00",
|
||||
status: "Delivered",
|
||||
image: "/images/product/product-03.jpg", // Replace with actual image URL
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "iPad Pro 3rd Gen",
|
||||
variants: "2 Variants",
|
||||
category: "Electronics",
|
||||
price: "$1699.00",
|
||||
status: "Canceled",
|
||||
image: "/images/product/product-04.jpg", // Replace with actual image URL
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "AirPods Pro 2nd Gen",
|
||||
variants: "1 Variant",
|
||||
category: "Accessories",
|
||||
price: "$240.00",
|
||||
status: "Delivered",
|
||||
image: "/images/product/product-05.jpg", // Replace with actual image URL
|
||||
},
|
||||
];
|
||||
|
||||
export default function RecentOrders() {
|
||||
return (
|
||||
<div className="overflow-hidden rounded-2xl border border-gray-200 bg-white px-4 pb-3 pt-4 dark:border-gray-800 dark:bg-white/[0.03] sm:px-6">
|
||||
<div className="flex flex-col gap-2 mb-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-800 dark:text-white/90">
|
||||
Recent Orders
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<button className="inline-flex items-center gap-2 rounded-lg border border-gray-300 bg-white px-4 py-2.5 text-theme-sm font-medium text-gray-700 shadow-theme-xs hover:bg-gray-50 hover:text-gray-800 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03] dark:hover:text-gray-200">
|
||||
<svg
|
||||
className="stroke-current fill-white dark:fill-gray-800"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M2.29004 5.90393H17.7067"
|
||||
stroke=""
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M17.7075 14.0961H2.29085"
|
||||
stroke=""
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M12.0826 3.33331C13.5024 3.33331 14.6534 4.48431 14.6534 5.90414C14.6534 7.32398 13.5024 8.47498 12.0826 8.47498C10.6627 8.47498 9.51172 7.32398 9.51172 5.90415C9.51172 4.48432 10.6627 3.33331 12.0826 3.33331Z"
|
||||
fill=""
|
||||
stroke=""
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
<path
|
||||
d="M7.91745 11.525C6.49762 11.525 5.34662 12.676 5.34662 14.0959C5.34661 15.5157 6.49762 16.6667 7.91745 16.6667C9.33728 16.6667 10.4883 15.5157 10.4883 14.0959C10.4883 12.676 9.33728 11.525 7.91745 11.525Z"
|
||||
fill=""
|
||||
stroke=""
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
</svg>
|
||||
Filter
|
||||
</button>
|
||||
<button className="inline-flex items-center gap-2 rounded-lg border border-gray-300 bg-white px-4 py-2.5 text-theme-sm font-medium text-gray-700 shadow-theme-xs hover:bg-gray-50 hover:text-gray-800 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03] dark:hover:text-gray-200">
|
||||
See all
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="max-w-full overflow-x-auto">
|
||||
<Table>
|
||||
{/* Table Header */}
|
||||
<TableHeader className="border-gray-100 dark:border-gray-800 border-y">
|
||||
<TableRow>
|
||||
<TableCell
|
||||
isHeader
|
||||
className="py-3 font-medium text-gray-500 text-start text-theme-xs dark:text-gray-400"
|
||||
>
|
||||
Products
|
||||
</TableCell>
|
||||
<TableCell
|
||||
isHeader
|
||||
className="py-3 font-medium text-gray-500 text-start text-theme-xs dark:text-gray-400"
|
||||
>
|
||||
Category
|
||||
</TableCell>
|
||||
<TableCell
|
||||
isHeader
|
||||
className="py-3 font-medium text-gray-500 text-start text-theme-xs dark:text-gray-400"
|
||||
>
|
||||
Price
|
||||
</TableCell>
|
||||
<TableCell
|
||||
isHeader
|
||||
className="py-3 font-medium text-gray-500 text-start text-theme-xs dark:text-gray-400"
|
||||
>
|
||||
Status
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
||||
{/* Table Body */}
|
||||
|
||||
<TableBody className="divide-y divide-gray-100 dark:divide-gray-800">
|
||||
{tableData.map((product) => (
|
||||
<TableRow key={product.id} className="">
|
||||
<TableCell className="py-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="h-[50px] w-[50px] overflow-hidden rounded-md">
|
||||
<Image
|
||||
width={50}
|
||||
height={50}
|
||||
src={product.image}
|
||||
className="h-[50px] w-[50px]"
|
||||
alt={product.name}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-800 text-theme-sm dark:text-white/90">
|
||||
{product.name}
|
||||
</p>
|
||||
<span className="text-gray-500 text-theme-xs dark:text-gray-400">
|
||||
{product.variants}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="py-3 text-gray-500 text-theme-sm dark:text-gray-400">
|
||||
{product.price}
|
||||
</TableCell>
|
||||
<TableCell className="py-3 text-gray-500 text-theme-sm dark:text-gray-400">
|
||||
{product.category}
|
||||
</TableCell>
|
||||
<TableCell className="py-3 text-gray-500 text-theme-sm dark:text-gray-400">
|
||||
<Badge
|
||||
size="sm"
|
||||
color={
|
||||
product.status === "Delivered"
|
||||
? "success"
|
||||
: product.status === "Pending"
|
||||
? "warning"
|
||||
: "error"
|
||||
}
|
||||
>
|
||||
{product.status}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
180
src/components/ecommerce/StatisticsChart.tsx
Normal file
180
src/components/ecommerce/StatisticsChart.tsx
Normal file
@@ -0,0 +1,180 @@
|
||||
"use client";
|
||||
import { useEffect, useRef } from "react";
|
||||
import dynamic from "next/dynamic";
|
||||
import { ApexOptions } from "apexcharts";
|
||||
import flatpickr from "flatpickr";
|
||||
import ChartTab from "../common/ChartTab";
|
||||
import { CalenderIcon } from "../../icons";
|
||||
|
||||
const Chart = dynamic(() => import("react-apexcharts"), { ssr: false });
|
||||
|
||||
export default function StatisticsChart() {
|
||||
const datePickerRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!datePickerRef.current) return;
|
||||
|
||||
const today = new Date();
|
||||
const sevenDaysAgo = new Date();
|
||||
sevenDaysAgo.setDate(today.getDate() - 6);
|
||||
|
||||
const fp = flatpickr(datePickerRef.current, {
|
||||
mode: "range",
|
||||
static: true,
|
||||
monthSelectorType: "static",
|
||||
dateFormat: "M d",
|
||||
defaultDate: [sevenDaysAgo, today],
|
||||
clickOpens: true,
|
||||
prevArrow:
|
||||
'<svg class="stroke-current" width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12.5 15L7.5 10L12.5 5" stroke="" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>',
|
||||
nextArrow:
|
||||
'<svg class="stroke-current" width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M7.5 15L12.5 10L7.5 5" stroke="" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>',
|
||||
});
|
||||
|
||||
return () => {
|
||||
if (!Array.isArray(fp)) {
|
||||
fp.destroy();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const options: ApexOptions = {
|
||||
legend: {
|
||||
show: false, // Hide legend
|
||||
position: "top",
|
||||
horizontalAlign: "left",
|
||||
},
|
||||
colors: ["#465FFF", "#9CB9FF"], // Define line colors
|
||||
chart: {
|
||||
fontFamily: "Outfit, sans-serif",
|
||||
height: 310,
|
||||
type: "line", // Set the chart type to 'line'
|
||||
toolbar: {
|
||||
show: false, // Hide chart toolbar
|
||||
},
|
||||
},
|
||||
stroke: {
|
||||
curve: "straight", // Define the line style (straight, smooth, or step)
|
||||
width: [2, 2], // Line width for each dataset
|
||||
},
|
||||
|
||||
fill: {
|
||||
type: "gradient",
|
||||
gradient: {
|
||||
opacityFrom: 0.55,
|
||||
opacityTo: 0,
|
||||
},
|
||||
},
|
||||
markers: {
|
||||
size: 0, // Size of the marker points
|
||||
strokeColors: "#fff", // Marker border color
|
||||
strokeWidth: 2,
|
||||
hover: {
|
||||
size: 6, // Marker size on hover
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
xaxis: {
|
||||
lines: {
|
||||
show: false, // Hide grid lines on x-axis
|
||||
},
|
||||
},
|
||||
yaxis: {
|
||||
lines: {
|
||||
show: true, // Show grid lines on y-axis
|
||||
},
|
||||
},
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false, // Disable data labels
|
||||
},
|
||||
tooltip: {
|
||||
enabled: true, // Enable tooltip
|
||||
x: {
|
||||
format: "dd MMM yyyy", // Format for x-axis tooltip
|
||||
},
|
||||
},
|
||||
xaxis: {
|
||||
type: "category", // Category-based x-axis
|
||||
categories: [
|
||||
"Jan",
|
||||
"Feb",
|
||||
"Mar",
|
||||
"Apr",
|
||||
"May",
|
||||
"Jun",
|
||||
"Jul",
|
||||
"Aug",
|
||||
"Sep",
|
||||
"Oct",
|
||||
"Nov",
|
||||
"Dec",
|
||||
],
|
||||
axisBorder: {
|
||||
show: false, // Hide x-axis border
|
||||
},
|
||||
axisTicks: {
|
||||
show: false, // Hide x-axis ticks
|
||||
},
|
||||
tooltip: {
|
||||
enabled: false, // Disable tooltip for x-axis points
|
||||
},
|
||||
},
|
||||
yaxis: {
|
||||
labels: {
|
||||
style: {
|
||||
fontSize: "12px", // Adjust font size for y-axis labels
|
||||
colors: ["#6B7280"], // Color of the labels
|
||||
},
|
||||
},
|
||||
title: {
|
||||
text: "", // Remove y-axis title
|
||||
style: {
|
||||
fontSize: "0px",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const series = [
|
||||
{
|
||||
name: "Sales",
|
||||
data: [180, 190, 170, 160, 175, 165, 170, 205, 230, 210, 240, 235],
|
||||
},
|
||||
{
|
||||
name: "Revenue",
|
||||
data: [40, 30, 50, 40, 55, 40, 70, 100, 110, 120, 150, 140],
|
||||
},
|
||||
];
|
||||
return (
|
||||
<div className="rounded-2xl border border-gray-200 bg-white px-5 pb-5 pt-5 dark:border-gray-800 dark:bg-white/[0.03] sm:px-6 sm:pt-6">
|
||||
<div className="flex flex-col gap-5 mb-6 sm:flex-row sm:justify-between">
|
||||
<div className="w-full">
|
||||
<h3 className="text-lg font-semibold text-gray-800 dark:text-white/90">
|
||||
Statistics
|
||||
</h3>
|
||||
<p className="mt-1 text-gray-500 text-theme-sm dark:text-gray-400">
|
||||
Target you've set for each month
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 sm:justify-end">
|
||||
<ChartTab />
|
||||
<div className="relative inline-flex items-center">
|
||||
<CalenderIcon className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 lg:left-3 lg:top-1/2 lg:translate-x-0 lg:-translate-y-1/2 text-gray-500 dark:text-gray-400 pointer-events-none z-10" />
|
||||
<input
|
||||
ref={datePickerRef}
|
||||
className="h-10 w-10 lg:w-40 lg:h-auto lg:pl-10 lg:pr-3 lg:py-2 rounded-lg border border-gray-200 bg-white text-sm font-medium text-transparent lg:text-gray-700 outline-none dark:border-gray-700 dark:bg-gray-800 dark:lg:text-gray-300 cursor-pointer"
|
||||
placeholder="Select date range"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="max-w-full overflow-x-auto custom-scrollbar">
|
||||
<div className="min-w-[1000px] xl:min-w-full">
|
||||
<Chart options={options} series={series} type="area" height={310} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
53
src/components/example/ModalExample/DefaultModal.tsx
Normal file
53
src/components/example/ModalExample/DefaultModal.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
import ComponentCard from "../../common/ComponentCard";
|
||||
|
||||
import { Modal } from "../../ui/modal";
|
||||
import Button from "../../ui/button/Button";
|
||||
import { useModal } from "@/hooks/useModal";
|
||||
|
||||
export default function DefaultModal() {
|
||||
const { isOpen, openModal, closeModal } = useModal();
|
||||
const handleSave = () => {
|
||||
// Handle save logic here
|
||||
console.log("Saving changes...");
|
||||
closeModal();
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<ComponentCard title="Default Modal">
|
||||
<Button size="sm" onClick={openModal}>
|
||||
Open Modal
|
||||
</Button>
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={closeModal}
|
||||
className="max-w-[600px] p-5 lg:p-10"
|
||||
>
|
||||
<h4 className="font-semibold text-gray-800 mb-7 text-title-sm dark:text-white/90">
|
||||
Modal Heading
|
||||
</h4>
|
||||
<p className="text-sm leading-6 text-gray-500 dark:text-gray-400">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||
Pellentesque euismod est quis mauris lacinia pharetra. Sed a ligula
|
||||
ac odio condimentum aliquet a nec nulla. Aliquam bibendum ex sit
|
||||
amet ipsum rutrum feugiat ultrices enim quam.
|
||||
</p>
|
||||
<p className="mt-5 text-sm leading-6 text-gray-500 dark:text-gray-400">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||
Pellentesque euismod est quis mauris lacinia pharetra. Sed a ligula
|
||||
ac odio.
|
||||
</p>
|
||||
<div className="flex items-center justify-end w-full gap-3 mt-8">
|
||||
<Button size="sm" variant="outline" onClick={closeModal}>
|
||||
Close
|
||||
</Button>
|
||||
<Button size="sm" onClick={handleSave}>
|
||||
Save Changes
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
</ComponentCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
71
src/components/example/ModalExample/FormInModal.tsx
Normal file
71
src/components/example/ModalExample/FormInModal.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
import ComponentCard from "../../common/ComponentCard";
|
||||
import Button from "../../ui/button/Button";
|
||||
import { Modal } from "../../ui/modal";
|
||||
import Label from "../../form/Label";
|
||||
import Input from "../../form/input/InputField";
|
||||
import { useModal } from "@/hooks/useModal";
|
||||
|
||||
export default function FormInModal() {
|
||||
const { isOpen, openModal, closeModal } = useModal();
|
||||
const handleSave = () => {
|
||||
// Handle save logic here
|
||||
console.log("Saving changes...");
|
||||
closeModal();
|
||||
};
|
||||
return (
|
||||
<ComponentCard title="Form In Modal">
|
||||
<Button size="sm" onClick={openModal}>
|
||||
Open Modal
|
||||
</Button>
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={closeModal}
|
||||
className="max-w-[584px] p-5 lg:p-10"
|
||||
>
|
||||
<form className="">
|
||||
<h4 className="mb-6 text-lg font-medium text-gray-800 dark:text-white/90">
|
||||
Personal Information
|
||||
</h4>
|
||||
|
||||
<div className="grid grid-cols-1 gap-x-6 gap-y-5 sm:grid-cols-2">
|
||||
<div className="col-span-1">
|
||||
<Label>First Name</Label>
|
||||
<Input type="text" placeholder="Emirhan" />
|
||||
</div>
|
||||
|
||||
<div className="col-span-1">
|
||||
<Label>Last Name</Label>
|
||||
<Input type="text" placeholder="Boruch" />
|
||||
</div>
|
||||
|
||||
<div className="col-span-1">
|
||||
<Label>Last Name</Label>
|
||||
<Input type="email" placeholder="emirhanboruch55@gmail.com" />
|
||||
</div>
|
||||
|
||||
<div className="col-span-1">
|
||||
<Label>Phone</Label>
|
||||
<Input type="text" placeholder="+09 363 398 46" />
|
||||
</div>
|
||||
|
||||
<div className="col-span-1 sm:col-span-2">
|
||||
<Label>Bio</Label>
|
||||
<Input type="text" placeholder="Team Manager" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end w-full gap-3 mt-6">
|
||||
<Button size="sm" variant="outline" onClick={closeModal}>
|
||||
Close
|
||||
</Button>
|
||||
<Button size="sm" onClick={handleSave}>
|
||||
Save Changes
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
</ComponentCard>
|
||||
);
|
||||
}
|
||||
66
src/components/example/ModalExample/FullScreenModal.tsx
Normal file
66
src/components/example/ModalExample/FullScreenModal.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
"use client";
|
||||
import { useModal } from "@/hooks/useModal";
|
||||
import ComponentCard from "../../common/ComponentCard";
|
||||
|
||||
import Button from "../../ui/button/Button";
|
||||
import { Modal } from "../../ui/modal";
|
||||
|
||||
export default function FullScreenModal() {
|
||||
const {
|
||||
isOpen: isFullscreenModalOpen,
|
||||
openModal: openFullscreenModal,
|
||||
closeModal: closeFullscreenModal,
|
||||
} = useModal();
|
||||
const handleSave = () => {
|
||||
// Handle save logic here
|
||||
console.log("Saving changes...");
|
||||
closeFullscreenModal();
|
||||
};
|
||||
return (
|
||||
<ComponentCard title="Full Screen Modal">
|
||||
<Button size="sm" onClick={openFullscreenModal}>
|
||||
Open Modal
|
||||
</Button>
|
||||
<Modal
|
||||
isOpen={isFullscreenModalOpen}
|
||||
onClose={closeFullscreenModal}
|
||||
isFullscreen={true}
|
||||
showCloseButton={true}
|
||||
>
|
||||
<div className="fixed top-0 left-0 flex flex-col justify-between w-full h-screen p-6 overflow-x-hidden overflow-y-auto bg-white dark:bg-gray-900 lg:p-10">
|
||||
<div>
|
||||
<h4 className="font-semibold text-gray-800 mb-7 text-title-sm dark:text-white/90">
|
||||
Modal Heading
|
||||
</h4>
|
||||
<p className="text-sm leading-6 text-gray-500 dark:text-gray-400">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||
Pellentesque euismod est quis mauris lacinia pharetra. Sed a
|
||||
ligula ac odio condimentum aliquet a nec nulla. Aliquam bibendum
|
||||
ex sit amet ipsum rutrum feugiat ultrices enim quam.
|
||||
</p>
|
||||
<p className="mt-5 text-sm leading-6 text-gray-500 dark:text-gray-400">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||
Pellentesque euismod est quis mauris lacinia pharetra. Sed a
|
||||
ligula ac odio condimentum aliquet a nec nulla. Aliquam bibendum
|
||||
ex sit amet ipsum rutrum feugiat ultrices enim quam odio
|
||||
condimentum aliquet a nec nulla pellentesque euismod est quis
|
||||
mauris lacinia pharetra.
|
||||
</p>
|
||||
<p className="mt-5 text-sm leading-6 text-gray-500 dark:text-gray-400">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||
Pellentesque euismod est quis mauris lacinia pharetra.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center justify-end w-full gap-3 mt-8">
|
||||
<Button size="sm" variant="outline" onClick={closeFullscreenModal}>
|
||||
Close
|
||||
</Button>
|
||||
<Button size="sm" onClick={handleSave}>
|
||||
Save Changes
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</ComponentCard>
|
||||
);
|
||||
}
|
||||
282
src/components/example/ModalExample/ModalBasedAlerts.tsx
Normal file
282
src/components/example/ModalExample/ModalBasedAlerts.tsx
Normal file
@@ -0,0 +1,282 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
import ComponentCard from "../../common/ComponentCard";
|
||||
|
||||
import { Modal } from "../../ui/modal";
|
||||
import { useModal } from "@/hooks/useModal";
|
||||
|
||||
export default function ModalBasedAlerts() {
|
||||
const successModal = useModal();
|
||||
const infoModal = useModal();
|
||||
const warningModal = useModal();
|
||||
const errorModal = useModal();
|
||||
return (
|
||||
<ComponentCard title="Modal Based Alerts">
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<button
|
||||
onClick={successModal.openModal}
|
||||
className="px-4 py-3 text-sm font-medium text-white rounded-lg bg-success-500 shadow-theme-xs hover:bg-success-600"
|
||||
>
|
||||
Success Alert
|
||||
</button>
|
||||
<button
|
||||
onClick={infoModal.openModal}
|
||||
className="px-4 py-3 text-sm font-medium text-white rounded-lg bg-blue-light-500 shadow-theme-xs hover:bg-blue-light-600"
|
||||
>
|
||||
Info Alert
|
||||
</button>
|
||||
<button
|
||||
onClick={warningModal.openModal}
|
||||
className="px-4 py-3 text-sm font-medium text-white rounded-lg bg-warning-500 shadow-theme-xs hover:bg-warning-600"
|
||||
>
|
||||
Warning Alert
|
||||
</button>
|
||||
<button
|
||||
onClick={errorModal.openModal}
|
||||
className="px-4 py-3 text-sm font-medium text-white rounded-lg bg-error-500 shadow-theme-xs hover:bg-error-600"
|
||||
>
|
||||
Danger Alert
|
||||
</button>
|
||||
</div>
|
||||
{/* Success Modal */}
|
||||
<Modal
|
||||
isOpen={successModal.isOpen}
|
||||
onClose={successModal.closeModal}
|
||||
className="max-w-[600px] p-5 lg:p-10"
|
||||
>
|
||||
<div className="text-center">
|
||||
<div className="relative flex items-center justify-center z-1 mb-7">
|
||||
<svg
|
||||
className="fill-success-50 dark:fill-success-500/15"
|
||||
width="90"
|
||||
height="90"
|
||||
viewBox="0 0 90 90"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M34.364 6.85053C38.6205 -2.28351 51.3795 -2.28351 55.636 6.85053C58.0129 11.951 63.5594 14.6722 68.9556 13.3853C78.6192 11.0807 86.5743 21.2433 82.2185 30.3287C79.7862 35.402 81.1561 41.5165 85.5082 45.0122C93.3019 51.2725 90.4628 63.9451 80.7747 66.1403C75.3648 67.3661 71.5265 72.2695 71.5572 77.9156C71.6123 88.0265 60.1169 93.6664 52.3918 87.3184C48.0781 83.7737 41.9219 83.7737 37.6082 87.3184C29.8831 93.6664 18.3877 88.0266 18.4428 77.9156C18.4735 72.2695 14.6352 67.3661 9.22531 66.1403C-0.462787 63.9451 -3.30193 51.2725 4.49185 45.0122C8.84391 41.5165 10.2138 35.402 7.78151 30.3287C3.42572 21.2433 11.3808 11.0807 21.0444 13.3853C26.4406 14.6722 31.9871 11.951 34.364 6.85053Z"
|
||||
fill=""
|
||||
fillOpacity=""
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<span className="absolute -translate-x-1/2 -translate-y-1/2 left-1/2 top-1/2">
|
||||
<svg
|
||||
className="fill-success-600 dark:fill-success-500"
|
||||
width="38"
|
||||
height="38"
|
||||
viewBox="0 0 38 38"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M5.9375 19.0004C5.9375 11.7854 11.7864 5.93652 19.0014 5.93652C26.2164 5.93652 32.0653 11.7854 32.0653 19.0004C32.0653 26.2154 26.2164 32.0643 19.0014 32.0643C11.7864 32.0643 5.9375 26.2154 5.9375 19.0004ZM19.0014 2.93652C10.1296 2.93652 2.9375 10.1286 2.9375 19.0004C2.9375 27.8723 10.1296 35.0643 19.0014 35.0643C27.8733 35.0643 35.0653 27.8723 35.0653 19.0004C35.0653 10.1286 27.8733 2.93652 19.0014 2.93652ZM24.7855 17.0575C25.3713 16.4717 25.3713 15.522 24.7855 14.9362C24.1997 14.3504 23.25 14.3504 22.6642 14.9362L17.7177 19.8827L15.3387 17.5037C14.7529 16.9179 13.8031 16.9179 13.2173 17.5037C12.6316 18.0894 12.6316 19.0392 13.2173 19.625L16.657 23.0647C16.9383 23.346 17.3199 23.504 17.7177 23.504C18.1155 23.504 18.4971 23.346 18.7784 23.0647L24.7855 17.0575Z"
|
||||
fill=""
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<h4 className="mb-2 text-2xl font-semibold text-gray-800 dark:text-white/90 sm:text-title-sm">
|
||||
Well Done!
|
||||
</h4>
|
||||
<p className="text-sm leading-6 text-gray-500 dark:text-gray-400">
|
||||
Lorem ipsum dolor sit amet consectetur. Feugiat ipsum libero tempor
|
||||
felis risus nisi non. Quisque eu ut tempor curabitur.
|
||||
</p>
|
||||
|
||||
<div className="flex items-center justify-center w-full gap-3 mt-7">
|
||||
<button
|
||||
type="button"
|
||||
className="flex justify-center w-full px-4 py-3 text-sm font-medium text-white rounded-lg bg-success-500 shadow-theme-xs hover:bg-success-600 sm:w-auto"
|
||||
>
|
||||
Okay, Got It
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
{/* Info Modal */}
|
||||
<Modal
|
||||
isOpen={infoModal.isOpen}
|
||||
onClose={infoModal.closeModal}
|
||||
className="max-w-[600px] p-5 lg:p-10"
|
||||
>
|
||||
<div className="text-center">
|
||||
<div className="relative flex items-center justify-center z-1 mb-7">
|
||||
<svg
|
||||
className="fill-blue-light-50 dark:fill-blue-light-500/15"
|
||||
width="90"
|
||||
height="90"
|
||||
viewBox="0 0 90 90"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M34.364 6.85053C38.6205 -2.28351 51.3795 -2.28351 55.636 6.85053C58.0129 11.951 63.5594 14.6722 68.9556 13.3853C78.6192 11.0807 86.5743 21.2433 82.2185 30.3287C79.7862 35.402 81.1561 41.5165 85.5082 45.0122C93.3019 51.2725 90.4628 63.9451 80.7747 66.1403C75.3648 67.3661 71.5265 72.2695 71.5572 77.9156C71.6123 88.0265 60.1169 93.6664 52.3918 87.3184C48.0781 83.7737 41.9219 83.7737 37.6082 87.3184C29.8831 93.6664 18.3877 88.0266 18.4428 77.9156C18.4735 72.2695 14.6352 67.3661 9.22531 66.1403C-0.462787 63.9451 -3.30193 51.2725 4.49185 45.0122C8.84391 41.5165 10.2138 35.402 7.78151 30.3287C3.42572 21.2433 11.3808 11.0807 21.0444 13.3853C26.4406 14.6722 31.9871 11.951 34.364 6.85053Z"
|
||||
fill=""
|
||||
fillOpacity=""
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<span className="absolute -translate-x-1/2 -translate-y-1/2 left-1/2 top-1/2">
|
||||
<svg
|
||||
className="fill-blue-light-500 dark:fill-blue-light-500"
|
||||
width="38"
|
||||
height="38"
|
||||
viewBox="0 0 38 38"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M5.85547 18.9998C5.85547 11.7396 11.7411 5.854 19.0013 5.854C26.2615 5.854 32.1471 11.7396 32.1471 18.9998C32.1471 26.2601 26.2615 32.1457 19.0013 32.1457C11.7411 32.1457 5.85547 26.2601 5.85547 18.9998ZM19.0013 2.854C10.0842 2.854 2.85547 10.0827 2.85547 18.9998C2.85547 27.9169 10.0842 35.1457 19.0013 35.1457C27.9184 35.1457 35.1471 27.9169 35.1471 18.9998C35.1471 10.0827 27.9184 2.854 19.0013 2.854ZM16.9999 11.9145C16.9999 13.0191 17.8953 13.9145 18.9999 13.9145H19.0015C20.106 13.9145 21.0015 13.0191 21.0015 11.9145C21.0015 10.81 20.106 9.91454 19.0015 9.91454H18.9999C17.8953 9.91454 16.9999 10.81 16.9999 11.9145ZM19.0014 27.8171C18.173 27.8171 17.5014 27.1455 17.5014 26.3171V17.3293C17.5014 16.5008 18.173 15.8293 19.0014 15.8293C19.8299 15.8293 20.5014 16.5008 20.5014 17.3293L20.5014 26.3171C20.5014 27.1455 19.8299 27.8171 19.0014 27.8171Z"
|
||||
fill=""
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h4 className="mb-2 text-2xl font-semibold text-gray-800 dark:text-white/90 sm:text-title-sm">
|
||||
Information Alert!
|
||||
</h4>
|
||||
<p className="text-sm leading-6 text-gray-500 dark:text-gray-400">
|
||||
Lorem ipsum dolor sit amet consectetur. Feugiat ipsum libero tempor
|
||||
felis risus nisi non. Quisque eu ut tempor curabitur.
|
||||
</p>
|
||||
|
||||
<div className="flex items-center justify-center w-full gap-3 mt-7">
|
||||
<button
|
||||
type="button"
|
||||
className="flex justify-center w-full px-4 py-3 text-sm font-medium text-white rounded-lg bg-blue-light-500 shadow-theme-xs hover:bg-blue-light-600 sm:w-auto"
|
||||
>
|
||||
Okay, Got It
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
{/* Warning Modal */}
|
||||
<Modal
|
||||
isOpen={warningModal.isOpen}
|
||||
onClose={warningModal.closeModal}
|
||||
className="max-w-[600px] p-5 lg:p-10"
|
||||
>
|
||||
<div className="text-center">
|
||||
<div className="relative flex items-center justify-center z-1 mb-7">
|
||||
<svg
|
||||
className="fill-warning-50 dark:fill-warning-500/15"
|
||||
width="90"
|
||||
height="90"
|
||||
viewBox="0 0 90 90"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M34.364 6.85053C38.6205 -2.28351 51.3795 -2.28351 55.636 6.85053C58.0129 11.951 63.5594 14.6722 68.9556 13.3853C78.6192 11.0807 86.5743 21.2433 82.2185 30.3287C79.7862 35.402 81.1561 41.5165 85.5082 45.0122C93.3019 51.2725 90.4628 63.9451 80.7747 66.1403C75.3648 67.3661 71.5265 72.2695 71.5572 77.9156C71.6123 88.0265 60.1169 93.6664 52.3918 87.3184C48.0781 83.7737 41.9219 83.7737 37.6082 87.3184C29.8831 93.6664 18.3877 88.0266 18.4428 77.9156C18.4735 72.2695 14.6352 67.3661 9.22531 66.1403C-0.462787 63.9451 -3.30193 51.2725 4.49185 45.0122C8.84391 41.5165 10.2138 35.402 7.78151 30.3287C3.42572 21.2433 11.3808 11.0807 21.0444 13.3853C26.4406 14.6722 31.9871 11.951 34.364 6.85053Z"
|
||||
fill=""
|
||||
fillOpacity=""
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<span className="absolute -translate-x-1/2 -translate-y-1/2 left-1/2 top-1/2">
|
||||
<svg
|
||||
className="fill-warning-600 dark:fill-orange-400"
|
||||
width="38"
|
||||
height="38"
|
||||
viewBox="0 0 38 38"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M32.1445 19.0002C32.1445 26.2604 26.2589 32.146 18.9987 32.146C11.7385 32.146 5.85287 26.2604 5.85287 19.0002C5.85287 11.7399 11.7385 5.85433 18.9987 5.85433C26.2589 5.85433 32.1445 11.7399 32.1445 19.0002ZM18.9987 35.146C27.9158 35.146 35.1445 27.9173 35.1445 19.0002C35.1445 10.0831 27.9158 2.85433 18.9987 2.85433C10.0816 2.85433 2.85287 10.0831 2.85287 19.0002C2.85287 27.9173 10.0816 35.146 18.9987 35.146ZM21.0001 26.0855C21.0001 24.9809 20.1047 24.0855 19.0001 24.0855L18.9985 24.0855C17.894 24.0855 16.9985 24.9809 16.9985 26.0855C16.9985 27.19 17.894 28.0855 18.9985 28.0855L19.0001 28.0855C20.1047 28.0855 21.0001 27.19 21.0001 26.0855ZM18.9986 10.1829C19.827 10.1829 20.4986 10.8545 20.4986 11.6829L20.4986 20.6707C20.4986 21.4992 19.827 22.1707 18.9986 22.1707C18.1701 22.1707 17.4986 21.4992 17.4986 20.6707L17.4986 11.6829C17.4986 10.8545 18.1701 10.1829 18.9986 10.1829Z"
|
||||
fill=""
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h4 className="mb-2 text-2xl font-semibold text-gray-800 dark:text-white/90 sm:text-title-sm">
|
||||
Warning Alert!
|
||||
</h4>
|
||||
<p className="text-sm leading-6 text-gray-500 dark:text-gray-400">
|
||||
Lorem ipsum dolor sit amet consectetur. Feugiat ipsum libero tempor
|
||||
felis risus nisi non. Quisque eu ut tempor curabitur.
|
||||
</p>
|
||||
|
||||
<div className="flex items-center justify-center w-full gap-3 mt-7">
|
||||
<button
|
||||
type="button"
|
||||
className="flex justify-center w-full px-4 py-3 text-sm font-medium text-white rounded-lg bg-warning-500 shadow-theme-xs hover:bg-warning-600 sm:w-auto"
|
||||
>
|
||||
Okay, Got It
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
{/* Error Modal */}
|
||||
<Modal
|
||||
isOpen={errorModal.isOpen}
|
||||
onClose={errorModal.closeModal}
|
||||
className="max-w-[600px] p-5 lg:p-10"
|
||||
>
|
||||
<div className="text-center">
|
||||
<div className="relative flex items-center justify-center z-1 mb-7">
|
||||
<svg
|
||||
className="fill-error-50 dark:fill-error-500/15"
|
||||
width="90"
|
||||
height="90"
|
||||
viewBox="0 0 90 90"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M34.364 6.85053C38.6205 -2.28351 51.3795 -2.28351 55.636 6.85053C58.0129 11.951 63.5594 14.6722 68.9556 13.3853C78.6192 11.0807 86.5743 21.2433 82.2185 30.3287C79.7862 35.402 81.1561 41.5165 85.5082 45.0122C93.3019 51.2725 90.4628 63.9451 80.7747 66.1403C75.3648 67.3661 71.5265 72.2695 71.5572 77.9156C71.6123 88.0265 60.1169 93.6664 52.3918 87.3184C48.0781 83.7737 41.9219 83.7737 37.6082 87.3184C29.8831 93.6664 18.3877 88.0266 18.4428 77.9156C18.4735 72.2695 14.6352 67.3661 9.22531 66.1403C-0.462787 63.9451 -3.30193 51.2725 4.49185 45.0122C8.84391 41.5165 10.2138 35.402 7.78151 30.3287C3.42572 21.2433 11.3808 11.0807 21.0444 13.3853C26.4406 14.6722 31.9871 11.951 34.364 6.85053Z"
|
||||
fill=""
|
||||
fillOpacity=""
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<span className="absolute -translate-x-1/2 -translate-y-1/2 left-1/2 top-1/2">
|
||||
<svg
|
||||
className="fill-error-600 dark:fill-error-500"
|
||||
width="38"
|
||||
height="38"
|
||||
viewBox="0 0 38 38"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M9.62684 11.7496C9.04105 11.1638 9.04105 10.2141 9.62684 9.6283C10.2126 9.04252 11.1624 9.04252 11.7482 9.6283L18.9985 16.8786L26.2485 9.62851C26.8343 9.04273 27.7841 9.04273 28.3699 9.62851C28.9556 10.2143 28.9556 11.164 28.3699 11.7498L21.1198 18.9999L28.3699 26.25C28.9556 26.8358 28.9556 27.7855 28.3699 28.3713C27.7841 28.9571 26.8343 28.9571 26.2485 28.3713L18.9985 21.1212L11.7482 28.3715C11.1624 28.9573 10.2126 28.9573 9.62684 28.3715C9.04105 27.7857 9.04105 26.836 9.62684 26.2502L16.8771 18.9999L9.62684 11.7496Z"
|
||||
fill=""
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h4 className="mb-2 text-2xl font-semibold text-gray-800 dark:text-white/90 sm:text-title-sm">
|
||||
Danger Alert!
|
||||
</h4>
|
||||
<p className="text-sm leading-6 text-gray-500 dark:text-gray-400">
|
||||
Lorem ipsum dolor sit amet consectetur. Feugiat ipsum libero tempor
|
||||
felis risus nisi non. Quisque eu ut tempor curabitur.
|
||||
</p>
|
||||
|
||||
<div className="flex items-center justify-center w-full gap-3 mt-7">
|
||||
<button
|
||||
type="button"
|
||||
className="flex justify-center w-full px-4 py-3 text-sm font-medium text-white rounded-lg bg-error-500 shadow-theme-xs hover:bg-error-600 sm:w-auto"
|
||||
>
|
||||
Okay, Got It
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</ComponentCard>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
import ComponentCard from "../../common/ComponentCard";
|
||||
import Button from "../../ui/button/Button";
|
||||
import { Modal } from "../../ui/modal";
|
||||
import { useModal } from "@/hooks/useModal";
|
||||
|
||||
export default function VerticallyCenteredModal() {
|
||||
const { isOpen, openModal, closeModal } = useModal();
|
||||
const handleSave = () => {
|
||||
// Handle save logic here
|
||||
console.log("Saving changes...");
|
||||
closeModal();
|
||||
};
|
||||
return (
|
||||
<ComponentCard title="Vertically Centered Modal">
|
||||
<Button size="sm" onClick={openModal}>
|
||||
Open Modal
|
||||
</Button>
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={closeModal}
|
||||
showCloseButton={false}
|
||||
className="max-w-[507px] p-6 lg:p-10"
|
||||
>
|
||||
<div className="text-center">
|
||||
<h4 className="mb-2 text-2xl font-semibold text-gray-800 dark:text-white/90 sm:text-title-sm">
|
||||
All Done! Success Confirmed
|
||||
</h4>
|
||||
<p className="text-sm leading-6 text-gray-500 dark:text-gray-400">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||
Pellentesque euismod est quis mauris lacinia pharetra.
|
||||
</p>
|
||||
|
||||
<div className="flex items-center justify-center w-full gap-3 mt-8">
|
||||
<Button size="sm" variant="outline" onClick={closeModal}>
|
||||
Close
|
||||
</Button>
|
||||
<Button size="sm" onClick={handleSave}>
|
||||
Save Changes
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</ComponentCard>
|
||||
);
|
||||
}
|
||||
23
src/components/form/Form.tsx
Normal file
23
src/components/form/Form.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import React, { FC, ReactNode, FormEvent } from "react";
|
||||
|
||||
interface FormProps {
|
||||
onSubmit: (event: FormEvent<HTMLFormElement>) => void;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const Form: FC<FormProps> = ({ onSubmit, children, className }) => {
|
||||
return (
|
||||
<form
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault(); // Prevent default form submission
|
||||
onSubmit(event);
|
||||
}}
|
||||
className={` ${className}`} // Default spacing between form fields
|
||||
>
|
||||
{children}
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default Form;
|
||||
27
src/components/form/Label.tsx
Normal file
27
src/components/form/Label.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import React, { FC, ReactNode } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
interface LabelProps {
|
||||
htmlFor?: string;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const Label: FC<LabelProps> = ({ htmlFor, children, className }) => {
|
||||
return (
|
||||
<label
|
||||
htmlFor={htmlFor}
|
||||
className={twMerge(
|
||||
// Default classes that apply by default
|
||||
"mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400",
|
||||
|
||||
// User-defined className that can override the default margin
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
||||
export default Label;
|
||||
166
src/components/form/MultiSelect.tsx
Normal file
166
src/components/form/MultiSelect.tsx
Normal file
@@ -0,0 +1,166 @@
|
||||
import React, { useState } from "react";
|
||||
|
||||
interface Option {
|
||||
value: string;
|
||||
text: string;
|
||||
selected: boolean;
|
||||
}
|
||||
|
||||
interface MultiSelectProps {
|
||||
label: string;
|
||||
options: Option[];
|
||||
defaultSelected?: string[];
|
||||
onChange?: (selected: string[]) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const MultiSelect: React.FC<MultiSelectProps> = ({
|
||||
label,
|
||||
options,
|
||||
defaultSelected = [],
|
||||
onChange,
|
||||
disabled = false,
|
||||
}) => {
|
||||
const [selectedOptions, setSelectedOptions] =
|
||||
useState<string[]>(defaultSelected);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const toggleDropdown = () => {
|
||||
if (disabled) return;
|
||||
setIsOpen((prev) => !prev);
|
||||
};
|
||||
|
||||
const handleSelect = (optionValue: string) => {
|
||||
const newSelectedOptions = selectedOptions.includes(optionValue)
|
||||
? selectedOptions.filter((value) => value !== optionValue)
|
||||
: [...selectedOptions, optionValue];
|
||||
|
||||
setSelectedOptions(newSelectedOptions);
|
||||
if (onChange) onChange(newSelectedOptions);
|
||||
};
|
||||
|
||||
const removeOption = (index: number, value: string) => {
|
||||
const newSelectedOptions = selectedOptions.filter((opt) => opt !== value);
|
||||
setSelectedOptions(newSelectedOptions);
|
||||
if (onChange) onChange(newSelectedOptions);
|
||||
};
|
||||
|
||||
const selectedValuesText = selectedOptions.map(
|
||||
(value) => options.find((option) => option.value === value)?.text || ""
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<label className="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
|
||||
{label}
|
||||
</label>
|
||||
|
||||
<div className="relative z-20 inline-block w-full">
|
||||
<div className="relative flex flex-col items-center">
|
||||
<div onClick={toggleDropdown} className="w-full">
|
||||
<div className="mb-2 flex h-11 rounded-lg border border-gray-300 py-1.5 pl-3 pr-3 shadow-theme-xs outline-hidden transition focus:border-brand-300 focus:shadow-focus-ring dark:border-gray-700 dark:bg-gray-900 dark:focus:border-brand-300">
|
||||
<div className="flex flex-wrap flex-auto gap-2">
|
||||
{selectedValuesText.length > 0 ? (
|
||||
selectedValuesText.map((text, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="group flex items-center justify-center rounded-full border-[0.7px] border-transparent bg-gray-100 py-1 pl-2.5 pr-2 text-sm text-gray-800 hover:border-gray-200 dark:bg-gray-800 dark:text-white/90 dark:hover:border-gray-800"
|
||||
>
|
||||
<span className="flex-initial max-w-full">{text}</span>
|
||||
<div className="flex flex-row-reverse flex-auto">
|
||||
<div
|
||||
onClick={() =>
|
||||
removeOption(index, selectedOptions[index])
|
||||
}
|
||||
className="pl-2 text-gray-500 cursor-pointer group-hover:text-gray-400 dark:text-gray-400"
|
||||
>
|
||||
<svg
|
||||
className="fill-current"
|
||||
role="button"
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 14 14"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M3.40717 4.46881C3.11428 4.17591 3.11428 3.70104 3.40717 3.40815C3.70006 3.11525 4.17494 3.11525 4.46783 3.40815L6.99943 5.93975L9.53095 3.40822C9.82385 3.11533 10.2987 3.11533 10.5916 3.40822C10.8845 3.70112 10.8845 4.17599 10.5916 4.46888L8.06009 7.00041L10.5916 9.53193C10.8845 9.82482 10.8845 10.2997 10.5916 10.5926C10.2987 10.8855 9.82385 10.8855 9.53095 10.5926L6.99943 8.06107L4.46783 10.5927C4.17494 10.8856 3.70006 10.8856 3.40717 10.5927C3.11428 10.2998 3.11428 9.8249 3.40717 9.53201L5.93877 7.00041L3.40717 4.46881Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<input
|
||||
placeholder="Select option"
|
||||
className="w-full h-full p-1 pr-2 text-sm bg-transparent border-0 outline-hidden appearance-none placeholder:text-gray-800 focus:border-0 focus:outline-hidden focus:ring-0 dark:placeholder:text-white/90"
|
||||
readOnly
|
||||
value="Select option"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center py-1 pl-1 pr-1 w-7">
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleDropdown}
|
||||
className="w-5 h-5 text-gray-700 outline-hidden cursor-pointer focus:outline-hidden dark:text-gray-400"
|
||||
>
|
||||
<svg
|
||||
className={`stroke-current ${isOpen ? "rotate-180" : ""}`}
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M4.79175 7.39551L10.0001 12.6038L15.2084 7.39551"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isOpen && (
|
||||
<div
|
||||
className="absolute left-0 z-40 w-full overflow-y-auto bg-white rounded-lg shadow-sm top-full max-h-select dark:bg-gray-900"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
{options.map((option, index) => (
|
||||
<div key={index}>
|
||||
<div
|
||||
className={`hover:bg-primary/5 w-full cursor-pointer rounded-t border-b border-gray-200 dark:border-gray-800`}
|
||||
onClick={() => handleSelect(option.value)}
|
||||
>
|
||||
<div
|
||||
className={`relative flex w-full items-center p-2 pl-2 ${
|
||||
selectedOptions.includes(option.value)
|
||||
? "bg-primary/10"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
<div className="mx-2 leading-6 text-gray-800 dark:text-white/90">
|
||||
{option.text}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MultiSelect;
|
||||
64
src/components/form/Select.tsx
Normal file
64
src/components/form/Select.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import React, { useState } from "react";
|
||||
|
||||
interface Option {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface SelectProps {
|
||||
options: Option[];
|
||||
placeholder?: string;
|
||||
onChange: (value: string) => void;
|
||||
className?: string;
|
||||
defaultValue?: string;
|
||||
}
|
||||
|
||||
const Select: React.FC<SelectProps> = ({
|
||||
options,
|
||||
placeholder = "Select an option",
|
||||
onChange,
|
||||
className = "",
|
||||
defaultValue = "",
|
||||
}) => {
|
||||
// Manage the selected value
|
||||
const [selectedValue, setSelectedValue] = useState<string>(defaultValue);
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const value = e.target.value;
|
||||
setSelectedValue(value);
|
||||
onChange(value); // Trigger parent handler
|
||||
};
|
||||
|
||||
return (
|
||||
<select
|
||||
className={`h-11 w-full appearance-none rounded-lg border border-gray-300 px-4 py-2.5 pr-11 text-sm shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800 ${
|
||||
selectedValue
|
||||
? "text-gray-800 dark:text-white/90"
|
||||
: "text-gray-400 dark:text-gray-400"
|
||||
} ${className}`}
|
||||
value={selectedValue}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{/* Placeholder option */}
|
||||
<option
|
||||
value=""
|
||||
disabled
|
||||
className="text-gray-700 dark:bg-gray-900 dark:text-gray-400"
|
||||
>
|
||||
{placeholder}
|
||||
</option>
|
||||
{/* Map over options */}
|
||||
{options.map((option) => (
|
||||
<option
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
className="text-gray-700 dark:bg-gray-900 dark:text-gray-400"
|
||||
>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
};
|
||||
|
||||
export default Select;
|
||||
60
src/components/form/date-picker.tsx
Normal file
60
src/components/form/date-picker.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import { useEffect } from 'react';
|
||||
import flatpickr from 'flatpickr';
|
||||
import 'flatpickr/dist/flatpickr.css';
|
||||
import Label from './Label';
|
||||
import { CalenderIcon } from '../../icons';
|
||||
import Hook = flatpickr.Options.Hook;
|
||||
import DateOption = flatpickr.Options.DateOption;
|
||||
|
||||
type PropsType = {
|
||||
id: string;
|
||||
mode?: "single" | "multiple" | "range" | "time";
|
||||
onChange?: Hook | Hook[];
|
||||
defaultDate?: DateOption;
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
};
|
||||
|
||||
export default function DatePicker({
|
||||
id,
|
||||
mode,
|
||||
onChange,
|
||||
label,
|
||||
defaultDate,
|
||||
placeholder,
|
||||
}: PropsType) {
|
||||
useEffect(() => {
|
||||
const flatPickr = flatpickr(`#${id}`, {
|
||||
mode: mode || "single",
|
||||
static: true,
|
||||
monthSelectorType: "static",
|
||||
dateFormat: "Y-m-d",
|
||||
defaultDate,
|
||||
onChange,
|
||||
});
|
||||
|
||||
return () => {
|
||||
if (!Array.isArray(flatPickr)) {
|
||||
flatPickr.destroy();
|
||||
}
|
||||
};
|
||||
}, [mode, onChange, id, defaultDate]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{label && <Label htmlFor={id}>{label}</Label>}
|
||||
|
||||
<div className="relative">
|
||||
<input
|
||||
id={id}
|
||||
placeholder={placeholder}
|
||||
className="h-11 w-full rounded-lg border appearance-none px-4 py-2.5 text-sm shadow-theme-xs placeholder:text-gray-400 focus:outline-hidden focus:ring-3 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 bg-transparent text-gray-800 border-gray-300 focus:border-brand-300 focus:ring-brand-500/20 dark:border-gray-700 dark:focus:border-brand-800"
|
||||
/>
|
||||
|
||||
<span className="absolute text-gray-500 -translate-y-1/2 pointer-events-none right-3 top-1/2 dark:text-gray-400">
|
||||
<CalenderIcon className="size-6" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
37
src/components/form/form-elements/CheckboxComponents.tsx
Normal file
37
src/components/form/form-elements/CheckboxComponents.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
"use client";
|
||||
import React, { useState } from "react";
|
||||
import ComponentCard from "../../common/ComponentCard";
|
||||
import Checkbox from "../input/Checkbox";
|
||||
|
||||
export default function CheckboxComponents() {
|
||||
const [isChecked, setIsChecked] = useState(false);
|
||||
const [isCheckedTwo, setIsCheckedTwo] = useState(true);
|
||||
const [isCheckedDisabled, setIsCheckedDisabled] = useState(false);
|
||||
return (
|
||||
<ComponentCard title="Checkbox">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<Checkbox checked={isChecked} onChange={setIsChecked} />
|
||||
<span className="block text-sm font-medium text-gray-700 dark:text-gray-400">
|
||||
Default
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Checkbox
|
||||
checked={isCheckedTwo}
|
||||
onChange={setIsCheckedTwo}
|
||||
label="Checked"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Checkbox
|
||||
checked={isCheckedDisabled}
|
||||
onChange={setIsCheckedDisabled}
|
||||
disabled
|
||||
label="Disabled"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ComponentCard>
|
||||
);
|
||||
}
|
||||
120
src/components/form/form-elements/DefaultInputs.tsx
Normal file
120
src/components/form/form-elements/DefaultInputs.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
"use client";
|
||||
import React, { useState } from 'react';
|
||||
import ComponentCard from '../../common/ComponentCard';
|
||||
import Label from '../Label';
|
||||
import Input from '../input/InputField';
|
||||
import Select from '../Select';
|
||||
import { ChevronDownIcon, EyeCloseIcon, EyeIcon, TimeIcon } from '../../../icons';
|
||||
import DatePicker from '@/components/form/date-picker';
|
||||
|
||||
export default function DefaultInputs() {
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const options = [
|
||||
{ value: "marketing", label: "Marketing" },
|
||||
{ value: "template", label: "Template" },
|
||||
{ value: "development", label: "Development" },
|
||||
];
|
||||
const handleSelectChange = (value: string) => {
|
||||
console.log("Selected value:", value);
|
||||
};
|
||||
return (
|
||||
<ComponentCard title="Default Inputs">
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<Label>Input</Label>
|
||||
<Input type="text" />
|
||||
</div>
|
||||
<div>
|
||||
<Label>Input with Placeholder</Label>
|
||||
<Input type="text" placeholder="info@gmail.com" />
|
||||
</div>
|
||||
<div>
|
||||
<Label>Select Input</Label>
|
||||
<div className="relative">
|
||||
<Select
|
||||
options={options}
|
||||
placeholder="Select an option"
|
||||
onChange={handleSelectChange}
|
||||
className="dark:bg-dark-900"
|
||||
/>
|
||||
<span className="absolute text-gray-500 -translate-y-1/2 pointer-events-none right-3 top-1/2 dark:text-gray-400">
|
||||
<ChevronDownIcon/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Password Input</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
type={showPassword ? "text" : "password"}
|
||||
placeholder="Enter your password"
|
||||
/>
|
||||
<button
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute z-30 -translate-y-1/2 cursor-pointer right-4 top-1/2"
|
||||
>
|
||||
{showPassword ? (
|
||||
<EyeIcon className="fill-gray-500 dark:fill-gray-400" />
|
||||
) : (
|
||||
<EyeCloseIcon className="fill-gray-500 dark:fill-gray-400" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<DatePicker
|
||||
id="date-picker"
|
||||
label="Date Picker Input"
|
||||
placeholder="Select a date"
|
||||
onChange={(dates, currentDateString) => {
|
||||
// Handle your logic
|
||||
console.log({ dates, currentDateString });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="tm">Time Picker Input</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
type="time"
|
||||
id="tm"
|
||||
name="tm"
|
||||
onChange={(e) => console.log(e.target.value)}
|
||||
/>
|
||||
<span className="absolute text-gray-500 -translate-y-1/2 pointer-events-none right-3 top-1/2 dark:text-gray-400">
|
||||
<TimeIcon />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="tm">Input with Payment</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Card number"
|
||||
className="pl-[62px]"
|
||||
/>
|
||||
<span className="absolute left-0 top-1/2 flex h-11 w-[46px] -translate-y-1/2 items-center justify-center border-r border-gray-200 dark:border-gray-800">
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<circle cx="6.25" cy="10" r="5.625" fill="#E80B26" />
|
||||
<circle cx="13.75" cy="10" r="5.625" fill="#F59D31" />
|
||||
<path
|
||||
d="M10 14.1924C11.1508 13.1625 11.875 11.6657 11.875 9.99979C11.875 8.33383 11.1508 6.8371 10 5.80713C8.84918 6.8371 8.125 8.33383 8.125 9.99979C8.125 11.6657 8.84918 13.1625 10 14.1924Z"
|
||||
fill="#FC6020"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ComponentCard>
|
||||
);
|
||||
}
|
||||
77
src/components/form/form-elements/DropZone.tsx
Normal file
77
src/components/form/form-elements/DropZone.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
import ComponentCard from "../../common/ComponentCard";
|
||||
import { useDropzone } from "react-dropzone";
|
||||
|
||||
const DropzoneComponent: React.FC = () => {
|
||||
const onDrop = (acceptedFiles: File[]) => {
|
||||
console.log("Files dropped:", acceptedFiles);
|
||||
// Handle file uploads here
|
||||
};
|
||||
|
||||
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
||||
onDrop,
|
||||
accept: {
|
||||
"image/png": [],
|
||||
"image/jpeg": [],
|
||||
"image/webp": [],
|
||||
"image/svg+xml": [],
|
||||
},
|
||||
});
|
||||
return (
|
||||
<ComponentCard title="Dropzone">
|
||||
<div className="transition border border-gray-300 border-dashed cursor-pointer dark:hover:border-brand-500 dark:border-gray-700 rounded-xl hover:border-brand-500">
|
||||
<form
|
||||
{...getRootProps()}
|
||||
className={`dropzone rounded-xl border-dashed border-gray-300 p-7 lg:p-10
|
||||
${
|
||||
isDragActive
|
||||
? "border-brand-500 bg-gray-100 dark:bg-gray-800"
|
||||
: "border-gray-300 bg-gray-50 dark:border-gray-700 dark:bg-gray-900"
|
||||
}
|
||||
`}
|
||||
id="demo-upload"
|
||||
>
|
||||
{/* Hidden Input */}
|
||||
<input {...getInputProps()} />
|
||||
|
||||
<div className="dz-message flex flex-col items-center m-0!">
|
||||
{/* Icon Container */}
|
||||
<div className="mb-[22px] flex justify-center">
|
||||
<div className="flex h-[68px] w-[68px] items-center justify-center rounded-full bg-gray-200 text-gray-700 dark:bg-gray-800 dark:text-gray-400">
|
||||
<svg
|
||||
className="fill-current"
|
||||
width="29"
|
||||
height="28"
|
||||
viewBox="0 0 29 28"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M14.5019 3.91699C14.2852 3.91699 14.0899 4.00891 13.953 4.15589L8.57363 9.53186C8.28065 9.82466 8.2805 10.2995 8.5733 10.5925C8.8661 10.8855 9.34097 10.8857 9.63396 10.5929L13.7519 6.47752V18.667C13.7519 19.0812 14.0877 19.417 14.5019 19.417C14.9161 19.417 15.2519 19.0812 15.2519 18.667V6.48234L19.3653 10.5929C19.6583 10.8857 20.1332 10.8855 20.426 10.5925C20.7188 10.2995 20.7186 9.82463 20.4256 9.53184L15.0838 4.19378C14.9463 4.02488 14.7367 3.91699 14.5019 3.91699ZM5.91626 18.667C5.91626 18.2528 5.58047 17.917 5.16626 17.917C4.75205 17.917 4.41626 18.2528 4.41626 18.667V21.8337C4.41626 23.0763 5.42362 24.0837 6.66626 24.0837H22.3339C23.5766 24.0837 24.5839 23.0763 24.5839 21.8337V18.667C24.5839 18.2528 24.2482 17.917 23.8339 17.917C23.4197 17.917 23.0839 18.2528 23.0839 18.667V21.8337C23.0839 22.2479 22.7482 22.5837 22.3339 22.5837H6.66626C6.25205 22.5837 5.91626 22.2479 5.91626 21.8337V18.667Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Text Content */}
|
||||
<h4 className="mb-3 font-semibold text-gray-800 text-theme-xl dark:text-white/90">
|
||||
{isDragActive ? "Drop Files Here" : "Drag & Drop Files Here"}
|
||||
</h4>
|
||||
|
||||
<span className=" text-center mb-5 block w-full max-w-[290px] text-sm text-gray-700 dark:text-gray-400">
|
||||
Drag and drop your PNG, JPG, WebP, SVG images here or browse
|
||||
</span>
|
||||
|
||||
<span className="font-medium underline text-theme-sm text-brand-500">
|
||||
Browse File
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</ComponentCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default DropzoneComponent;
|
||||
23
src/components/form/form-elements/FileInputExample.tsx
Normal file
23
src/components/form/form-elements/FileInputExample.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
import ComponentCard from "../../common/ComponentCard";
|
||||
import FileInput from "../input/FileInput";
|
||||
import Label from "../Label";
|
||||
|
||||
export default function FileInputExample() {
|
||||
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0];
|
||||
if (file) {
|
||||
console.log("Selected file:", file.name);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ComponentCard title="File Input">
|
||||
<div>
|
||||
<Label>Upload file</Label>
|
||||
<FileInput onChange={handleFileChange} className="custom-class" />
|
||||
</div>
|
||||
</ComponentCard>
|
||||
);
|
||||
}
|
||||
56
src/components/form/form-elements/InputGroup.tsx
Normal file
56
src/components/form/form-elements/InputGroup.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
import ComponentCard from "../../common/ComponentCard";
|
||||
import Label from "../Label";
|
||||
import Input from "../input/InputField";
|
||||
import { EnvelopeIcon } from "../../../icons";
|
||||
import PhoneInput from "../group-input/PhoneInput";
|
||||
|
||||
export default function InputGroup() {
|
||||
const countries = [
|
||||
{ code: "US", label: "+1" },
|
||||
{ code: "GB", label: "+44" },
|
||||
{ code: "CA", label: "+1" },
|
||||
{ code: "AU", label: "+61" },
|
||||
];
|
||||
const handlePhoneNumberChange = (phoneNumber: string) => {
|
||||
console.log("Updated phone number:", phoneNumber);
|
||||
};
|
||||
return (
|
||||
<ComponentCard title="Input Group">
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<Label>Email</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
placeholder="info@gmail.com"
|
||||
type="text"
|
||||
className="pl-[62px]"
|
||||
/>
|
||||
<span className="absolute left-0 top-1/2 -translate-y-1/2 border-r border-gray-200 px-3.5 py-3 text-gray-500 dark:border-gray-800 dark:text-gray-400">
|
||||
<EnvelopeIcon />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Phone</Label>
|
||||
<PhoneInput
|
||||
selectPosition="start"
|
||||
countries={countries}
|
||||
placeholder="+1 (555) 000-0000"
|
||||
onChange={handlePhoneNumberChange}
|
||||
/>
|
||||
</div>{" "}
|
||||
<div>
|
||||
<Label>Phone</Label>
|
||||
<PhoneInput
|
||||
selectPosition="end"
|
||||
countries={countries}
|
||||
placeholder="+1 (555) 000-0000"
|
||||
onChange={handlePhoneNumberChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ComponentCard>
|
||||
);
|
||||
}
|
||||
70
src/components/form/form-elements/InputStates.tsx
Normal file
70
src/components/form/form-elements/InputStates.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
"use client";
|
||||
import React, { useState } from "react";
|
||||
import ComponentCard from "../../common/ComponentCard";
|
||||
import Input from "../input/InputField";
|
||||
import Label from "../Label";
|
||||
|
||||
export default function InputStates() {
|
||||
const [email, setEmail] = useState("");
|
||||
const [error, setError] = useState(false);
|
||||
|
||||
// Simulate a validation check
|
||||
const validateEmail = (value: string) => {
|
||||
const isValidEmail =
|
||||
/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(value);
|
||||
setError(!isValidEmail);
|
||||
return isValidEmail;
|
||||
};
|
||||
|
||||
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value;
|
||||
setEmail(value);
|
||||
validateEmail(value);
|
||||
};
|
||||
return (
|
||||
<ComponentCard
|
||||
title="Input States"
|
||||
desc="Validation styles for error, success and disabled states on form controls."
|
||||
>
|
||||
<div className="space-y-5 sm:space-y-6">
|
||||
{/* Error Input */}
|
||||
<div>
|
||||
<Label>Email</Label>
|
||||
<Input
|
||||
type="email"
|
||||
defaultValue={email}
|
||||
error={error}
|
||||
onChange={handleEmailChange}
|
||||
placeholder="Enter your email"
|
||||
hint={error ? "This is an invalid email address." : ""}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Success Input */}
|
||||
<div>
|
||||
<Label>Email</Label>
|
||||
<Input
|
||||
type="email"
|
||||
defaultValue={email}
|
||||
success={!error}
|
||||
onChange={handleEmailChange}
|
||||
placeholder="Enter your email"
|
||||
hint={!error ? "Valid email!" : ""}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Disabled Input */}
|
||||
<div>
|
||||
<Label>Email</Label>
|
||||
<Input
|
||||
type="text"
|
||||
defaultValue="disabled@example.com"
|
||||
disabled={true}
|
||||
placeholder="Disabled email"
|
||||
hint="This field is disabled."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ComponentCard>
|
||||
);
|
||||
}
|
||||
43
src/components/form/form-elements/RadioButtons.tsx
Normal file
43
src/components/form/form-elements/RadioButtons.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
"use client";
|
||||
import React, { useState } from "react";
|
||||
import ComponentCard from "../../common/ComponentCard";
|
||||
import Radio from "../input/Radio";
|
||||
|
||||
export default function RadioButtons() {
|
||||
const [selectedValue, setSelectedValue] = useState<string>("option2");
|
||||
|
||||
const handleRadioChange = (value: string) => {
|
||||
setSelectedValue(value);
|
||||
};
|
||||
return (
|
||||
<ComponentCard title="Radio Buttons">
|
||||
<div className="flex flex-wrap items-center gap-8">
|
||||
<Radio
|
||||
id="radio1"
|
||||
name="group1"
|
||||
value="option1"
|
||||
checked={selectedValue === "option1"}
|
||||
onChange={handleRadioChange}
|
||||
label="Default"
|
||||
/>
|
||||
<Radio
|
||||
id="radio2"
|
||||
name="group1"
|
||||
value="option2"
|
||||
checked={selectedValue === "option2"}
|
||||
onChange={handleRadioChange}
|
||||
label="Selected"
|
||||
/>
|
||||
<Radio
|
||||
id="radio3"
|
||||
name="group1"
|
||||
value="option3"
|
||||
checked={selectedValue === "option3"}
|
||||
onChange={handleRadioChange}
|
||||
label="Disabled"
|
||||
disabled={true}
|
||||
/>
|
||||
</div>
|
||||
</ComponentCard>
|
||||
);
|
||||
}
|
||||
61
src/components/form/form-elements/SelectInputs.tsx
Normal file
61
src/components/form/form-elements/SelectInputs.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
"use client";
|
||||
import React, { useState } from "react";
|
||||
import ComponentCard from "../../common/ComponentCard";
|
||||
import Label from "../Label";
|
||||
import Select from "../Select";
|
||||
import MultiSelect from "../MultiSelect";
|
||||
import { ChevronDownIcon } from "@/icons";
|
||||
|
||||
export default function SelectInputs() {
|
||||
const options = [
|
||||
{ value: "marketing", label: "Marketing" },
|
||||
{ value: "template", label: "Template" },
|
||||
{ value: "development", label: "Development" },
|
||||
];
|
||||
|
||||
const [selectedValues, setSelectedValues] = useState<string[]>([]);
|
||||
|
||||
const handleSelectChange = (value: string) => {
|
||||
console.log("Selected value:", value);
|
||||
};
|
||||
|
||||
const multiOptions = [
|
||||
{ value: "1", text: "Option 1", selected: false },
|
||||
{ value: "2", text: "Option 2", selected: false },
|
||||
{ value: "3", text: "Option 3", selected: false },
|
||||
{ value: "4", text: "Option 4", selected: false },
|
||||
{ value: "5", text: "Option 5", selected: false },
|
||||
];
|
||||
|
||||
return (
|
||||
<ComponentCard title="Select Inputs">
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<Label>Select Input</Label>
|
||||
<div className="relative">
|
||||
<Select
|
||||
options={options}
|
||||
placeholder="Select Option"
|
||||
onChange={handleSelectChange}
|
||||
className="dark:bg-dark-900"
|
||||
/>
|
||||
<span className="absolute text-gray-500 -translate-y-1/2 pointer-events-none right-3 top-1/2 dark:text-gray-400">
|
||||
<ChevronDownIcon/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<MultiSelect
|
||||
label="Multiple Select Options"
|
||||
options={multiOptions}
|
||||
defaultSelected={["1", "3"]}
|
||||
onChange={(values) => setSelectedValues(values)}
|
||||
/>
|
||||
<p className="sr-only">
|
||||
Selected Values: {selectedValues.join(", ")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</ComponentCard>
|
||||
);
|
||||
}
|
||||
43
src/components/form/form-elements/TextAreaInput.tsx
Normal file
43
src/components/form/form-elements/TextAreaInput.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
"use client";
|
||||
import React, { useState } from "react";
|
||||
import ComponentCard from "../../common/ComponentCard";
|
||||
import TextArea from "../input/TextArea";
|
||||
import Label from "../Label";
|
||||
|
||||
export default function TextAreaInput() {
|
||||
const [message, setMessage] = useState("");
|
||||
const [messageTwo, setMessageTwo] = useState("");
|
||||
return (
|
||||
<ComponentCard title="Textarea input field">
|
||||
<div className="space-y-6">
|
||||
{/* Default TextArea */}
|
||||
<div>
|
||||
<Label>Description</Label>
|
||||
<TextArea
|
||||
value={message}
|
||||
onChange={(value) => setMessage(value)}
|
||||
rows={6}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Disabled TextArea */}
|
||||
<div>
|
||||
<Label>Description</Label>
|
||||
<TextArea rows={6} disabled />
|
||||
</div>
|
||||
|
||||
{/* Error TextArea */}
|
||||
<div>
|
||||
<Label>Description</Label>
|
||||
<TextArea
|
||||
rows={6}
|
||||
value={messageTwo}
|
||||
error
|
||||
onChange={(value) => setMessageTwo(value)}
|
||||
hint="Please enter a valid message."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ComponentCard>
|
||||
);
|
||||
}
|
||||
42
src/components/form/form-elements/ToggleSwitch.tsx
Normal file
42
src/components/form/form-elements/ToggleSwitch.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
import ComponentCard from "../../common/ComponentCard";
|
||||
import Switch from "../switch/Switch";
|
||||
|
||||
export default function ToggleSwitch() {
|
||||
const handleSwitchChange = (checked: boolean) => {
|
||||
console.log("Switch is now:", checked ? "ON" : "OFF");
|
||||
};
|
||||
return (
|
||||
<ComponentCard title="Toggle switch input">
|
||||
<div className="flex gap-4">
|
||||
<Switch
|
||||
label="Default"
|
||||
defaultChecked={true}
|
||||
onChange={handleSwitchChange}
|
||||
/>
|
||||
<Switch
|
||||
label="Checked"
|
||||
defaultChecked={true}
|
||||
onChange={handleSwitchChange}
|
||||
/>
|
||||
<Switch label="Disabled" disabled={true} />
|
||||
</div>{" "}
|
||||
<div className="flex gap-4">
|
||||
<Switch
|
||||
label="Default"
|
||||
defaultChecked={true}
|
||||
onChange={handleSwitchChange}
|
||||
color="gray"
|
||||
/>
|
||||
<Switch
|
||||
label="Checked"
|
||||
defaultChecked={true}
|
||||
onChange={handleSwitchChange}
|
||||
color="gray"
|
||||
/>
|
||||
<Switch label="Disabled" disabled={true} color="gray" />
|
||||
</div>
|
||||
</ComponentCard>
|
||||
);
|
||||
}
|
||||
141
src/components/form/group-input/PhoneInput.tsx
Normal file
141
src/components/form/group-input/PhoneInput.tsx
Normal file
@@ -0,0 +1,141 @@
|
||||
"use client";
|
||||
import React, { useState } from "react";
|
||||
|
||||
interface CountryCode {
|
||||
code: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface PhoneInputProps {
|
||||
countries: CountryCode[];
|
||||
placeholder?: string;
|
||||
onChange?: (phoneNumber: string) => void;
|
||||
selectPosition?: "start" | "end"; // New prop for dropdown position
|
||||
}
|
||||
|
||||
const PhoneInput: React.FC<PhoneInputProps> = ({
|
||||
countries,
|
||||
placeholder = "+1 (555) 000-0000",
|
||||
onChange,
|
||||
selectPosition = "start", // Default position is 'start'
|
||||
}) => {
|
||||
const [selectedCountry, setSelectedCountry] = useState<string>("US");
|
||||
const [phoneNumber, setPhoneNumber] = useState<string>("+1");
|
||||
|
||||
const countryCodes: Record<string, string> = countries.reduce(
|
||||
(acc, { code, label }) => ({ ...acc, [code]: label }),
|
||||
{}
|
||||
);
|
||||
|
||||
const handleCountryChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const newCountry = e.target.value;
|
||||
setSelectedCountry(newCountry);
|
||||
setPhoneNumber(countryCodes[newCountry]);
|
||||
if (onChange) {
|
||||
onChange(countryCodes[newCountry]);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePhoneNumberChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newPhoneNumber = e.target.value;
|
||||
setPhoneNumber(newPhoneNumber);
|
||||
if (onChange) {
|
||||
onChange(newPhoneNumber);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative flex">
|
||||
{/* Dropdown position: Start */}
|
||||
{selectPosition === "start" && (
|
||||
<div className="absolute">
|
||||
<select
|
||||
value={selectedCountry}
|
||||
onChange={handleCountryChange}
|
||||
className="appearance-none bg-none rounded-l-lg border-0 border-r border-gray-200 bg-transparent py-3 pl-3.5 pr-8 leading-tight text-gray-700 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-800 dark:text-gray-400"
|
||||
>
|
||||
{countries.map((country) => (
|
||||
<option
|
||||
key={country.code}
|
||||
value={country.code}
|
||||
className="text-gray-700 dark:bg-gray-900 dark:text-gray-400"
|
||||
>
|
||||
{country.code}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="absolute inset-y-0 flex items-center text-gray-700 pointer-events-none bg-none right-3 dark:text-gray-400">
|
||||
<svg
|
||||
className="stroke-current"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M4.79175 7.396L10.0001 12.6043L15.2084 7.396"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Input field */}
|
||||
<input
|
||||
type="tel"
|
||||
value={phoneNumber}
|
||||
onChange={handlePhoneNumberChange}
|
||||
placeholder={placeholder}
|
||||
className={`dark:bg-dark-900 h-11 w-full ${
|
||||
selectPosition === "start" ? "pl-[84px]" : "pr-[84px]"
|
||||
} rounded-lg border border-gray-300 bg-transparent py-3 px-4 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800`}
|
||||
/>
|
||||
|
||||
{/* Dropdown position: End */}
|
||||
{selectPosition === "end" && (
|
||||
<div className="absolute right-0">
|
||||
<select
|
||||
value={selectedCountry}
|
||||
onChange={handleCountryChange}
|
||||
className="appearance-none bg-none rounded-r-lg border-0 border-l border-gray-200 bg-transparent py-3 pl-3.5 pr-8 leading-tight text-gray-700 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-800 dark:text-gray-400"
|
||||
>
|
||||
{countries.map((country) => (
|
||||
<option
|
||||
key={country.code}
|
||||
value={country.code}
|
||||
className="text-gray-700 dark:bg-gray-900 dark:text-gray-400"
|
||||
>
|
||||
{country.code}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="absolute inset-y-0 flex items-center text-gray-700 pointer-events-none right-3 dark:text-gray-400">
|
||||
<svg
|
||||
className="stroke-current"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M4.79175 7.396L10.0001 12.6043L15.2084 7.396"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PhoneInput;
|
||||
82
src/components/form/input/Checkbox.tsx
Normal file
82
src/components/form/input/Checkbox.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import type React from "react";
|
||||
|
||||
interface CheckboxProps {
|
||||
label?: string;
|
||||
checked: boolean;
|
||||
className?: string;
|
||||
id?: string;
|
||||
onChange: (checked: boolean) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const Checkbox: React.FC<CheckboxProps> = ({
|
||||
label,
|
||||
checked,
|
||||
id,
|
||||
onChange,
|
||||
className = "",
|
||||
disabled = false,
|
||||
}) => {
|
||||
return (
|
||||
<label
|
||||
className={`flex items-center space-x-3 group cursor-pointer ${
|
||||
disabled ? "cursor-not-allowed opacity-60" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="relative w-5 h-5">
|
||||
<input
|
||||
id={id}
|
||||
type="checkbox"
|
||||
className={`w-5 h-5 appearance-none cursor-pointer dark:border-gray-700 border border-gray-300 checked:border-transparent rounded-md checked:bg-brand-500 disabled:opacity-60
|
||||
${className}`}
|
||||
checked={checked}
|
||||
onChange={(e) => onChange(e.target.checked)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
{checked && (
|
||||
<svg
|
||||
className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 pointer-events-none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 14 14"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M11.6666 3.5L5.24992 9.91667L2.33325 7"
|
||||
stroke="white"
|
||||
strokeWidth="1.94437"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
{disabled && (
|
||||
<svg
|
||||
className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 pointer-events-none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 14 14"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M11.6666 3.5L5.24992 9.91667L2.33325 7"
|
||||
stroke="#E4E7EC"
|
||||
strokeWidth="2.33333"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
{label && (
|
||||
<span className="text-sm font-medium text-gray-800 dark:text-gray-200">
|
||||
{label}
|
||||
</span>
|
||||
)}
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
||||
export default Checkbox;
|
||||
18
src/components/form/input/FileInput.tsx
Normal file
18
src/components/form/input/FileInput.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import React, { FC } from "react";
|
||||
|
||||
interface FileInputProps {
|
||||
className?: string;
|
||||
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
}
|
||||
|
||||
const FileInput: FC<FileInputProps> = ({ className, onChange }) => {
|
||||
return (
|
||||
<input
|
||||
type="file"
|
||||
className={`focus:border-ring-brand-300 h-11 w-full overflow-hidden rounded-lg border border-gray-300 bg-transparent text-sm text-gray-500 shadow-theme-xs transition-colors file:mr-5 file:border-collapse file:cursor-pointer file:rounded-l-lg file:border-0 file:border-r file:border-solid file:border-gray-200 file:bg-gray-50 file:py-3 file:pl-3.5 file:pr-3 file:text-sm file:text-gray-700 placeholder:text-gray-400 hover:file:bg-gray-100 focus:outline-hidden focus:file:ring-brand-300 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-400 dark:text-white/90 dark:file:border-gray-800 dark:file:bg-white/[0.03] dark:file:text-gray-400 dark:placeholder:text-gray-400 ${className}`}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default FileInput;
|
||||
87
src/components/form/input/InputField.tsx
Normal file
87
src/components/form/input/InputField.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import React, { FC } from "react";
|
||||
|
||||
interface InputProps {
|
||||
type?: "text" | "number" | "email" | "password" | "date" | "time" | string;
|
||||
id?: string;
|
||||
name?: string;
|
||||
placeholder?: string;
|
||||
defaultValue?: string | number;
|
||||
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
className?: string;
|
||||
min?: string;
|
||||
max?: string;
|
||||
step?: number;
|
||||
disabled?: boolean;
|
||||
success?: boolean;
|
||||
error?: boolean;
|
||||
hint?: string; // Optional hint text
|
||||
autoComplete?: string;
|
||||
}
|
||||
|
||||
const Input: FC<InputProps> = ({
|
||||
type = "text",
|
||||
id,
|
||||
name,
|
||||
placeholder,
|
||||
defaultValue,
|
||||
onChange,
|
||||
className = "",
|
||||
min,
|
||||
max,
|
||||
step,
|
||||
disabled = false,
|
||||
success = false,
|
||||
error = false,
|
||||
hint,
|
||||
autoComplete
|
||||
}) => {
|
||||
// Determine input styles based on state (disabled, success, error)
|
||||
let inputClasses = `h-11 w-full rounded-lg border appearance-none px-4 py-2.5 text-sm shadow-theme-xs placeholder:text-gray-400 focus:outline-hidden focus:ring-3 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800 ${className}`;
|
||||
|
||||
// Add styles for the different states
|
||||
if (disabled) {
|
||||
inputClasses += ` text-gray-500 border-gray-300 cursor-not-allowed dark:bg-gray-800 dark:text-gray-400 dark:border-gray-700`;
|
||||
} else if (error) {
|
||||
inputClasses += ` text-error-800 border-error-500 focus:ring-3 focus:ring-error-500/10 dark:text-error-400 dark:border-error-500`;
|
||||
} else if (success) {
|
||||
inputClasses += ` text-success-500 border-success-400 focus:ring-success-500/10 focus:border-success-300 dark:text-success-400 dark:border-success-500`;
|
||||
} else {
|
||||
inputClasses += ` bg-transparent text-gray-800 border-gray-300 focus:border-brand-300 focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:focus:border-brand-800`;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<input
|
||||
type={type}
|
||||
id={id}
|
||||
name={name}
|
||||
placeholder={placeholder}
|
||||
defaultValue={defaultValue}
|
||||
onChange={onChange}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
disabled={disabled}
|
||||
className={inputClasses}
|
||||
autoComplete={autoComplete}
|
||||
/>
|
||||
|
||||
{/* Optional Hint Text */}
|
||||
{hint && (
|
||||
<p
|
||||
className={`mt-1.5 text-xs ${
|
||||
error
|
||||
? "text-error-500"
|
||||
: success
|
||||
? "text-success-500"
|
||||
: "text-gray-500"
|
||||
}`}
|
||||
>
|
||||
{hint}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Input;
|
||||
65
src/components/form/input/Radio.tsx
Normal file
65
src/components/form/input/Radio.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import React from "react";
|
||||
|
||||
interface RadioProps {
|
||||
id: string; // Unique ID for the radio button
|
||||
name: string; // Radio group name
|
||||
value: string; // Value of the radio button
|
||||
checked: boolean; // Whether the radio button is checked
|
||||
label: string; // Label for the radio button
|
||||
onChange: (value: string) => void; // Handler for value change
|
||||
className?: string; // Optional additional classes
|
||||
disabled?: boolean; // Optional disabled state for the radio button
|
||||
}
|
||||
|
||||
const Radio: React.FC<RadioProps> = ({
|
||||
id,
|
||||
name,
|
||||
value,
|
||||
checked,
|
||||
label,
|
||||
onChange,
|
||||
className = "",
|
||||
disabled = false,
|
||||
}) => {
|
||||
return (
|
||||
<label
|
||||
htmlFor={id}
|
||||
className={`relative flex cursor-pointer select-none items-center gap-3 text-sm font-medium ${
|
||||
disabled
|
||||
? "text-gray-300 dark:text-gray-600 cursor-not-allowed"
|
||||
: "text-gray-700 dark:text-gray-400"
|
||||
} ${className}`}
|
||||
>
|
||||
<input
|
||||
id={id}
|
||||
name={name}
|
||||
type="radio"
|
||||
value={value}
|
||||
checked={checked}
|
||||
onChange={() => !disabled && onChange(value)} // Prevent onChange when disabled
|
||||
className="sr-only"
|
||||
disabled={disabled} // Disable input
|
||||
/>
|
||||
<span
|
||||
className={`flex h-5 w-5 items-center justify-center rounded-full border-[1.25px] ${
|
||||
checked
|
||||
? "border-brand-500 bg-brand-500"
|
||||
: "bg-transparent border-gray-300 dark:border-gray-700"
|
||||
} ${
|
||||
disabled
|
||||
? "bg-gray-100 dark:bg-gray-700 border-gray-200 dark:border-gray-700"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`h-2 w-2 rounded-full bg-white ${
|
||||
checked ? "block" : "hidden"
|
||||
}`}
|
||||
></span>
|
||||
</span>
|
||||
{label}
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
||||
export default Radio;
|
||||
59
src/components/form/input/RadioSm.tsx
Normal file
59
src/components/form/input/RadioSm.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import React from "react";
|
||||
|
||||
interface RadioProps {
|
||||
id: string; // Unique ID for the radio button
|
||||
name: string; // Group name for the radio button
|
||||
value: string; // Value of the radio button
|
||||
checked: boolean; // Whether the radio button is checked
|
||||
label: string; // Label text for the radio button
|
||||
onChange: (value: string) => void; // Handler for when the radio button is toggled
|
||||
className?: string; // Optional custom classes for styling
|
||||
}
|
||||
|
||||
const RadioSm: React.FC<RadioProps> = ({
|
||||
id,
|
||||
name,
|
||||
value,
|
||||
checked,
|
||||
label,
|
||||
onChange,
|
||||
className = "",
|
||||
}) => {
|
||||
return (
|
||||
<label
|
||||
htmlFor={id}
|
||||
className={`flex cursor-pointer select-none items-center text-sm text-gray-500 dark:text-gray-400 ${className}`}
|
||||
>
|
||||
<span className="relative">
|
||||
{/* Hidden Input */}
|
||||
<input
|
||||
type="radio"
|
||||
id={id}
|
||||
name={name}
|
||||
value={value}
|
||||
checked={checked}
|
||||
onChange={() => onChange(value)}
|
||||
className="sr-only"
|
||||
/>
|
||||
{/* Styled Radio Circle */}
|
||||
<span
|
||||
className={`mr-2 flex h-4 w-4 items-center justify-center rounded-full border ${
|
||||
checked
|
||||
? "border-brand-500 bg-brand-500"
|
||||
: "bg-transparent border-gray-300 dark:border-gray-700"
|
||||
}`}
|
||||
>
|
||||
{/* Inner Dot */}
|
||||
<span
|
||||
className={`h-1.5 w-1.5 rounded-full ${
|
||||
checked ? "bg-white" : "bg-white dark:bg-[#1e2636]"
|
||||
}`}
|
||||
></span>
|
||||
</span>
|
||||
</span>
|
||||
{label}
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
||||
export default RadioSm;
|
||||
63
src/components/form/input/TextArea.tsx
Normal file
63
src/components/form/input/TextArea.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import React from "react";
|
||||
|
||||
interface TextareaProps {
|
||||
placeholder?: string; // Placeholder text
|
||||
rows?: number; // Number of rows
|
||||
value?: string; // Current value
|
||||
onChange?: (value: string) => void; // Change handler
|
||||
className?: string; // Additional CSS classes
|
||||
disabled?: boolean; // Disabled state
|
||||
error?: boolean; // Error state
|
||||
hint?: string; // Hint text to display
|
||||
}
|
||||
|
||||
const TextArea: React.FC<TextareaProps> = ({
|
||||
placeholder = "Enter your message", // Default placeholder
|
||||
rows = 3, // Default number of rows
|
||||
value = "", // Default value
|
||||
onChange, // Callback for changes
|
||||
className = "", // Additional custom styles
|
||||
disabled = false, // Disabled state
|
||||
error = false, // Error state
|
||||
hint = "", // Default hint text
|
||||
}) => {
|
||||
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
if (onChange) {
|
||||
onChange(e.target.value);
|
||||
}
|
||||
};
|
||||
|
||||
let textareaClasses = `w-full rounded-lg border px-4 py-2.5 text-sm shadow-theme-xs focus:outline-hidden ${className}`;
|
||||
|
||||
if (disabled) {
|
||||
textareaClasses += ` bg-gray-100 opacity-50 text-gray-500 border-gray-300 cursor-not-allowed dark:bg-gray-800 dark:text-gray-400 dark:border-gray-700`;
|
||||
} else if (error) {
|
||||
textareaClasses += ` bg-transparent text-gray-400 border-gray-300 focus:border-error-300 focus:ring-3 focus:ring-error-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:focus:border-error-800`;
|
||||
} else {
|
||||
textareaClasses += ` bg-transparent text-gray-400 border-gray-300 focus:border-brand-300 focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:focus:border-brand-800`;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<textarea
|
||||
placeholder={placeholder}
|
||||
rows={rows}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
disabled={disabled}
|
||||
className={textareaClasses}
|
||||
/>
|
||||
{hint && (
|
||||
<p
|
||||
className={`mt-2 text-sm ${
|
||||
error ? "text-error-500" : "text-gray-500 dark:text-gray-400"
|
||||
}`}
|
||||
>
|
||||
{hint}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TextArea;
|
||||
98
src/components/form/role-upgrade/Editor.tsx
Normal file
98
src/components/form/role-upgrade/Editor.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import dynamic from "next/dynamic";
|
||||
import "react-quill-new/dist/quill.snow.css";
|
||||
|
||||
const ReactQuillEditor = dynamic(
|
||||
async () => {
|
||||
const { default: RQ } = await import("react-quill-new");
|
||||
return ({ forwardedRef, ...props }: any) => (
|
||||
<RQ ref={forwardedRef} {...props} />
|
||||
);
|
||||
},
|
||||
{
|
||||
ssr: false,
|
||||
loading: () => (
|
||||
<div className="h-[480px] w-full animate-pulse bg-gray-100 rounded-lg" />
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
interface EditorProps {
|
||||
value: string;
|
||||
onChange: (content: string) => void;
|
||||
handleImageUpload?: (file: File) => Promise<string>;
|
||||
onEditorReady?: () => void;
|
||||
}
|
||||
|
||||
const RichTextEditor = ({
|
||||
value,
|
||||
onChange,
|
||||
handleImageUpload,
|
||||
onEditorReady,
|
||||
}: EditorProps) => {
|
||||
const quillRef = useRef<any>(null);
|
||||
|
||||
const modules = {
|
||||
toolbar: [
|
||||
[{ font: [] }, { size: [] }],
|
||||
["bold", "italic", "underline", "strike"],
|
||||
[{ color: [] }, { background: [] }],
|
||||
[{ script: "sub" }, { script: "super" }],
|
||||
[{ header: [1, 2, 3, 4, 5, 6, false] }],
|
||||
["blockquote", "code-block"],
|
||||
[
|
||||
{ list: "ordered" },
|
||||
{ list: "bullet" },
|
||||
{ indent: "-1" },
|
||||
{ indent: "+1" },
|
||||
],
|
||||
[{ direction: "rtl" }, { align: [] }],
|
||||
["link", "image", "video", "formula"],
|
||||
["clean"],
|
||||
],
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (quillRef.current && handleImageUpload) {
|
||||
const quill = quillRef.current.getEditor();
|
||||
const toolbar = quill.getModule("toolbar");
|
||||
toolbar.addHandler("image", () => {
|
||||
const input = document.createElement("input");
|
||||
input.setAttribute("type", "file");
|
||||
input.setAttribute("accept", "image/*");
|
||||
input.click();
|
||||
input.onchange = async () => {
|
||||
const file = input.files?.[0];
|
||||
if (file) {
|
||||
try {
|
||||
const url = await handleImageUpload(file);
|
||||
const range = quill.getSelection();
|
||||
quill.insertEmbed(range?.index || 0, "image", url);
|
||||
} catch (error) {
|
||||
console.error("Upload failed", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
onEditorReady?.();
|
||||
}, [handleImageUpload]);
|
||||
|
||||
return (
|
||||
<div className="editor-container bg-white dark:bg-gray-900 rounded-xl overflow-hidden border border-gray-200 dark:border-gray-800">
|
||||
<ReactQuillEditor
|
||||
forwardedRef={quillRef}
|
||||
theme="snow"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
modules={modules}
|
||||
className="min-h-[300px]"
|
||||
placeholder="Nhập nội dung tại đây..."
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RichTextEditor;
|
||||
73
src/components/form/switch/Switch.tsx
Normal file
73
src/components/form/switch/Switch.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
"use client";
|
||||
import React, { useState } from "react";
|
||||
|
||||
interface SwitchProps {
|
||||
label: string;
|
||||
defaultChecked?: boolean;
|
||||
disabled?: boolean;
|
||||
onChange?: (checked: boolean) => void;
|
||||
color?: "blue" | "gray"; // Added prop to toggle color theme
|
||||
}
|
||||
|
||||
const Switch: React.FC<SwitchProps> = ({
|
||||
label,
|
||||
defaultChecked = false,
|
||||
disabled = false,
|
||||
onChange,
|
||||
color = "blue", // Default to blue color
|
||||
}) => {
|
||||
const [isChecked, setIsChecked] = useState(defaultChecked);
|
||||
|
||||
const handleToggle = () => {
|
||||
if (disabled) return;
|
||||
const newCheckedState = !isChecked;
|
||||
setIsChecked(newCheckedState);
|
||||
if (onChange) {
|
||||
onChange(newCheckedState);
|
||||
}
|
||||
};
|
||||
|
||||
const switchColors =
|
||||
color === "blue"
|
||||
? {
|
||||
background: isChecked
|
||||
? "bg-brand-500 "
|
||||
: "bg-gray-200 dark:bg-white/10", // Blue version
|
||||
knob: isChecked
|
||||
? "translate-x-full bg-white"
|
||||
: "translate-x-0 bg-white",
|
||||
}
|
||||
: {
|
||||
background: isChecked
|
||||
? "bg-gray-800 dark:bg-white/10"
|
||||
: "bg-gray-200 dark:bg-white/10", // Gray version
|
||||
knob: isChecked
|
||||
? "translate-x-full bg-white"
|
||||
: "translate-x-0 bg-white",
|
||||
};
|
||||
|
||||
return (
|
||||
<label
|
||||
className={`flex cursor-pointer select-none items-center gap-3 text-sm font-medium ${
|
||||
disabled ? "text-gray-400" : "text-gray-700 dark:text-gray-400"
|
||||
}`}
|
||||
onClick={handleToggle} // Toggle when the label itself is clicked
|
||||
>
|
||||
<div className="relative">
|
||||
<div
|
||||
className={`block transition duration-150 ease-linear h-6 w-11 rounded-full ${
|
||||
disabled
|
||||
? "bg-gray-100 pointer-events-none dark:bg-gray-800"
|
||||
: switchColors.background
|
||||
}`}
|
||||
></div>
|
||||
<div
|
||||
className={`absolute left-0.5 top-0.5 h-5 w-5 rounded-full shadow-theme-sm duration-150 ease-linear transform ${switchColors.knob}`}
|
||||
></div>
|
||||
</div>
|
||||
{label}
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
||||
export default Switch;
|
||||
384
src/components/header/NotificationDropdown.tsx
Normal file
384
src/components/header/NotificationDropdown.tsx
Normal file
@@ -0,0 +1,384 @@
|
||||
"use client";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import React, { useState } from "react";
|
||||
import { Dropdown } from "../ui/dropdown/Dropdown";
|
||||
import { DropdownItem } from "../ui/dropdown/DropdownItem";
|
||||
|
||||
export default function NotificationDropdown() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [notifying, setNotifying] = useState(true);
|
||||
|
||||
function toggleDropdown() {
|
||||
setIsOpen(!isOpen);
|
||||
}
|
||||
|
||||
function closeDropdown() {
|
||||
setIsOpen(false);
|
||||
}
|
||||
|
||||
const handleClick = () => {
|
||||
toggleDropdown();
|
||||
setNotifying(false);
|
||||
};
|
||||
return (
|
||||
<div className="relative">
|
||||
<button
|
||||
className="relative dropdown-toggle flex items-center justify-center text-gray-500 transition-colors bg-white border border-gray-200 rounded-full hover:text-gray-700 h-11 w-11 hover:bg-gray-100 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-white"
|
||||
onClick={handleClick}
|
||||
>
|
||||
<span
|
||||
className={`absolute right-0 top-0.5 z-10 h-2 w-2 rounded-full bg-orange-400 ${
|
||||
!notifying ? "hidden" : "flex"
|
||||
}`}
|
||||
>
|
||||
<span className="absolute inline-flex w-full h-full bg-orange-400 rounded-full opacity-75 animate-ping"></span>
|
||||
</span>
|
||||
<svg
|
||||
className="fill-current"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M10.75 2.29248C10.75 1.87827 10.4143 1.54248 10 1.54248C9.58583 1.54248 9.25004 1.87827 9.25004 2.29248V2.83613C6.08266 3.20733 3.62504 5.9004 3.62504 9.16748V14.4591H3.33337C2.91916 14.4591 2.58337 14.7949 2.58337 15.2091C2.58337 15.6234 2.91916 15.9591 3.33337 15.9591H4.37504H15.625H16.6667C17.0809 15.9591 17.4167 15.6234 17.4167 15.2091C17.4167 14.7949 17.0809 14.4591 16.6667 14.4591H16.375V9.16748C16.375 5.9004 13.9174 3.20733 10.75 2.83613V2.29248ZM14.875 14.4591V9.16748C14.875 6.47509 12.6924 4.29248 10 4.29248C7.30765 4.29248 5.12504 6.47509 5.12504 9.16748V14.4591H14.875ZM8.00004 17.7085C8.00004 18.1228 8.33583 18.4585 8.75004 18.4585H11.25C11.6643 18.4585 12 18.1228 12 17.7085C12 17.2943 11.6643 16.9585 11.25 16.9585H8.75004C8.33583 16.9585 8.00004 17.2943 8.00004 17.7085Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<Dropdown
|
||||
isOpen={isOpen}
|
||||
onClose={closeDropdown}
|
||||
className="absolute -right-[240px] mt-[17px] flex h-[480px] w-[350px] flex-col rounded-2xl border border-gray-200 bg-white p-3 shadow-theme-lg dark:border-gray-800 dark:bg-gray-dark sm:w-[361px] lg:right-0"
|
||||
>
|
||||
<div className="flex items-center justify-between pb-3 mb-3 border-b border-gray-100 dark:border-gray-700">
|
||||
<h5 className="text-lg font-semibold text-gray-800 dark:text-gray-200">
|
||||
Notification
|
||||
</h5>
|
||||
<button
|
||||
onClick={toggleDropdown}
|
||||
className="text-gray-500 transition dropdown-toggle dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200"
|
||||
>
|
||||
<svg
|
||||
className="fill-current"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M6.21967 7.28131C5.92678 6.98841 5.92678 6.51354 6.21967 6.22065C6.51256 5.92775 6.98744 5.92775 7.28033 6.22065L11.999 10.9393L16.7176 6.22078C17.0105 5.92789 17.4854 5.92788 17.7782 6.22078C18.0711 6.51367 18.0711 6.98855 17.7782 7.28144L13.0597 12L17.7782 16.7186C18.0711 17.0115 18.0711 17.4863 17.7782 17.7792C17.4854 18.0721 17.0105 18.0721 16.7176 17.7792L11.999 13.0607L7.28033 17.7794C6.98744 18.0722 6.51256 18.0722 6.21967 17.7794C5.92678 17.4865 5.92678 17.0116 6.21967 16.7187L10.9384 12L6.21967 7.28131Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<ul className="flex flex-col h-auto overflow-y-auto custom-scrollbar">
|
||||
{/* Example notification items */}
|
||||
<li>
|
||||
<DropdownItem
|
||||
onItemClick={closeDropdown}
|
||||
className="flex gap-3 rounded-lg border-b border-gray-100 p-3 px-4.5 py-3 hover:bg-gray-100 dark:border-gray-800 dark:hover:bg-white/5"
|
||||
>
|
||||
<span className="relative block w-full h-10 rounded-full z-1 max-w-10">
|
||||
<Image
|
||||
width={40}
|
||||
height={40}
|
||||
src="/images/user/user-02.jpg"
|
||||
alt="User"
|
||||
className="w-full overflow-hidden rounded-full"
|
||||
/>
|
||||
<span className="absolute bottom-0 right-0 z-10 h-2.5 w-full max-w-2.5 rounded-full border-[1.5px] border-white bg-success-500 dark:border-gray-900"></span>
|
||||
</span>
|
||||
|
||||
<span className="block">
|
||||
<span className="mb-1.5 space-x-1 block text-theme-sm text-gray-500 dark:text-gray-400">
|
||||
<span className="font-medium text-gray-800 dark:text-white/90">
|
||||
Terry Franci
|
||||
</span>
|
||||
<span>requests permission to change</span>
|
||||
<span className="font-medium text-gray-800 dark:text-white/90">
|
||||
Project - Nganter App
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span className="flex items-center gap-2 text-gray-500 text-theme-xs dark:text-gray-400">
|
||||
<span>Project</span>
|
||||
<span className="w-1 h-1 bg-gray-400 rounded-full"></span>
|
||||
<span>5 min ago</span>
|
||||
</span>
|
||||
</span>
|
||||
</DropdownItem>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<DropdownItem
|
||||
onItemClick={closeDropdown}
|
||||
className="flex gap-3 rounded-lg border-b border-gray-100 p-3 px-4.5 py-3 hover:bg-gray-100 dark:border-gray-800 dark:hover:bg-white/5"
|
||||
>
|
||||
<span className="relative block w-full h-10 rounded-full z-1 max-w-10">
|
||||
<Image
|
||||
width={40}
|
||||
height={40}
|
||||
src="/images/user/user-03.jpg"
|
||||
alt="User"
|
||||
className="w-full overflow-hidden rounded-full"
|
||||
/>
|
||||
<span className="absolute bottom-0 right-0 z-10 h-2.5 w-full max-w-2.5 rounded-full border-[1.5px] border-white bg-success-500 dark:border-gray-900"></span>
|
||||
</span>
|
||||
|
||||
<span className="block">
|
||||
<span className="mb-1.5 block space-x-1 text-theme-sm text-gray-500 dark:text-gray-400">
|
||||
<span className="font-medium text-gray-800 dark:text-white/90">
|
||||
Alena Franci
|
||||
</span>
|
||||
<span> requests permission to change</span>
|
||||
<span className="font-medium text-gray-800 dark:text-white/90">
|
||||
Project - Nganter App
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span className="flex items-center gap-2 text-gray-500 text-theme-xs dark:text-gray-400">
|
||||
<span>Project</span>
|
||||
<span className="w-1 h-1 bg-gray-400 rounded-full"></span>
|
||||
<span>8 min ago</span>
|
||||
</span>
|
||||
</span>
|
||||
</DropdownItem>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<DropdownItem
|
||||
onItemClick={closeDropdown}
|
||||
className="flex gap-3 rounded-lg border-b border-gray-100 p-3 px-4.5 py-3 hover:bg-gray-100 dark:border-gray-800 dark:hover:bg-white/5"
|
||||
href="#"
|
||||
>
|
||||
<span className="relative block w-full h-10 rounded-full z-1 max-w-10">
|
||||
<Image
|
||||
width={40}
|
||||
height={40}
|
||||
src="/images/user/user-04.jpg"
|
||||
alt="User"
|
||||
className="w-full overflow-hidden rounded-full"
|
||||
/>
|
||||
<span className="absolute bottom-0 right-0 z-10 h-2.5 w-full max-w-2.5 rounded-full border-[1.5px] border-white bg-success-500 dark:border-gray-900"></span>
|
||||
</span>
|
||||
|
||||
<span className="block">
|
||||
<span className="mb-1.5 block space-x-1 text-theme-sm text-gray-500 dark:text-gray-400">
|
||||
<span className="font-medium text-gray-800 dark:text-white/90">
|
||||
Jocelyn Kenter
|
||||
</span>
|
||||
<span>requests permission to change</span>
|
||||
<span className="font-medium text-gray-800 dark:text-white/90">
|
||||
Project - Nganter App
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span className="flex items-center gap-2 text-gray-500 text-theme-xs dark:text-gray-400">
|
||||
<span>Project</span>
|
||||
<span className="w-1 h-1 bg-gray-400 rounded-full"></span>
|
||||
<span>15 min ago</span>
|
||||
</span>
|
||||
</span>
|
||||
</DropdownItem>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<DropdownItem
|
||||
onItemClick={closeDropdown}
|
||||
className="flex gap-3 rounded-lg border-b border-gray-100 p-3 px-4.5 py-3 hover:bg-gray-100 dark:border-gray-800 dark:hover:bg-white/5"
|
||||
href="#"
|
||||
>
|
||||
<span className="relative block w-full h-10 rounded-full z-1 max-w-10">
|
||||
<Image
|
||||
width={40}
|
||||
height={40}
|
||||
src="/images/user/user-05.jpg"
|
||||
alt="User"
|
||||
className="w-full overflow-hidden rounded-full"
|
||||
/>
|
||||
<span className="absolute bottom-0 right-0 z-10 h-2.5 w-full max-w-2.5 rounded-full border-[1.5px] border-white bg-error-500 dark:border-gray-900"></span>
|
||||
</span>
|
||||
|
||||
<span className="block">
|
||||
<span className="mb-1.5 space-x-1 block text-theme-sm text-gray-500 dark:text-gray-400">
|
||||
<span className="font-medium text-gray-800 dark:text-white/90">
|
||||
Brandon Philips
|
||||
</span>
|
||||
<span> requests permission to change</span>
|
||||
<span className="font-medium text-gray-800 dark:text-white/90">
|
||||
Project - Nganter App
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span className="flex items-center gap-2 text-gray-500 text-theme-xs dark:text-gray-400">
|
||||
<span>Project</span>
|
||||
<span className="w-1 h-1 bg-gray-400 rounded-full"></span>
|
||||
<span>1 hr ago</span>
|
||||
</span>
|
||||
</span>
|
||||
</DropdownItem>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<DropdownItem
|
||||
className="flex gap-3 rounded-lg border-b border-gray-100 p-3 px-4.5 py-3 hover:bg-gray-100 dark:border-gray-800 dark:hover:bg-white/5"
|
||||
onItemClick={closeDropdown}
|
||||
>
|
||||
<span className="relative block w-full h-10 rounded-full z-1 max-w-10">
|
||||
<Image
|
||||
width={40}
|
||||
height={40}
|
||||
src="/images/user/user-02.jpg"
|
||||
alt="User"
|
||||
className="w-full overflow-hidden rounded-full"
|
||||
/>
|
||||
<span className="absolute bottom-0 right-0 z-10 h-2.5 w-full max-w-2.5 rounded-full border-[1.5px] border-white bg-success-500 dark:border-gray-900"></span>
|
||||
</span>
|
||||
|
||||
<span className="block">
|
||||
<span className="mb-1.5 space-x-1 block text-theme-sm text-gray-500 dark:text-gray-400">
|
||||
<span className="font-medium text-gray-800 dark:text-white/90">
|
||||
Terry Franci
|
||||
</span>
|
||||
<span>requests permission to change</span>
|
||||
<span className="font-medium text-gray-800 dark:text-white/90">
|
||||
Project - Nganter App
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span className="flex items-center gap-2 text-gray-500 text-theme-xs dark:text-gray-400">
|
||||
<span>Project</span>
|
||||
<span className="w-1 h-1 bg-gray-400 rounded-full"></span>
|
||||
<span>5 min ago</span>
|
||||
</span>
|
||||
</span>
|
||||
</DropdownItem>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<DropdownItem
|
||||
onItemClick={closeDropdown}
|
||||
className="flex gap-3 rounded-lg border-b border-gray-100 p-3 px-4.5 py-3 hover:bg-gray-100 dark:border-gray-800 dark:hover:bg-white/5"
|
||||
>
|
||||
<span className="relative block w-full h-10 rounded-full z-1 max-w-10">
|
||||
<Image
|
||||
width={40}
|
||||
height={40}
|
||||
src="/images/user/user-03.jpg"
|
||||
alt="User"
|
||||
className="w-full overflow-hidden rounded-full"
|
||||
/>
|
||||
<span className="absolute bottom-0 right-0 z-10 h-2.5 w-full max-w-2.5 rounded-full border-[1.5px] border-white bg-success-500 dark:border-gray-900"></span>
|
||||
</span>
|
||||
|
||||
<span className="block">
|
||||
<span className="mb-1.5 space-x-1 block text-theme-sm text-gray-500 dark:text-gray-400">
|
||||
<span className="font-medium text-gray-800 dark:text-white/90">
|
||||
Alena Franci
|
||||
</span>
|
||||
<span>requests permission to change</span>
|
||||
<span className="font-medium text-gray-800 dark:text-white/90">
|
||||
Project - Nganter App
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span className="flex items-center gap-2 text-gray-500 text-theme-xs dark:text-gray-400">
|
||||
<span>Project</span>
|
||||
<span className="w-1 h-1 bg-gray-400 rounded-full"></span>
|
||||
<span>8 min ago</span>
|
||||
</span>
|
||||
</span>
|
||||
</DropdownItem>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<DropdownItem
|
||||
onItemClick={closeDropdown}
|
||||
className="flex gap-3 rounded-lg border-b border-gray-100 p-3 px-4.5 py-3 hover:bg-gray-100 dark:border-gray-800 dark:hover:bg-white/5"
|
||||
>
|
||||
<span className="relative block w-full h-10 rounded-full z-1 max-w-10">
|
||||
<Image
|
||||
width={40}
|
||||
height={40}
|
||||
src="/images/user/user-04.jpg"
|
||||
alt="User"
|
||||
className="w-full overflow-hidden rounded-full"
|
||||
/>
|
||||
<span className="absolute bottom-0 right-0 z-10 h-2.5 w-full max-w-2.5 rounded-full border-[1.5px] border-white bg-success-500 dark:border-gray-900"></span>
|
||||
</span>
|
||||
|
||||
<span className="block">
|
||||
<span className="mb-1.5 space-x-1 block text-theme-sm text-gray-500 dark:text-gray-400">
|
||||
<span className="font-medium text-gray-800 dark:text-white/90">
|
||||
Jocelyn Kenter
|
||||
</span>
|
||||
<span>requests permission to change</span>
|
||||
<span className="font-medium text-gray-800 dark:text-white/90">
|
||||
Project - Nganter App
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span className="flex items-center gap-2 text-gray-500 text-theme-xs dark:text-gray-400">
|
||||
<span>Project</span>
|
||||
<span className="w-1 h-1 bg-gray-400 rounded-full"></span>
|
||||
<span>15 min ago</span>
|
||||
</span>
|
||||
</span>
|
||||
</DropdownItem>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<DropdownItem
|
||||
onItemClick={closeDropdown}
|
||||
className="flex gap-3 rounded-lg border-b border-gray-100 p-3 px-4.5 py-3 hover:bg-gray-100 dark:border-gray-800 dark:hover:bg-white/5"
|
||||
href="#"
|
||||
>
|
||||
<span className="relative block w-full h-10 rounded-full z-1 max-w-10">
|
||||
<Image
|
||||
width={40}
|
||||
height={40}
|
||||
src="/images/user/user-05.jpg"
|
||||
alt="User"
|
||||
className="overflow-hidden rounded-full"
|
||||
/>
|
||||
<span className="absolute bottom-0 right-0 z-10 h-2.5 w-full max-w-2.5 rounded-full border-[1.5px] border-white bg-error-500 dark:border-gray-900"></span>
|
||||
</span>
|
||||
|
||||
<span className="block">
|
||||
<span className="mb-1.5 space-x-1 block text-theme-sm text-gray-500 dark:text-gray-400">
|
||||
<span className="font-medium text-gray-800 dark:text-white/90">
|
||||
Brandon Philips
|
||||
</span>
|
||||
<span>requests permission to change</span>
|
||||
<span className="font-medium text-gray-800 dark:text-white/90">
|
||||
Project - Nganter App
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span className="flex items-center gap-2 text-gray-500 text-theme-xs dark:text-gray-400">
|
||||
<span>Project</span>
|
||||
<span className="w-1 h-1 bg-gray-400 rounded-full"></span>
|
||||
<span>1 hr ago</span>
|
||||
</span>
|
||||
</span>
|
||||
</DropdownItem>
|
||||
</li>
|
||||
{/* Add more items as needed */}
|
||||
</ul>
|
||||
<Link
|
||||
href="/"
|
||||
className="block px-4 py-2 mt-3 text-sm font-medium text-center text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-100 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700"
|
||||
>
|
||||
View All Notifications
|
||||
</Link>
|
||||
</Dropdown>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
207
src/components/header/UserDropdown.tsx
Normal file
207
src/components/header/UserDropdown.tsx
Normal file
@@ -0,0 +1,207 @@
|
||||
"use client";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Dropdown } from "../ui/dropdown/Dropdown";
|
||||
import { DropdownItem } from "../ui/dropdown/DropdownItem";
|
||||
import { fullDataUser } from "@/interface/admin";
|
||||
import { UserMetaCardProps } from "@/interface/user";
|
||||
import { apiGetCurrentUser, apiLogout } from "@/service/auth";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { ListIcon } from "@/icons";
|
||||
|
||||
export default function UserDropdown() {
|
||||
const router = useRouter();
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [user, setUser] = useState<UserMetaCardProps | null>(null);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
function toggleDropdown(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) {
|
||||
e.stopPropagation();
|
||||
setIsOpen((prev) => !prev);
|
||||
}
|
||||
|
||||
function closeDropdown() {
|
||||
setIsOpen(false);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const fetchUser = async () => {
|
||||
try {
|
||||
const userData = await apiGetCurrentUser();
|
||||
// console.log("User data in dropdown:", userData);
|
||||
setUser(userData);
|
||||
} catch (err) {
|
||||
console.error("Lỗi:", err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
fetchUser();
|
||||
}, []);
|
||||
|
||||
const handleLogout = async (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
try {
|
||||
await apiLogout();
|
||||
} catch (error) {
|
||||
console.error("Logout failed", error);
|
||||
} finally {
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
|
||||
document.cookie.split(";").forEach((c) => {
|
||||
document.cookie = c
|
||||
.replace(/^ +/, "")
|
||||
.replace(/=.*/, "=;expires=" + new Date().toUTCString() + ";path=/");
|
||||
});
|
||||
|
||||
window.location.href = "/signin";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={toggleDropdown}
|
||||
className="flex items-center text-gray-700 dark:text-gray-400 dropdown-toggle"
|
||||
>
|
||||
<span className="mr-3 overflow-hidden rounded-full h-11 w-11">
|
||||
<Image
|
||||
width={44}
|
||||
height={44}
|
||||
src={user?.data?.profile?.avatar_url || "/images/no-images.jpg"}
|
||||
alt="User"
|
||||
/>
|
||||
</span>
|
||||
|
||||
<span className="block mr-1 font-medium text-theme-sm">
|
||||
{user?.data?.profile?.display_name || "User"}
|
||||
</span>
|
||||
|
||||
<svg
|
||||
className={`stroke-gray-500 dark:stroke-gray-400 transition-transform duration-200 ${
|
||||
isOpen ? "rotate-180" : ""
|
||||
}`}
|
||||
width="18"
|
||||
height="20"
|
||||
viewBox="0 0 18 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M4.3125 8.65625L9 13.3437L13.6875 8.65625"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<Dropdown
|
||||
isOpen={isOpen}
|
||||
onClose={closeDropdown}
|
||||
className="absolute right-0 mt-[17px] flex w-[260px] flex-col rounded-2xl border border-gray-200 bg-white p-3 shadow-theme-lg dark:border-gray-800 dark:bg-gray-dark"
|
||||
>
|
||||
<div>
|
||||
<span className="block font-medium text-gray-700 text-theme-sm dark:text-gray-400">
|
||||
{user?.data?.profile?.display_name || "User"}
|
||||
</span>
|
||||
<span className="mt-0.5 block text-theme-xs text-gray-500 dark:text-gray-400">
|
||||
{user?.data?.email || "email"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<ul className="flex flex-col gap-1 pt-4 pb-3 border-b border-gray-200 dark:border-gray-800">
|
||||
<li>
|
||||
<DropdownItem
|
||||
onItemClick={closeDropdown}
|
||||
tag="a"
|
||||
href="/user/account"
|
||||
className="flex items-center gap-3 px-3 py-2 font-medium text-gray-700 rounded-lg group text-theme-sm hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300"
|
||||
>
|
||||
<svg
|
||||
className="fill-gray-500 group-hover:fill-gray-700 dark:fill-gray-400 dark:group-hover:fill-gray-300"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M12 3.5C7.30558 3.5 3.5 7.30558 3.5 12C3.5 14.1526 4.3002 16.1184 5.61936 17.616C6.17279 15.3096 8.24852 13.5955 10.7246 13.5955H13.2746C15.7509 13.5955 17.8268 15.31 18.38 17.6167C19.6996 16.119 20.5 14.153 20.5 12C20.5 7.30558 16.6944 3.5 12 3.5ZM17.0246 18.8566V18.8455C17.0246 16.7744 15.3457 15.0955 13.2746 15.0955H10.7246C8.65354 15.0955 6.97461 16.7744 6.97461 18.8455V18.856C8.38223 19.8895 10.1198 20.5 12 20.5C13.8798 20.5 15.6171 19.8898 17.0246 18.8566ZM2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12ZM11.9991 7.25C10.8847 7.25 9.98126 8.15342 9.98126 9.26784C9.98126 10.3823 10.8847 11.2857 11.9991 11.2857C13.1135 11.2857 14.0169 10.3823 14.0169 9.26784C14.0169 8.15342 13.1135 7.25 11.9991 7.25ZM8.48126 9.26784C8.48126 7.32499 10.0563 5.75 11.9991 5.75C13.9419 5.75 15.5169 7.32499 15.5169 9.26784C15.5169 11.2107 13.9419 12.7857 11.9991 12.7857C10.0563 12.7857 8.48126 11.2107 8.48126 9.26784Z"
|
||||
fill=""
|
||||
/>
|
||||
</svg>
|
||||
Tài Khoản
|
||||
</DropdownItem>
|
||||
</li>
|
||||
<li>
|
||||
<DropdownItem
|
||||
onItemClick={closeDropdown}
|
||||
tag="a"
|
||||
href="/user/role-upgrade"
|
||||
className="flex items-center gap-3 px-3 py-2 font-medium text-gray-700 rounded-lg group text-theme-sm hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300"
|
||||
>
|
||||
<span className="menu-item-icon">
|
||||
<ListIcon />
|
||||
</span>
|
||||
Nhà Sử Học
|
||||
</DropdownItem>
|
||||
</li>
|
||||
{/* <li>
|
||||
<DropdownItem
|
||||
onItemClick={closeDropdown}
|
||||
tag="a"
|
||||
href="/profile"
|
||||
className="flex items-center gap-3 px-3 py-2 font-medium text-gray-700 rounded-lg group text-theme-sm hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300"
|
||||
>
|
||||
<svg
|
||||
className="fill-gray-500 group-hover:fill-gray-700 dark:fill-gray-400 dark:group-hover:fill-gray-300"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M3.5 12C3.5 7.30558 7.30558 3.5 12 3.5C16.6944 3.5 20.5 7.30558 20.5 12C20.5 16.6944 16.6944 20.5 12 20.5C7.30558 20.5 3.5 16.6944 3.5 12ZM12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2ZM11.0991 7.52507C11.0991 8.02213 11.5021 8.42507 11.9991 8.42507H12.0001C12.4972 8.42507 12.9001 8.02213 12.9001 7.52507C12.9001 7.02802 12.4972 6.62507 12.0001 6.62507H11.9991C11.5021 6.62507 11.0991 7.02802 11.0991 7.52507ZM12.0001 17.3714C11.5859 17.3714 11.2501 17.0356 11.2501 16.6214V10.9449C11.2501 10.5307 11.5859 10.1949 12.0001 10.1949C12.4143 10.1949 12.7501 10.5307 12.7501 10.9449V16.6214C12.7501 17.0356 12.4143 17.3714 12.0001 17.3714Z"
|
||||
fill=""
|
||||
/>
|
||||
</svg>
|
||||
Hỗ Trợ
|
||||
</DropdownItem>
|
||||
</li> */}
|
||||
</ul>
|
||||
<Link
|
||||
onClick={handleLogout}
|
||||
href="/signin"
|
||||
className="flex items-center gap-3 px-3 py-2 mt-3 font-medium text-gray-700 rounded-lg group text-theme-sm hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300"
|
||||
>
|
||||
<svg
|
||||
className="fill-gray-500 group-hover:fill-gray-700 dark:group-hover:fill-gray-300"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M15.1007 19.247C14.6865 19.247 14.3507 18.9112 14.3507 18.497L14.3507 14.245H12.8507V18.497C12.8507 19.7396 13.8581 20.747 15.1007 20.747H18.5007C19.7434 20.747 20.7507 19.7396 20.7507 18.497L20.7507 5.49609C20.7507 4.25345 19.7433 3.24609 18.5007 3.24609H15.1007C13.8581 3.24609 12.8507 4.25345 12.8507 5.49609V9.74501L14.3507 9.74501V5.49609C14.3507 5.08188 14.6865 4.74609 15.1007 4.74609L18.5007 4.74609C18.9149 4.74609 19.2507 5.08188 19.2507 5.49609L19.2507 18.497C19.2507 18.9112 18.9149 19.247 18.5007 19.247H15.1007ZM3.25073 11.9984C3.25073 12.2144 3.34204 12.4091 3.48817 12.546L8.09483 17.1556C8.38763 17.4485 8.86251 17.4487 9.15549 17.1559C9.44848 16.8631 9.44863 16.3882 9.15583 16.0952L5.81116 12.7484L16.0007 12.7484C16.4149 12.7484 16.7507 12.4127 16.7507 11.9984C16.7507 11.5842 16.4149 11.2484 16.0007 11.2484L5.81528 11.2484L9.15585 7.90554C9.44864 7.61255 9.44847 7.13767 9.15547 6.84488C8.86248 6.55209 8.3876 6.55226 8.09481 6.84525L3.52309 11.4202C3.35673 11.5577 3.25073 11.7657 3.25073 11.9984Z"
|
||||
fill=""
|
||||
/>
|
||||
</svg>
|
||||
Đăng xuất
|
||||
</Link>
|
||||
</Dropdown>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
290
src/components/tables/ApplicationDetailModal.tsx
Normal file
290
src/components/tables/ApplicationDetailModal.tsx
Normal file
@@ -0,0 +1,290 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import Image from "next/image";
|
||||
import Swal from "sweetalert2";
|
||||
import { apiUpdateApplicationStatus } from "@/service/adminService";
|
||||
import { URL_MEDIA } from "../../../api";
|
||||
|
||||
import Lightbox from "yet-another-react-lightbox";
|
||||
import Zoom from "yet-another-react-lightbox/plugins/zoom";
|
||||
import Captions from "yet-another-react-lightbox/plugins/captions";
|
||||
import "yet-another-react-lightbox/styles.css";
|
||||
import "yet-another-react-lightbox/plugins/captions.css";
|
||||
import { IsolatedContent } from "@/components/ui/IsolatedContent";
|
||||
import { apiDeleteHistorianCV } from "@/service/historianService";
|
||||
import { statusConfig } from "@/service/handler";
|
||||
|
||||
interface Props {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
application: any;
|
||||
onRefresh: () => void;
|
||||
}
|
||||
|
||||
export default function ApplicationDetailModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
application,
|
||||
onRefresh,
|
||||
}: Props) {
|
||||
const [reviewNote, setReviewNote] = useState("");
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
const [index, setIndex] = useState(-1);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && application) {
|
||||
setReviewNote(application.review_note || "");
|
||||
} else {
|
||||
setReviewNote("");
|
||||
}
|
||||
}, [isOpen, application]);
|
||||
|
||||
const isImageFile = (file: any) => {
|
||||
const isImageMime = file.mime_type?.startsWith("image/");
|
||||
const isImageExt = /\.(jpg|jpeg|png|webp|gif)$/i.test(file.storage_key);
|
||||
return isImageMime || isImageExt;
|
||||
};
|
||||
|
||||
const mediaList = application?.media || [];
|
||||
const imageMediaOnly = mediaList.filter(isImageFile);
|
||||
|
||||
const imageSlides = imageMediaOnly.map((item: any) => ({
|
||||
src: `${URL_MEDIA}${item.storage_key}`,
|
||||
title: item.original_name,
|
||||
description: `Dung lượng: ${(item.size / 1024).toFixed(2)} KB`,
|
||||
}));
|
||||
|
||||
const handleMediaClick = (item: any) => {
|
||||
const fileUrl = `${URL_MEDIA}${item.storage_key}`;
|
||||
if (isImageFile(item)) {
|
||||
const photoIndex = imageMediaOnly.findIndex(
|
||||
(img: any) => img.id === item.id,
|
||||
);
|
||||
setIndex(photoIndex);
|
||||
} else {
|
||||
const googleDocsUrl = `https://docs.google.com/viewer?url=${encodeURIComponent(fileUrl)}&embedded=true`;
|
||||
window.open(googleDocsUrl, "_blank");
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateStatus = async (status: "APPROVED" | "REJECTED") => {
|
||||
if (!application) return;
|
||||
if (!reviewNote.trim()) {
|
||||
textareaRef.current?.focus();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
await apiUpdateApplicationStatus(application.id, {
|
||||
status,
|
||||
review_note: reviewNote,
|
||||
});
|
||||
Swal.fire("Thành công!", "Trạng thái hồ sơ đã được cập nhật.", "success");
|
||||
onRefresh();
|
||||
onClose();
|
||||
} catch (error) {
|
||||
Swal.fire("Lỗi", "Không thể cập nhật trạng thái.", "error");
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteApplication = async () => {
|
||||
await apiDeleteHistorianCV(application.id);
|
||||
Swal.fire("Thành công!", "Hồ sơ đã được xóa.", "success");
|
||||
onRefresh();
|
||||
onClose();
|
||||
};
|
||||
|
||||
if (!isOpen || !application) return null;
|
||||
|
||||
const userData = application.user || {};
|
||||
const currentStatus = statusConfig[application.status] || {
|
||||
container: "bg-gray-50 border-gray-200 text-gray-600 shadow-sm",
|
||||
};
|
||||
return (
|
||||
<div className="fixed inset-0 z-999 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm">
|
||||
<div className="w-full max-w-4xl max-h-[90vh] bg-white rounded-2xl dark:bg-gray-900 flex flex-col overflow-hidden text-gray-800 dark:text-gray-200">
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-800">
|
||||
<h3 className="text-xl font-semibold">Chi tiết yêu cầu nâng cấp</h3>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-full"
|
||||
>
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 p-6 overflow-y-auto custom-scrollbar">
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="p-5 border border-gray-200 rounded-xl dark:border-gray-800 bg-gray-50/50 dark:bg-white/[0.02]">
|
||||
<h4 className="mb-4 text-xs font-bold text-gray-500 uppercase tracking-wider">
|
||||
Thông tin ứng viên
|
||||
</h4>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="relative w-16 h-16 overflow-hidden border border-gray-200 rounded-full shrink-0">
|
||||
<Image
|
||||
fill
|
||||
src={userData.avatar_url || "/images/no-images.jpg"}
|
||||
alt="avatar"
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-lg font-semibold">
|
||||
{userData.display_name || "N/A"}
|
||||
</h4>
|
||||
<p className="text-sm text-gray-500">
|
||||
{userData.email || "Không có email"}
|
||||
</p>
|
||||
<div className="mt-1 flex gap-2 items-center">
|
||||
<span className="px-3 py-1 text-[11px] font-bold uppercase text-blue-600 bg-blue-100 rounded-md ">
|
||||
{application.verify_type}
|
||||
</span>
|
||||
<span
|
||||
className={`
|
||||
inline-flex items-center px-3 py-1
|
||||
rounded-md border text-[11px] font-semibold uppercase
|
||||
transition-colors duration-200
|
||||
${currentStatus.container}
|
||||
`}
|
||||
>
|
||||
{application.status}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="mb-3 text-xs font-bold text-gray-500 uppercase tracking-wider">
|
||||
Nội dung ứng tuyển
|
||||
</h4>
|
||||
<div className="p-4 prose bg-white border border-gray-200 min-h-[100px] rounded-xl dark:border-gray-800 dark:bg-gray-900 dark:text-gray-200 max-w-none">
|
||||
<IsolatedContent html={application.content} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="mb-3 text-xs font-bold text-gray-500 uppercase tracking-wider">
|
||||
Tệp đính kèm ({mediaList.length})
|
||||
</h4>
|
||||
{mediaList.length > 0 ? (
|
||||
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4">
|
||||
{mediaList.map((media: any, idx: number) => {
|
||||
const isImg = isImageFile(media);
|
||||
return (
|
||||
<div
|
||||
key={idx}
|
||||
onClick={() => handleMediaClick(media)}
|
||||
className="relative overflow-hidden border border-gray-200 group aspect-square rounded-xl dark:border-gray-700 bg-gray-50 dark:bg-gray-800 cursor-pointer"
|
||||
>
|
||||
{isImg ? (
|
||||
<Image
|
||||
src={`${URL_MEDIA}${media.storage_key}`}
|
||||
alt="media"
|
||||
fill
|
||||
className="object-cover transition-transform duration-300 group-hover:scale-110"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center w-full h-full p-3 text-center">
|
||||
<div className="w-10 h-10 mb-2 flex items-center justify-center bg-white dark:bg-zinc-800 rounded-lg shadow-sm text-xl">
|
||||
📄
|
||||
</div>
|
||||
<span className="text-[10px] font-medium text-gray-600 line-clamp-2">
|
||||
{media.original_name}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="absolute inset-0 flex items-center justify-center transition-opacity bg-black/40 opacity-0 group-hover:opacity-100 z-10">
|
||||
<span className="text-white text-xs font-bold px-3 py-1 bg-white/20 backdrop-blur-md rounded-full border border-white/30">
|
||||
{isImg ? "Xem ảnh" : "Xem file"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm italic text-gray-400">
|
||||
Không có tệp đính kèm.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="mb-3 text-xs font-bold text-gray-500 uppercase tracking-wider">
|
||||
Ghi chú duyệt hồ sơ
|
||||
</h4>
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
className="w-full p-4 text-sm bg-white border border-gray-200 rounded-xl dark:border-gray-800 dark:bg-gray-900 focus:ring-2 focus:ring-blue-500 outline-none h-[100px]"
|
||||
placeholder="Nhập lý do (bắt buộc)..."
|
||||
value={reviewNote}
|
||||
onChange={(e) => setReviewNote(e.target.value)}
|
||||
disabled={application.status !== "PENDING"}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end gap-3 px-6 py-4 bg-gray-50 border-t border-gray-200 dark:bg-gray-900/50 dark:border-gray-800">
|
||||
{application.status === "PENDING" && (
|
||||
<>
|
||||
<button
|
||||
onClick={() => handleUpdateStatus("REJECTED")}
|
||||
disabled={isSubmitting}
|
||||
className="px-5 py-2 text-sm font-medium text-white bg-red-500 rounded-xl hover:bg-red-600"
|
||||
>
|
||||
Từ chối
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleUpdateStatus("APPROVED")}
|
||||
disabled={isSubmitting}
|
||||
className="px-5 py-2 text-sm font-medium text-white bg-green-500 rounded-xl hover:bg-green-600"
|
||||
>
|
||||
Phê duyệt
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
<button
|
||||
onClick={handleDeleteApplication}
|
||||
className="px-5 py-2 text-sm font-medium text-white bg-red-500 rounded-xl hover:bg-red-600"
|
||||
>
|
||||
Xóa
|
||||
</button>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-5 py-2 text-sm font-medium border border-gray-300 rounded-xl hover:bg-gray-100 transition-colors"
|
||||
>
|
||||
Đóng
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Lightbox
|
||||
index={index}
|
||||
open={index >= 0}
|
||||
close={() => setIndex(-1)}
|
||||
slides={imageSlides}
|
||||
plugins={[Zoom, Captions]}
|
||||
styles={{ root: { zIndex: 999999 } }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
294
src/components/tables/ApplicationTable.tsx
Normal file
294
src/components/tables/ApplicationTable.tsx
Normal file
@@ -0,0 +1,294 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "../ui/table";
|
||||
import Badge from "../ui/badge/Badge";
|
||||
import { ApplicationDto } from "@/interface/historian";
|
||||
|
||||
export type AppSortColumn =
|
||||
| "created_at"
|
||||
| "status"
|
||||
| "verify_type"
|
||||
| "reviewed_at"
|
||||
| "reviewed_by"
|
||||
| "updated_at";
|
||||
|
||||
interface ApplicationTableProps {
|
||||
data: ApplicationDto[];
|
||||
onSort: (column: AppSortColumn) => void;
|
||||
onViewDetail: (app: ApplicationDto) => void;
|
||||
sortBy?: AppSortColumn;
|
||||
sortOrder?: "asc" | "desc";
|
||||
}
|
||||
|
||||
export default function ApplicationTable({
|
||||
data,
|
||||
onSort,
|
||||
onViewDetail,
|
||||
sortBy,
|
||||
sortOrder,
|
||||
}: ApplicationTableProps) {
|
||||
const formatDate = (dateString: string | null | undefined) => {
|
||||
if (!dateString) return "-";
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString("vi-VN", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
};
|
||||
|
||||
const SortIcon = ({ column }: { column: AppSortColumn }) => {
|
||||
const isActive = sortBy === column;
|
||||
return (
|
||||
<div className="flex flex-col ml-2 opacity-50 cursor-pointer hover:opacity-100">
|
||||
<svg
|
||||
className={`w-3 h-3 ${isActive && sortOrder === "asc" ? "text-blue-700 opacity-100" : "text-gray-400"}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={3}
|
||||
d="M5 15l7-7 7 7"
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
className={`w-3 h-3 -mt-1 ${isActive && sortOrder === "desc" ? "text-blue-700 opacity-100" : "text-gray-400"}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={3}
|
||||
d="M19 9l-7 7-7-7"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getStatusBadge = (status: string | number) => {
|
||||
const s = status?.toString();
|
||||
switch (s) {
|
||||
case "1":
|
||||
case "PENDING":
|
||||
return (
|
||||
<Badge size="sm" variant="light" color="warning">
|
||||
Đang chờ
|
||||
</Badge>
|
||||
);
|
||||
case "2":
|
||||
case "APPROVED":
|
||||
return (
|
||||
<Badge size="sm" variant="light" color="success">
|
||||
Đã duyệt
|
||||
</Badge>
|
||||
);
|
||||
case "3":
|
||||
case "REJECTED":
|
||||
return (
|
||||
<Badge size="sm" variant="light" color="error">
|
||||
Từ chối
|
||||
</Badge>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<Badge size="sm" variant="light" color="light">
|
||||
{status || "N/A"}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const renderVerifyTypes = (
|
||||
verifyType: string | string[] | number | number[],
|
||||
) => {
|
||||
const typeMap: Record<string, string> = {
|
||||
"1": "Thẻ nhận dạng nhà nghiên cứu",
|
||||
ID_CARD: "Thẻ nhận dạng nhà nghiên cứu",
|
||||
"2": "Bằng cấp",
|
||||
EDUCATION: "Bằng cấp",
|
||||
"3": "Chuyên gia",
|
||||
EXPERT: "Chuyên gia",
|
||||
"4": "Khác",
|
||||
OTHER: "Khác",
|
||||
};
|
||||
|
||||
const typesArray = Array.isArray(verifyType) ? verifyType : [verifyType];
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{typesArray.map((type, index) => {
|
||||
const t = type?.toString();
|
||||
if (!t) return null;
|
||||
return (
|
||||
<span
|
||||
key={index}
|
||||
className="px-2 py-0.5 rounded-md bg-blue-50 text-blue-600 dark:bg-blue-500/10 dark:text-blue-400 text-[11px] font-medium whitespace-nowrap"
|
||||
>
|
||||
{typeMap[t] || t}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden rounded-xl border border-gray-200 bg-white dark:border-white/[0.05] dark:bg-white/[0.03]">
|
||||
<div className="max-w-full overflow-x-auto">
|
||||
<div className="min-w-[1300px]">
|
||||
<Table>
|
||||
<TableHeader className="border-b border-gray-100 dark:border-white/[0.05]">
|
||||
<TableRow>
|
||||
<TableCell
|
||||
isHeader
|
||||
className="px-5 py-3 font-medium text-gray-500 text-start text-theme-xs dark:text-gray-400"
|
||||
>
|
||||
Người gửi (ID)
|
||||
</TableCell>
|
||||
<TableCell
|
||||
isHeader
|
||||
className="px-5 py-3 font-medium text-gray-500 text-start text-theme-xs dark:text-gray-400 min-w-[220px]"
|
||||
>
|
||||
Loại xác minh
|
||||
</TableCell>
|
||||
<TableCell
|
||||
isHeader
|
||||
className="px-5 py-3 font-medium text-gray-500 text-start text-theme-xs dark:text-gray-400"
|
||||
>
|
||||
Đính kèm
|
||||
</TableCell>
|
||||
<TableCell
|
||||
isHeader
|
||||
className="px-5 py-3 font-medium text-gray-500 text-start text-theme-xs dark:text-gray-400"
|
||||
>
|
||||
<div
|
||||
className="flex items-center cursor-pointer select-none"
|
||||
onClick={() => onSort("status")}
|
||||
>
|
||||
Trạng thái <SortIcon column="status" />
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
isHeader
|
||||
className="px-5 py-3 font-medium text-gray-500 text-start text-theme-xs dark:text-gray-400"
|
||||
>
|
||||
<div
|
||||
className="flex items-center cursor-pointer select-none"
|
||||
onClick={() => onSort("created_at")}
|
||||
>
|
||||
Ngày nộp <SortIcon column="created_at" />
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
isHeader
|
||||
className="px-5 py-3 font-medium text-gray-500 text-start text-theme-xs dark:text-gray-400"
|
||||
>
|
||||
<div
|
||||
className="flex items-center cursor-pointer select-none"
|
||||
onClick={() => onSort("reviewed_at")}
|
||||
>
|
||||
Cập nhật <SortIcon column="reviewed_at" />
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
isHeader
|
||||
className="px-5 py-3 font-medium text-gray-500 text-start text-theme-xs dark:text-gray-400"
|
||||
>
|
||||
Cập nhật bởi
|
||||
</TableCell>
|
||||
<TableCell
|
||||
isHeader
|
||||
className="px-5 py-3 font-medium text-gray-500 text-start text-theme-xs dark:text-gray-400 max-w-[200px]"
|
||||
>
|
||||
Ghi chú
|
||||
</TableCell>
|
||||
<TableCell
|
||||
isHeader
|
||||
className="px-5 py-3 font-medium text-center text-gray-500 text-theme-xs dark:text-gray-400"
|
||||
>
|
||||
Thao tác
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
||||
<TableBody className="divide-y divide-gray-100 dark:divide-white/[0.05]">
|
||||
{data.length > 0 ? (
|
||||
data.map((app) => (
|
||||
<TableRow
|
||||
key={app.id}
|
||||
className="hover:bg-gray-50/50 dark:hover:bg-white/[0.01] transition-colors"
|
||||
>
|
||||
<TableCell className="px-5 py-4 text-start font-mono text-theme-xs">
|
||||
{app.user.display_name}
|
||||
</TableCell>
|
||||
<TableCell className="px-5 py-4 text-start">
|
||||
{renderVerifyTypes(app.verify_type)}
|
||||
</TableCell>
|
||||
<TableCell className="px-5 py-4 text-center text-theme-sm">
|
||||
<span className="font-bold text-gray-800 dark:text-white mr-1">
|
||||
{app.media?.length || 0}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="px-5 py-4 text-center ">
|
||||
{getStatusBadge(app.status)}
|
||||
</TableCell>
|
||||
<TableCell className="px-5 py-4 text-gray-600 text-theme-sm dark:text-gray-400">
|
||||
{formatDate(app.created_at)}
|
||||
</TableCell>
|
||||
<TableCell className="px-5 py-4 text-gray-600 text-theme-sm dark:text-gray-400">
|
||||
{formatDate(app.reviewed_at)}
|
||||
</TableCell>
|
||||
<TableCell className="px-5 py-4 text-gray-600 text-theme-sm dark:text-gray-400">
|
||||
{app.reviewer?.display_name || "-"}
|
||||
</TableCell>
|
||||
<TableCell className="group relative px-5 pb-4 text-start text-theme-xs text-gray-500 dark:text-gray-400 max-w-50 min-w-50">
|
||||
<div className="truncate">{app.review_note || "-"}</div>
|
||||
|
||||
{app.review_note && (
|
||||
<div className="invisible group-hover:visible absolute z-50 bottom-full left-1/2 -translate-x-1/2 w-max max-w-75 p-2 backdrop-blur-xs text-black text-xs rounded-lg shadow-xl wrap-break-words whitespace-normal">
|
||||
{app.review_note}
|
||||
<div className="absolute top-full left-1/2 border-6 border-transparent border-t-white"></div>
|
||||
</div>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="px-5 py-4 text-center">
|
||||
<button
|
||||
onClick={() => onViewDetail(app)}
|
||||
className="text-brand-500 hover:text-brand-600 font-medium text-theme-sm"
|
||||
>
|
||||
Chi tiết
|
||||
</button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={9}
|
||||
className="px-5 py-20 text-center text-gray-500 italic"
|
||||
>
|
||||
Không tìm thấy dữ liệu hồ sơ
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
293
src/components/tables/BasicTableOne.tsx
Normal file
293
src/components/tables/BasicTableOne.tsx
Normal file
@@ -0,0 +1,293 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "../ui/table";
|
||||
import Badge from "../ui/badge/Badge";
|
||||
import Image from "next/image";
|
||||
import { fullDataUser } from "@/interface/admin";
|
||||
|
||||
type SortColumn = "created_at" | "updated_at" | "display_name" | "email";
|
||||
|
||||
interface Role {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface BasicTableOneProps {
|
||||
data: fullDataUser[];
|
||||
onSort: (column: SortColumn) => void;
|
||||
onViewDetail: (user: fullDataUser) => void;
|
||||
sortBy?: SortColumn;
|
||||
sortOrder?: "asc" | "desc";
|
||||
onFilterRole?: (role: string) => void;
|
||||
selectedRole?: string;
|
||||
roles?: Role[];
|
||||
}
|
||||
|
||||
export default function BasicTableOne({
|
||||
data,
|
||||
onSort,
|
||||
onViewDetail,
|
||||
sortBy,
|
||||
sortOrder,
|
||||
onFilterRole,
|
||||
selectedRole,
|
||||
roles = [],
|
||||
}: BasicTableOneProps) {
|
||||
const formatDate = (dateString: string) => {
|
||||
if (!dateString) return "-";
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString("en-GB", {
|
||||
day: "2-digit",
|
||||
month: "short",
|
||||
year: "numeric",
|
||||
});
|
||||
};
|
||||
|
||||
const SortIcon = ({ column }: { column: SortColumn }) => {
|
||||
const isActive = sortBy === column;
|
||||
return (
|
||||
<div className="flex flex-col ml-2 opacity-50 cursor-pointer hover:opacity-100">
|
||||
<svg
|
||||
className={`w-3 h-3 ${isActive && sortOrder === "asc" ? "text-blue-700 opacity-100" : "text-gray-400"}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={3}
|
||||
d="M5 15l7-7 7 7"
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
className={`w-3 h-3 -mt-1 ${isActive && sortOrder === "desc" ? "text-blue-700 opacity-100" : "text-gray-400"}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={3}
|
||||
d="M19 9l-7 7-7-7"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden rounded-xl border border-gray-200 bg-white dark:border-white/[0.05] dark:bg-white/[0.03]">
|
||||
<div className="max-w-full overflow-x-auto">
|
||||
<div className="min-w-[1100px]">
|
||||
<Table>
|
||||
<TableHeader className="border-b border-gray-100 dark:border-white/[0.05]">
|
||||
<TableRow>
|
||||
<TableCell
|
||||
isHeader
|
||||
className="px-5 py-3 font-medium text-gray-500 text-start text-theme-xs dark:text-gray-400 min-w-[265px] max-w-[265px]"
|
||||
>
|
||||
<div
|
||||
className="flex items-center cursor-pointer select-none"
|
||||
onClick={() => onSort("display_name")}
|
||||
>
|
||||
Người dùng <SortIcon column="display_name" />
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
<TableCell
|
||||
isHeader
|
||||
className="px-5 py-3 font-medium text-gray-500 text-start text-theme-xs dark:text-gray-400 min-w-[337px] max-w-[337px]"
|
||||
>
|
||||
<div
|
||||
className="flex items-center cursor-pointer select-none"
|
||||
onClick={() => onSort("email")}
|
||||
>
|
||||
Email <SortIcon column="email" />
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
<TableCell
|
||||
isHeader
|
||||
className="px-5 py-3 font-medium text-start text-theme-xs min-w-[188px] max-w-[188px]"
|
||||
>
|
||||
<div className="relative inline-flex items-center group">
|
||||
<select
|
||||
value={selectedRole}
|
||||
onChange={(e) => onFilterRole?.(e.target.value)}
|
||||
className="bg-transparent border-none outline-none cursor-pointer appearance-none text-gray-500 dark:text-gray-400 pr-5 hover:text-brand-500 transition-colors font-medium"
|
||||
>
|
||||
<option value="">Vai trò (Tất cả)</option>
|
||||
{roles.map((role) => (
|
||||
<option key={role.id} value={role.id}>
|
||||
{role.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<svg
|
||||
className="w-3 h-3 absolute right-0 text-gray-400 pointer-events-none group-hover:text-brand-500"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={3}
|
||||
d="M19 9l-7 7-7-7"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
<TableCell
|
||||
isHeader
|
||||
className="px-5 py-3 font-medium text-gray-500 text-start text-theme-xs dark:text-gray-400"
|
||||
>
|
||||
Trạng thái
|
||||
</TableCell>
|
||||
|
||||
<TableCell
|
||||
isHeader
|
||||
className="px-5 py-3 font-medium text-gray-500 text-start text-theme-xs dark:text-gray-400"
|
||||
>
|
||||
<div
|
||||
className="flex items-center cursor-pointer select-none"
|
||||
onClick={() => onSort("created_at")}
|
||||
>
|
||||
Ngày tham gia <SortIcon column="created_at" />
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
<TableCell
|
||||
isHeader
|
||||
className="px-5 py-3 font-medium text-gray-500 text-start text-theme-xs dark:text-gray-400"
|
||||
>
|
||||
<div
|
||||
className="flex items-center cursor-pointer select-none"
|
||||
onClick={() => onSort("updated_at")}
|
||||
>
|
||||
Cập nhật <SortIcon column="updated_at" />
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
<TableCell
|
||||
isHeader
|
||||
className="px-5 py-3 font-medium text-gray-500 text-center text-theme-xs dark:text-gray-400"
|
||||
>
|
||||
Thao tác
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
||||
<TableBody className="divide-y divide-gray-100 dark:divide-white/[0.05]">
|
||||
{data.length > 0 ? (
|
||||
data.map((user) => (
|
||||
<TableRow
|
||||
key={user.id}
|
||||
className="hover:bg-gray-50/50 dark:hover:bg-white/[0.01] transition-colors"
|
||||
>
|
||||
<TableCell className="px-5 py-4 text-start">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 overflow-hidden rounded-full flex-shrink-0 bg-gray-100 dark:bg-gray-800 flex items-center justify-center">
|
||||
{user.profile?.avatar_url ? (
|
||||
<Image
|
||||
width={40}
|
||||
height={40}
|
||||
src={user.profile.avatar_url}
|
||||
alt="Avatar"
|
||||
className="object-cover w-full h-full"
|
||||
/>
|
||||
) : (
|
||||
<span className="font-bold text-brand-500 uppercase text-xs">
|
||||
{user.profile?.display_name?.charAt(0) || "U"}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<span className="block font-medium text-gray-800 text-theme-sm dark:text-white/90">
|
||||
{user.profile?.display_name || "N/A"}
|
||||
</span>
|
||||
<span className="block text-gray-500 text-theme-xs dark:text-gray-400">
|
||||
ID: {user.id.slice(0, 8)}...
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="px-5 py-4 text-gray-600 text-start text-theme-sm dark:text-gray-400">
|
||||
{user.email}
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="px-5 py-4 text-start text-theme-sm">
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{user.roles?.map((role) => (
|
||||
<span
|
||||
key={role.id}
|
||||
className="px-2 py-0.5 rounded-md bg-brand-50 text-brand-600 dark:bg-brand-500/10 dark:text-brand-400 text-[10px] uppercase"
|
||||
>
|
||||
{role.name}
|
||||
</span>
|
||||
)) || (
|
||||
<span className="text-gray-400 italic text-[10px]">
|
||||
No Role
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="px-5 py-4 text-start">
|
||||
<Badge
|
||||
size="sm"
|
||||
variant="light"
|
||||
color={user.is_deleted ? "error" : "success"}
|
||||
>
|
||||
{user.is_deleted ? "Bị khóa" : "Hoạt động"}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="px-5 py-4 text-gray-600 text-theme-sm dark:text-gray-400">
|
||||
{formatDate(user.created_at)}
|
||||
</TableCell>
|
||||
<TableCell className="px-5 py-4 text-gray-600 text-theme-sm dark:text-gray-400">
|
||||
{formatDate(user.updated_at)}
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="px-5 py-4 text-center">
|
||||
<button
|
||||
onClick={() => onViewDetail(user)}
|
||||
className="text-brand-500 hover:text-brand-600 font-medium text-theme-sm"
|
||||
>
|
||||
Chi tiết
|
||||
</button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={7}
|
||||
className="px-5 py-24 text-center text-gray-500 italic"
|
||||
>
|
||||
<div className="flex flex-col items-center justify-center gap-2">
|
||||
<p className="text-theme-sm">
|
||||
Không tìm thấy dữ liệu người dùng
|
||||
</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
170
src/components/tables/ChangeRoleModal.tsx
Normal file
170
src/components/tables/ChangeRoleModal.tsx
Normal file
@@ -0,0 +1,170 @@
|
||||
"use client";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Modal } from "../ui/modal";
|
||||
import Button from "../ui/button/Button";
|
||||
import { fullDataUser } from "@/interface/admin";
|
||||
import { apiGetAllRole, apiChangeRole } from "@/service/adminService";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Role {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface ChangeRoleModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
user: fullDataUser | null;
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
const DEFAULT_ROLE_NAME = "USER";
|
||||
|
||||
export default function ChangeRoleModal({ isOpen, onClose, user, onSuccess }: ChangeRoleModalProps) {
|
||||
const [roles, setRoles] = useState<Role[]>([]);
|
||||
const [selectedRoleIds, setSelectedRoleIds] = useState<string[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [fetchingRoles, setFetchingRoles] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && user) {
|
||||
setFetchingRoles(true);
|
||||
apiGetAllRole()
|
||||
.then((res) => {
|
||||
if (res?.status) setRoles(res.data);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Lỗi fetch roles:", err);
|
||||
toast.error("Không thể lấy danh sách vai trò");
|
||||
})
|
||||
.finally(() => setFetchingRoles(false));
|
||||
|
||||
const currentUserRoles = user.roles?.map((r) => r.id) || [];
|
||||
setSelectedRoleIds(currentUserRoles);
|
||||
}
|
||||
}, [isOpen, user]);
|
||||
|
||||
const handleToggleRole = (roleId: string, isDefault: boolean) => {
|
||||
if (isDefault) return;
|
||||
|
||||
setSelectedRoleIds((prev) =>
|
||||
prev.includes(roleId)
|
||||
? prev.filter((id) => id !== roleId)
|
||||
: [...prev, roleId]
|
||||
);
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!user) return;
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
const payload = {
|
||||
role_ids: selectedRoleIds,
|
||||
user_id: user.id,
|
||||
};
|
||||
|
||||
await apiChangeRole(user.id, payload);
|
||||
toast.success("Cập nhật vai trò thành công!");
|
||||
onSuccess();
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast.error("Lỗi khi cập nhật vai trò!");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!user) return null;
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} className="max-w-[400px] m-4">
|
||||
<div className="bg-white dark:bg-gray-900 rounded-xl p-6 shadow-lg border border-gray-100 dark:border-gray-800 w-full relative">
|
||||
|
||||
<div className="mb-6 pr-6">
|
||||
<h3 className="text-lg font-semibold text-gray-800 dark:text-white">
|
||||
Vai trò người dùng
|
||||
</h3>
|
||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400 truncate">
|
||||
{user.profile?.display_name || user.email}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
{fetchingRoles ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<div className="w-6 h-6 border-2 border-gray-200 border-t-brand-500 rounded-full animate-spin"></div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col space-y-1 max-h-[300px] overflow-y-auto custom-scrollbar -mx-2 px-2">
|
||||
{roles.map((role) => {
|
||||
const isSelected = selectedRoleIds.includes(role.id);
|
||||
const isDefault = role.name === DEFAULT_ROLE_NAME;
|
||||
|
||||
return (
|
||||
<label
|
||||
key={role.id}
|
||||
className={`flex items-center gap-3 p-2.5 rounded-lg transition-colors ${
|
||||
isDefault
|
||||
? "opacity-40 cursor-not-allowed bg-gray-50 dark:bg-gray-800/30"
|
||||
: "cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800/50"
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isDefault ? true : isSelected}
|
||||
onChange={() => handleToggleRole(role.id, isDefault)}
|
||||
disabled={isDefault}
|
||||
className={`w-4 h-4 rounded border-gray-300 focus:ring-brand-500 dark:border-gray-600 dark:bg-gray-700 ${
|
||||
isDefault ? "text-gray-400" : "text-brand-500"
|
||||
}`}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<span className={`text-sm ${
|
||||
isDefault
|
||||
? "text-gray-500 font-normal"
|
||||
: isSelected
|
||||
? "text-gray-900 dark:text-white font-medium"
|
||||
: "text-gray-700 dark:text-gray-300"
|
||||
}`}>
|
||||
{role.name}
|
||||
</span>
|
||||
{isDefault && (
|
||||
<span className="text-[10px] text-gray-400 italic">
|
||||
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-end gap-3 mt-8">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
disabled={loading || fetchingRoles}
|
||||
>
|
||||
Hủy
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
type="submit"
|
||||
disabled={loading || fetchingRoles}
|
||||
className="min-w-[100px]"
|
||||
>
|
||||
{loading ? "Đang lưu..." : "Lưu"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
237
src/components/tables/MediaTable.tsx
Normal file
237
src/components/tables/MediaTable.tsx
Normal file
@@ -0,0 +1,237 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "../ui/table";
|
||||
import Badge from "../ui/badge/Badge";
|
||||
|
||||
export type MediaSortColumn =
|
||||
| "created_at"
|
||||
| "updated_at"
|
||||
| "size"
|
||||
| "original_name"
|
||||
| "mime_type";
|
||||
|
||||
export interface MediaItem {
|
||||
id: string;
|
||||
user_id: string;
|
||||
storage_key: string;
|
||||
original_name: string;
|
||||
mime_type: string;
|
||||
size: number;
|
||||
file_metadata: any;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
interface MediaTableProps {
|
||||
data: MediaItem[];
|
||||
onSort: (column: MediaSortColumn) => void;
|
||||
sortBy?: MediaSortColumn;
|
||||
sortOrder?: "asc" | "desc";
|
||||
// Các props mới thêm cho yêu cầu chọn, xem, xóa
|
||||
selectedIds: string[];
|
||||
onToggleSelect: (id: string) => void;
|
||||
onToggleSelectAll: (checked: boolean) => void;
|
||||
onViewSingle: (item: MediaItem, index: number) => void;
|
||||
onDeleteSingle: (id: string) => void;
|
||||
}
|
||||
|
||||
export default function MediaTable({
|
||||
data,
|
||||
onSort,
|
||||
sortBy,
|
||||
sortOrder,
|
||||
selectedIds,
|
||||
onToggleSelect,
|
||||
onToggleSelectAll,
|
||||
onViewSingle,
|
||||
onDeleteSingle,
|
||||
}: MediaTableProps) {
|
||||
const formatDate = (dateString: string | null | undefined) => {
|
||||
if (!dateString) return "-";
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString("vi-VN", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
};
|
||||
|
||||
const formatBytes = (bytes: number) => {
|
||||
if (bytes === 0) return "0 Bytes";
|
||||
const k = 1024;
|
||||
const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
||||
};
|
||||
|
||||
const SortIcon = ({ column }: { column: MediaSortColumn }) => {
|
||||
const isActive = sortBy === column;
|
||||
return (
|
||||
<div className="flex flex-col ml-2 opacity-50 cursor-pointer hover:opacity-100">
|
||||
<svg
|
||||
className={`w-3 h-3 ${isActive && sortOrder === "asc" ? "text-blue-700 opacity-100" : "text-gray-400"}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 15l7-7 7 7" />
|
||||
</svg>
|
||||
<svg
|
||||
className={`w-3 h-3 -mt-1 ${isActive && sortOrder === "desc" ? "text-blue-700 opacity-100" : "text-gray-400"}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getMimeTypeBadge = (mimeType: string) => {
|
||||
if (mimeType.includes("image")) {
|
||||
return (
|
||||
<Badge size="sm" variant="light" color="success">
|
||||
Hình ảnh
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
if (mimeType.includes("pdf") || mimeType.includes("word") || mimeType.includes("document")) {
|
||||
return (
|
||||
<Badge size="sm" variant="light" color="warning">
|
||||
Tài liệu
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Badge size="sm" variant="light" color="light">
|
||||
Khác
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
const isAllSelected = data.length > 0 && selectedIds.length === data.length;
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden rounded-xl border border-gray-200 bg-white dark:border-white/[0.05] dark:bg-white/[0.03]">
|
||||
<div className="max-w-full overflow-x-auto">
|
||||
<div className="min-w-[1000px]">
|
||||
<Table>
|
||||
<TableHeader className="border-b border-gray-100 dark:border-white/[0.05]">
|
||||
<TableRow>
|
||||
<TableCell isHeader className="px-5 py-3 w-12 text-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isAllSelected}
|
||||
onChange={(e) => onToggleSelectAll(e.target.checked)}
|
||||
className="w-4 h-4 text-brand-500 border-gray-300 rounded cursor-pointer focus:ring-brand-500 dark:bg-gray-800 dark:border-gray-600"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell isHeader className="px-5 py-3 font-medium text-gray-500 text-start text-theme-xs dark:text-gray-400">
|
||||
<div className="flex items-center cursor-pointer select-none" onClick={() => onSort("original_name")}>
|
||||
Tên tệp <SortIcon column="original_name" />
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell isHeader className="px-5 py-3 font-medium text-gray-500 text-start text-theme-xs dark:text-gray-400">
|
||||
<div className="flex items-center cursor-pointer select-none" onClick={() => onSort("mime_type")}>
|
||||
Định dạng <SortIcon column="mime_type" />
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell isHeader className="px-5 py-3 font-medium text-gray-500 text-start text-theme-xs dark:text-gray-400">
|
||||
<div className="flex items-center cursor-pointer select-none" onClick={() => onSort("size")}>
|
||||
Kích thước <SortIcon column="size" />
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell isHeader className="px-5 py-3 font-medium text-gray-500 text-start text-theme-xs dark:text-gray-400">
|
||||
<div className="flex items-center cursor-pointer select-none" onClick={() => onSort("created_at")}>
|
||||
Ngày tải lên <SortIcon column="created_at" />
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell isHeader className="px-5 py-3 font-medium text-gray-500 text-start text-theme-xs dark:text-gray-400">
|
||||
<div className="flex items-center cursor-pointer select-none" onClick={() => onSort("updated_at")}>
|
||||
Cập nhật <SortIcon column="updated_at" />
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell isHeader className="px-5 py-3 font-medium text-center text-gray-500 text-theme-xs dark:text-gray-400 min-w-[120px]">
|
||||
Thao tác
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
||||
<TableBody className="divide-y divide-gray-100 dark:divide-white/[0.05]">
|
||||
{data.length > 0 ? (
|
||||
data.map((item, idx) => (
|
||||
<TableRow
|
||||
key={item.id}
|
||||
className="hover:bg-gray-50/50 dark:hover:bg-white/[0.01] transition-colors"
|
||||
>
|
||||
{/* Yêu cầu 1: Cột 0 - Ô hình vuông chọn từng item */}
|
||||
<TableCell className="px-5 py-4 text-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedIds.includes(item.id)}
|
||||
onChange={() => onToggleSelect(item.id)}
|
||||
className="w-4 h-4 text-brand-500 border-gray-300 rounded cursor-pointer focus:ring-brand-500 dark:bg-gray-800 dark:border-gray-600"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="px-5 py-4 text-start font-mono text-theme-xs truncate max-w-[250px]">
|
||||
{item.original_name}
|
||||
</TableCell>
|
||||
<TableCell className="px-5 py-4 text-start">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div>{getMimeTypeBadge(item.mime_type)}</div>
|
||||
<span className="text-[10px] text-gray-400">{item.mime_type}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="px-5 py-4 text-start text-theme-sm font-medium">
|
||||
{formatBytes(item.size)}
|
||||
</TableCell>
|
||||
<TableCell className="px-5 py-4 text-gray-600 text-theme-sm dark:text-gray-400">
|
||||
{formatDate(item.created_at)}
|
||||
</TableCell>
|
||||
<TableCell className="px-5 py-4 text-gray-600 text-theme-sm dark:text-gray-400">
|
||||
{formatDate(item.updated_at)}
|
||||
</TableCell>
|
||||
<TableCell className="px-5 py-4 text-center">
|
||||
<div className="flex items-center justify-center gap-4">
|
||||
<button
|
||||
onClick={() => onViewSingle(item, idx)}
|
||||
className="text-brand-500 hover:text-brand-600 font-medium text-theme-sm"
|
||||
>
|
||||
Xem
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onDeleteSingle(item.id)}
|
||||
className="text-red-500 hover:text-red-600 font-medium text-theme-sm"
|
||||
>
|
||||
Xóa
|
||||
</button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={7}
|
||||
className="px-5 py-20 text-center text-gray-500 italic"
|
||||
>
|
||||
Không tìm thấy tệp tin nào
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
52
src/components/tables/Pagination.tsx
Normal file
52
src/components/tables/Pagination.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
type PaginationProps = {
|
||||
currentPage: number;
|
||||
totalPages: number;
|
||||
onPageChange: (page: number) => void;
|
||||
};
|
||||
|
||||
const Pagination: React.FC<PaginationProps> = ({
|
||||
currentPage,
|
||||
totalPages,
|
||||
onPageChange,
|
||||
}) => {
|
||||
const allPages = Array.from({ length: totalPages }, (_, i) => i + 1);
|
||||
|
||||
return (
|
||||
<div className="flex items-center ">
|
||||
<button
|
||||
onClick={() => onPageChange(currentPage - 1)}
|
||||
disabled={currentPage === 1}
|
||||
className="mr-2.5 flex items-center h-10 justify-center rounded-lg border border-gray-300 bg-white px-3.5 py-2.5 text-gray-700 shadow-theme-xs hover:bg-gray-50 disabled:opacity-50 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03] text-sm"
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
<div className="flex items-center gap-2">
|
||||
{currentPage > 3 && <span className="px-2">...</span>}
|
||||
{allPages.map((page) => (
|
||||
<button
|
||||
key={page}
|
||||
onClick={() => onPageChange(page)}
|
||||
className={`px-4 py-2 rounded ${
|
||||
currentPage === page
|
||||
? "bg-brand-500 text-white"
|
||||
: "text-gray-700 dark:text-gray-400"
|
||||
} flex w-10 items-center justify-center h-10 rounded-lg text-sm font-medium hover:bg-blue-500/[0.08] hover:text-brand-500 dark:hover:text-brand-500`}
|
||||
>
|
||||
{page}
|
||||
</button>
|
||||
))}
|
||||
{currentPage < totalPages - 2 && <span className="px-2">...</span>}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => onPageChange(currentPage + 1)}
|
||||
disabled={currentPage === totalPages}
|
||||
className="ml-2.5 flex items-center justify-center rounded-lg border border-gray-300 bg-white px-3.5 py-2.5 text-gray-700 shadow-theme-xs text-sm hover:bg-gray-50 h-10 disabled:opacity-50 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03]"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Pagination;
|
||||
127
src/components/tables/UserDetailModal.tsx
Normal file
127
src/components/tables/UserDetailModal.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
"use client";
|
||||
import { Modal } from "../ui/modal";
|
||||
import UserMetaCard from "@/components/user-profile/UserMetaCard";
|
||||
import UserInfoCard from "@/components/user-profile/UserInfoCard";
|
||||
import { fullDataUser } from "@/interface/admin";
|
||||
import { useEffect, useState } from "react";
|
||||
import { MediaDto } from "@/interface/media";
|
||||
import { apiGetUserMedia } from "@/service/adminService";
|
||||
import MediaCard from "@/components/user-profile/Media";
|
||||
|
||||
interface UserDetailModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
user: fullDataUser | null;
|
||||
onChangeRole: (user: fullDataUser) => void;
|
||||
onDelete: (user: fullDataUser) => void;
|
||||
onRestore: (user: fullDataUser) => void;
|
||||
}
|
||||
|
||||
export default function UserDetailModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
user,
|
||||
onChangeRole,
|
||||
onDelete,
|
||||
onRestore,
|
||||
}: UserDetailModalProps) {
|
||||
const [mediaData, setMediaData] = useState<MediaDto | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const formattedData = { data: user };
|
||||
|
||||
useEffect(() => {
|
||||
if (user?.id && isOpen) {
|
||||
const fetchUserMedia = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const mediaResponse = await apiGetUserMedia(user.id);
|
||||
setMediaData(mediaResponse);
|
||||
} catch (err) {
|
||||
console.error("Lỗi fetch media:", err);
|
||||
setMediaData(null);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
fetchUserMedia();
|
||||
}
|
||||
}, [user?.id, isOpen]);
|
||||
|
||||
if (!user) return null;
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} className="max-w-[850px] m-4">
|
||||
<div className="no-scrollbar relative w-full max-w-[850px] overflow-y-auto rounded-3xl bg-white p-4 dark:bg-gray-900 lg:p-8">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h3 className="text-xl font-bold text-gray-800 dark:text-white">
|
||||
Chi tiết người dùng
|
||||
</h3>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-gray-400 hover:text-gray-600 transition-colors"
|
||||
>
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6 custom-scrollbar max-h-[65vh] overflow-y-auto pr-2">
|
||||
<UserMetaCard data={formattedData as any} />
|
||||
<UserInfoCard data={formattedData as any} />
|
||||
|
||||
<div className="min-h-[150px] relative">
|
||||
{loading ? (
|
||||
<div className="flex flex-col items-center justify-center py-10 space-y-3">
|
||||
<div className="w-10 h-10 border-4 border-gray-200 border-t-brand-500 rounded-full animate-spin"></div>
|
||||
<p className="text-sm text-gray-500 animate-pulse">Đang tải tài liệu...</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{(mediaData?.data?.length ?? 0) > 0 ? (
|
||||
<MediaCard data={mediaData ?? {}} />
|
||||
) : (
|
||||
<div className="p-5 border border-dashed border-gray-200 rounded-2xl text-center text-gray-400 text-sm">
|
||||
Người dùng này chưa có dữ liệu media.
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 pt-6 border-t border-gray-100 dark:border-white/[0.05] flex items-center justify-between">
|
||||
<div className="text-sm text-gray-500">
|
||||
Thao tác quản trị viên
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={() => onChangeRole(user)}
|
||||
className="px-4 py-2 text-sm font-medium text-purple-600 bg-purple-50 rounded-lg hover:bg-purple-100 dark:bg-purple-500/10 dark:text-purple-400 dark:hover:bg-purple-500/20 transition-colors"
|
||||
>
|
||||
Đổi vai trò
|
||||
</button>
|
||||
|
||||
{user.is_deleted ? (
|
||||
<button
|
||||
onClick={() => onRestore(user)}
|
||||
className="px-4 py-2 text-sm font-medium text-green-600 bg-green-50 rounded-lg hover:bg-green-100 dark:bg-green-500/10 dark:text-green-400 dark:hover:bg-green-500/20 transition-colors"
|
||||
>
|
||||
Khôi phục
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => onDelete(user)}
|
||||
className="px-4 py-2 text-sm font-medium text-red-600 bg-red-50 rounded-lg hover:bg-red-100 dark:bg-red-500/10 dark:text-red-400 dark:hover:bg-red-500/20 transition-colors"
|
||||
>
|
||||
Khóa / Xóa
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
41
src/components/ui/IsolatedContent.tsx
Normal file
41
src/components/ui/IsolatedContent.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
export function IsolatedContent({ html }: { html: string }) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (containerRef.current) {
|
||||
let shadow = containerRef.current.shadowRoot;
|
||||
if (!shadow) {
|
||||
shadow = containerRef.current.attachShadow({ mode: "open" });
|
||||
}
|
||||
|
||||
const styleTag = `
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, sans-serif;
|
||||
color: inherit;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.inner-content {
|
||||
font-size: 14.5px;
|
||||
color: currentColor;
|
||||
}
|
||||
p { margin-bottom: 1rem; }
|
||||
h1, h2, h3 { margin-top: 1.5rem; margin-bottom: 0.5rem; font-weight: 700; line-height: 1.2; }
|
||||
ul, ol { padding-left: 1.5rem; margin-bottom: 1rem; }
|
||||
li { margin-bottom: 0.25rem; }
|
||||
img { max-width: 100%; height: auto; border-radius: 8px; }
|
||||
a { color: #3b82f6; text-decoration: underline; }
|
||||
blockquote { border-left: 4px solid #e5e7eb; padding-left: 1rem; font-style: italic; color: #6b7280; }
|
||||
</style>
|
||||
`;
|
||||
|
||||
|
||||
shadow.innerHTML = `${styleTag}<div class="inner-content">${html || "<i>Không có nội dung.</i>"}</div>`;
|
||||
}
|
||||
}, [html]);
|
||||
|
||||
return <div ref={containerRef} />;
|
||||
}
|
||||
145
src/components/ui/alert/Alert.tsx
Normal file
145
src/components/ui/alert/Alert.tsx
Normal file
@@ -0,0 +1,145 @@
|
||||
import Link from "next/link";
|
||||
import React from "react";
|
||||
|
||||
interface AlertProps {
|
||||
variant: "success" | "error" | "warning" | "info"; // Alert type
|
||||
title: string; // Title of the alert
|
||||
message: string; // Message of the alert
|
||||
showLink?: boolean; // Whether to show the "Learn More" link
|
||||
linkHref?: string; // Link URL
|
||||
linkText?: string; // Link text
|
||||
}
|
||||
|
||||
const Alert: React.FC<AlertProps> = ({
|
||||
variant,
|
||||
title,
|
||||
message,
|
||||
showLink = false,
|
||||
linkHref = "#",
|
||||
linkText = "Learn more",
|
||||
}) => {
|
||||
// Tailwind classes for each variant
|
||||
const variantClasses = {
|
||||
success: {
|
||||
container:
|
||||
"border-success-500 bg-success-50 dark:border-success-500/30 dark:bg-success-500/15",
|
||||
icon: "text-success-500",
|
||||
},
|
||||
error: {
|
||||
container:
|
||||
"border-error-500 bg-error-50 dark:border-error-500/30 dark:bg-error-500/15",
|
||||
icon: "text-error-500",
|
||||
},
|
||||
warning: {
|
||||
container:
|
||||
"border-warning-500 bg-warning-50 dark:border-warning-500/30 dark:bg-warning-500/15",
|
||||
icon: "text-warning-500",
|
||||
},
|
||||
info: {
|
||||
container:
|
||||
"border-blue-light-500 bg-blue-light-50 dark:border-blue-light-500/30 dark:bg-blue-light-500/15",
|
||||
icon: "text-blue-light-500",
|
||||
},
|
||||
};
|
||||
|
||||
// Icon for each variant
|
||||
const icons = {
|
||||
success: (
|
||||
<svg
|
||||
className="fill-current"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M3.70186 12.0001C3.70186 7.41711 7.41711 3.70186 12.0001 3.70186C16.5831 3.70186 20.2984 7.41711 20.2984 12.0001C20.2984 16.5831 16.5831 20.2984 12.0001 20.2984C7.41711 20.2984 3.70186 16.5831 3.70186 12.0001ZM12.0001 1.90186C6.423 1.90186 1.90186 6.423 1.90186 12.0001C1.90186 17.5772 6.423 22.0984 12.0001 22.0984C17.5772 22.0984 22.0984 17.5772 22.0984 12.0001C22.0984 6.423 17.5772 1.90186 12.0001 1.90186ZM15.6197 10.7395C15.9712 10.388 15.9712 9.81819 15.6197 9.46672C15.2683 9.11525 14.6984 9.11525 14.347 9.46672L11.1894 12.6243L9.6533 11.0883C9.30183 10.7368 8.73198 10.7368 8.38051 11.0883C8.02904 11.4397 8.02904 12.0096 8.38051 12.3611L10.553 14.5335C10.7217 14.7023 10.9507 14.7971 11.1894 14.7971C11.428 14.7971 11.657 14.7023 11.8257 14.5335L15.6197 10.7395Z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
error: (
|
||||
<svg
|
||||
className="fill-current"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M20.3499 12.0004C20.3499 16.612 16.6115 20.3504 11.9999 20.3504C7.38832 20.3504 3.6499 16.612 3.6499 12.0004C3.6499 7.38881 7.38833 3.65039 11.9999 3.65039C16.6115 3.65039 20.3499 7.38881 20.3499 12.0004ZM11.9999 22.1504C17.6056 22.1504 22.1499 17.6061 22.1499 12.0004C22.1499 6.3947 17.6056 1.85039 11.9999 1.85039C6.39421 1.85039 1.8499 6.3947 1.8499 12.0004C1.8499 17.6061 6.39421 22.1504 11.9999 22.1504ZM13.0008 16.4753C13.0008 15.923 12.5531 15.4753 12.0008 15.4753L11.9998 15.4753C11.4475 15.4753 10.9998 15.923 10.9998 16.4753C10.9998 17.0276 11.4475 17.4753 11.9998 17.4753L12.0008 17.4753C12.5531 17.4753 13.0008 17.0276 13.0008 16.4753ZM11.9998 6.62898C12.414 6.62898 12.7498 6.96476 12.7498 7.37898L12.7498 13.0555C12.7498 13.4697 12.414 13.8055 11.9998 13.8055C11.5856 13.8055 11.2498 13.4697 11.2498 13.0555L11.2498 7.37898C11.2498 6.96476 11.5856 6.62898 11.9998 6.62898Z"
|
||||
fill="#F04438"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
warning: (
|
||||
<svg
|
||||
className="fill-current"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M3.6501 12.0001C3.6501 7.38852 7.38852 3.6501 12.0001 3.6501C16.6117 3.6501 20.3501 7.38852 20.3501 12.0001C20.3501 16.6117 16.6117 20.3501 12.0001 20.3501C7.38852 20.3501 3.6501 16.6117 3.6501 12.0001ZM12.0001 1.8501C6.39441 1.8501 1.8501 6.39441 1.8501 12.0001C1.8501 17.6058 6.39441 22.1501 12.0001 22.1501C17.6058 22.1501 22.1501 17.6058 22.1501 12.0001C22.1501 6.39441 17.6058 1.8501 12.0001 1.8501ZM10.9992 7.52517C10.9992 8.07746 11.4469 8.52517 11.9992 8.52517H12.0002C12.5525 8.52517 13.0002 8.07746 13.0002 7.52517C13.0002 6.97289 12.5525 6.52517 12.0002 6.52517H11.9992C11.4469 6.52517 10.9992 6.97289 10.9992 7.52517ZM12.0002 17.3715C11.586 17.3715 11.2502 17.0357 11.2502 16.6215V10.945C11.2502 10.5308 11.586 10.195 12.0002 10.195C12.4144 10.195 12.7502 10.5308 12.7502 10.945V16.6215C12.7502 17.0357 12.4144 17.3715 12.0002 17.3715Z"
|
||||
fill=""
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
info: (
|
||||
<svg
|
||||
className="fill-current"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M3.6501 11.9996C3.6501 7.38803 7.38852 3.64961 12.0001 3.64961C16.6117 3.64961 20.3501 7.38803 20.3501 11.9996C20.3501 16.6112 16.6117 20.3496 12.0001 20.3496C7.38852 20.3496 3.6501 16.6112 3.6501 11.9996ZM12.0001 1.84961C6.39441 1.84961 1.8501 6.39392 1.8501 11.9996C1.8501 17.6053 6.39441 22.1496 12.0001 22.1496C17.6058 22.1496 22.1501 17.6053 22.1501 11.9996C22.1501 6.39392 17.6058 1.84961 12.0001 1.84961ZM10.9992 7.52468C10.9992 8.07697 11.4469 8.52468 11.9992 8.52468H12.0002C12.5525 8.52468 13.0002 8.07697 13.0002 7.52468C13.0002 6.9724 12.5525 6.52468 12.0002 6.52468H11.9992C11.4469 6.52468 10.9992 6.9724 10.9992 7.52468ZM12.0002 17.371C11.586 17.371 11.2502 17.0352 11.2502 16.621V10.9445C11.2502 10.5303 11.586 10.1945 12.0002 10.1945C12.4144 10.1945 12.7502 10.5303 12.7502 10.9445V16.621C12.7502 17.0352 12.4144 17.371 12.0002 17.371Z"
|
||||
fill=""
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`rounded-xl border p-4 ${variantClasses[variant].container}`}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className={`-mt-0.5 ${variantClasses[variant].icon}`}>
|
||||
{icons[variant]}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="mb-1 text-sm font-semibold text-gray-800 dark:text-white/90">
|
||||
{title}
|
||||
</h4>
|
||||
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">{message}</p>
|
||||
|
||||
{showLink && (
|
||||
<Link
|
||||
href={linkHref}
|
||||
className="inline-block mt-3 text-sm font-medium text-gray-500 underline dark:text-gray-400"
|
||||
>
|
||||
{linkText}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Alert;
|
||||
65
src/components/ui/avatar/Avatar.tsx
Normal file
65
src/components/ui/avatar/Avatar.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import Image from "next/image";
|
||||
import React from "react";
|
||||
|
||||
interface AvatarProps {
|
||||
src: string; // URL of the avatar image
|
||||
alt?: string; // Alt text for the avatar
|
||||
size?: "xsmall" | "small" | "medium" | "large" | "xlarge" | "xxlarge"; // Avatar size
|
||||
status?: "online" | "offline" | "busy" | "none"; // Status indicator
|
||||
}
|
||||
|
||||
const sizeClasses = {
|
||||
xsmall: "h-6 w-6 max-w-6",
|
||||
small: "h-8 w-8 max-w-8",
|
||||
medium: "h-10 w-10 max-w-10",
|
||||
large: "h-12 w-12 max-w-12",
|
||||
xlarge: "h-14 w-14 max-w-14",
|
||||
xxlarge: "h-16 w-16 max-w-16",
|
||||
};
|
||||
|
||||
const statusSizeClasses = {
|
||||
xsmall: "h-1.5 w-1.5 max-w-1.5",
|
||||
small: "h-2 w-2 max-w-2",
|
||||
medium: "h-2.5 w-2.5 max-w-2.5",
|
||||
large: "h-3 w-3 max-w-3",
|
||||
xlarge: "h-3.5 w-3.5 max-w-3.5",
|
||||
xxlarge: "h-4 w-4 max-w-4",
|
||||
};
|
||||
|
||||
const statusColorClasses = {
|
||||
online: "bg-success-500",
|
||||
offline: "bg-error-400",
|
||||
busy: "bg-warning-500",
|
||||
};
|
||||
|
||||
const Avatar: React.FC<AvatarProps> = ({
|
||||
src,
|
||||
alt = "User Avatar",
|
||||
size = "medium",
|
||||
status = "none",
|
||||
}) => {
|
||||
return (
|
||||
<div className={`relative rounded-full ${sizeClasses[size]}`}>
|
||||
{/* Avatar Image */}
|
||||
<Image
|
||||
width="0"
|
||||
height="0"
|
||||
sizes="100vw"
|
||||
src={src}
|
||||
alt={alt}
|
||||
className="object-cover w-full rounded-full"
|
||||
/>
|
||||
|
||||
{/* Status Indicator */}
|
||||
{status !== "none" && (
|
||||
<span
|
||||
className={`absolute bottom-0 right-0 rounded-full border-[1.5px] border-white dark:border-gray-900 ${
|
||||
statusSizeClasses[size]
|
||||
} ${statusColorClasses[status] || ""}`}
|
||||
></span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Avatar;
|
||||
47
src/components/ui/avatar/AvatarText.tsx
Normal file
47
src/components/ui/avatar/AvatarText.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import React from "react";
|
||||
|
||||
interface AvatarTextProps {
|
||||
name: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const AvatarText: React.FC<AvatarTextProps> = ({ name, className = "" }) => {
|
||||
// Generate initials from name
|
||||
const initials = name
|
||||
.split(" ")
|
||||
.map((word) => word[0])
|
||||
.join("")
|
||||
.toUpperCase()
|
||||
.slice(0, 2);
|
||||
|
||||
// Generate a consistent pastel color based on the name
|
||||
const getColorClass = (name: string) => {
|
||||
const colors = [
|
||||
"bg-brand-100 text-brand-600",
|
||||
"bg-pink-100 text-pink-600",
|
||||
"bg-cyan-100 text-cyan-600",
|
||||
"bg-orange-100 text-orange-600",
|
||||
"bg-green-100 text-green-600",
|
||||
"bg-purple-100 text-purple-600",
|
||||
"bg-yellow-100 text-yellow-600",
|
||||
"bg-error-100 text-error-600",
|
||||
];
|
||||
|
||||
const index = name
|
||||
.split("")
|
||||
.reduce((acc, char) => acc + char.charCodeAt(0), 0);
|
||||
return colors[index % colors.length];
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex h-10 w-10 ${className} items-center justify-center rounded-full ${getColorClass(
|
||||
name
|
||||
)}`}
|
||||
>
|
||||
<span className="text-sm font-medium">{initials}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AvatarText;
|
||||
79
src/components/ui/badge/Badge.tsx
Normal file
79
src/components/ui/badge/Badge.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import React from "react";
|
||||
|
||||
type BadgeVariant = "light" | "solid";
|
||||
type BadgeSize = "sm" | "md";
|
||||
type BadgeColor =
|
||||
| "primary"
|
||||
| "success"
|
||||
| "error"
|
||||
| "warning"
|
||||
| "info"
|
||||
| "light"
|
||||
| "dark";
|
||||
|
||||
interface BadgeProps {
|
||||
variant?: BadgeVariant; // Light or solid variant
|
||||
size?: BadgeSize; // Badge size
|
||||
color?: BadgeColor; // Badge color
|
||||
startIcon?: React.ReactNode; // Icon at the start
|
||||
endIcon?: React.ReactNode; // Icon at the end
|
||||
children: React.ReactNode; // Badge content
|
||||
}
|
||||
|
||||
const Badge: React.FC<BadgeProps> = ({
|
||||
variant = "light",
|
||||
color = "primary",
|
||||
size = "md",
|
||||
startIcon,
|
||||
endIcon,
|
||||
children,
|
||||
}) => {
|
||||
const baseStyles =
|
||||
"inline-flex items-center px-2.5 py-0.5 justify-center gap-1 rounded-full font-medium";
|
||||
|
||||
// Define size styles
|
||||
const sizeStyles = {
|
||||
sm: "text-theme-xs", // Smaller padding and font size
|
||||
md: "text-sm", // Default padding and font size
|
||||
};
|
||||
|
||||
// Define color styles for variants
|
||||
const variants = {
|
||||
light: {
|
||||
primary:
|
||||
"bg-brand-50 text-brand-500 dark:bg-brand-500/15 dark:text-brand-400",
|
||||
success:
|
||||
"bg-success-50 text-success-600 dark:bg-success-500/15 dark:text-success-500",
|
||||
error:
|
||||
"bg-error-50 text-error-600 dark:bg-error-500/15 dark:text-error-500",
|
||||
warning:
|
||||
"bg-warning-50 text-warning-600 dark:bg-warning-500/15 dark:text-orange-400",
|
||||
info: "bg-blue-light-50 text-blue-light-500 dark:bg-blue-light-500/15 dark:text-blue-light-500",
|
||||
light: "bg-gray-100 text-gray-700 dark:bg-white/5 dark:text-white/80",
|
||||
dark: "bg-gray-500 text-white dark:bg-white/5 dark:text-white",
|
||||
},
|
||||
solid: {
|
||||
primary: "bg-brand-500 text-white dark:text-white",
|
||||
success: "bg-success-500 text-white dark:text-white",
|
||||
error: "bg-error-500 text-white dark:text-white",
|
||||
warning: "bg-warning-500 text-white dark:text-white",
|
||||
info: "bg-blue-light-500 text-white dark:text-white",
|
||||
light: "bg-gray-400 dark:bg-white/5 text-white dark:text-white/80",
|
||||
dark: "bg-gray-700 text-white dark:text-white",
|
||||
},
|
||||
};
|
||||
|
||||
// Get styles based on size and color variant
|
||||
const sizeClass = sizeStyles[size];
|
||||
const colorStyles = variants[variant][color];
|
||||
|
||||
return (
|
||||
<span className={`${baseStyles} ${sizeClass} ${colorStyles}`}>
|
||||
{startIcon && <span className="mr-1">{startIcon}</span>}
|
||||
{children}
|
||||
{endIcon && <span className="ml-1">{endIcon}</span>}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default Badge;
|
||||
57
src/components/ui/button/Button.tsx
Normal file
57
src/components/ui/button/Button.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import React, { ReactNode } from "react";
|
||||
|
||||
interface ButtonProps {
|
||||
children: ReactNode; // Button text or content
|
||||
size?: "sm" | "md"; // Button size
|
||||
variant?: "primary" | "outline"; // Button variant
|
||||
startIcon?: ReactNode; // Icon before the text
|
||||
endIcon?: ReactNode; // Icon after the text
|
||||
onClick?: () => void; // Click handler
|
||||
disabled?: boolean; // Disabled state
|
||||
className?: string; // Disabled state
|
||||
type?: "button" | "submit" | "reset";
|
||||
}
|
||||
|
||||
const Button: React.FC<ButtonProps> = ({
|
||||
children,
|
||||
size = "md",
|
||||
variant = "primary",
|
||||
startIcon,
|
||||
endIcon,
|
||||
onClick,
|
||||
className = "",
|
||||
disabled = false,
|
||||
type = "button",
|
||||
}) => {
|
||||
// Size Classes
|
||||
const sizeClasses = {
|
||||
sm: "px-4 py-3 text-sm",
|
||||
md: "px-5 py-3.5 text-sm",
|
||||
};
|
||||
|
||||
// Variant Classes
|
||||
const variantClasses = {
|
||||
primary:
|
||||
"bg-brand-500 text-white shadow-theme-xs hover:bg-brand-600 disabled:bg-brand-300",
|
||||
outline:
|
||||
"bg-white text-gray-700 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 dark:bg-gray-800 dark:text-gray-400 dark:ring-gray-700 dark:hover:bg-white/[0.03] dark:hover:text-gray-300",
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
className={`inline-flex items-center justify-center font-medium gap-2 rounded-lg transition ${className} ${
|
||||
sizeClasses[size]
|
||||
} ${variantClasses[variant]} ${
|
||||
disabled ? "cursor-not-allowed opacity-50" : ""
|
||||
}`}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
>
|
||||
{startIcon && <span className="flex items-center">{startIcon}</span>}
|
||||
{children}
|
||||
{endIcon && <span className="flex items-center">{endIcon}</span>}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default Button;
|
||||
48
src/components/ui/dropdown/Dropdown.tsx
Normal file
48
src/components/ui/dropdown/Dropdown.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
"use client";
|
||||
import type React from "react";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
interface DropdownProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const Dropdown: React.FC<DropdownProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
children,
|
||||
className = "",
|
||||
}) => {
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (
|
||||
dropdownRef.current &&
|
||||
!dropdownRef.current.contains(event.target as Node) &&
|
||||
!(event.target as HTMLElement).closest('.dropdown-toggle')
|
||||
) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
};
|
||||
}, [onClose]);
|
||||
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={dropdownRef}
|
||||
className={`absolute z-40 right-0 mt-2 rounded-xl border border-gray-200 bg-white shadow-theme-lg dark:border-gray-800 dark:bg-gray-dark ${className}`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
46
src/components/ui/dropdown/DropdownItem.tsx
Normal file
46
src/components/ui/dropdown/DropdownItem.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import type React from "react";
|
||||
import Link from "next/link";
|
||||
|
||||
interface DropdownItemProps {
|
||||
tag?: "a" | "button";
|
||||
href?: string;
|
||||
onClick?: () => void;
|
||||
onItemClick?: () => void;
|
||||
baseClassName?: string;
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const DropdownItem: React.FC<DropdownItemProps> = ({
|
||||
tag = "button",
|
||||
href,
|
||||
onClick,
|
||||
onItemClick,
|
||||
baseClassName = "block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 hover:text-gray-900",
|
||||
className = "",
|
||||
children,
|
||||
}) => {
|
||||
const combinedClasses = `${baseClassName} ${className}`.trim();
|
||||
|
||||
const handleClick = (event: React.MouseEvent) => {
|
||||
if (tag === "button") {
|
||||
event.preventDefault();
|
||||
}
|
||||
if (onClick) onClick();
|
||||
if (onItemClick) onItemClick();
|
||||
};
|
||||
|
||||
if (tag === "a" && href) {
|
||||
return (
|
||||
<Link href={href} className={combinedClasses} onClick={handleClick}>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button onClick={handleClick} className={combinedClasses}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
18
src/components/ui/images/ResponsiveImage.tsx
Normal file
18
src/components/ui/images/ResponsiveImage.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import Image from "next/image";
|
||||
import React from "react";
|
||||
|
||||
export default function ResponsiveImage() {
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className="overflow-hidden">
|
||||
<Image
|
||||
src="/images/grid-image/image-01.png"
|
||||
alt="Cover"
|
||||
className="w-full border border-gray-200 rounded-xl dark:border-gray-800"
|
||||
width={1054}
|
||||
height={600}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
38
src/components/ui/images/ThreeColumnImageGrid.tsx
Normal file
38
src/components/ui/images/ThreeColumnImageGrid.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import Image from "next/image";
|
||||
import React from "react";
|
||||
|
||||
export default function ThreeColumnImageGrid() {
|
||||
return (
|
||||
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2 xl:grid-cols-3">
|
||||
<div>
|
||||
<Image
|
||||
src="/images/grid-image/image-04.png"
|
||||
alt=" grid"
|
||||
className="w-full border border-gray-200 rounded-xl dark:border-gray-800"
|
||||
width={338}
|
||||
height={192}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Image
|
||||
src="/images/grid-image/image-05.png"
|
||||
alt=" grid"
|
||||
className="w-full border border-gray-200 rounded-xl dark:border-gray-800"
|
||||
width={338}
|
||||
height={192}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Image
|
||||
src="/images/grid-image/image-06.png"
|
||||
alt=" grid"
|
||||
className="w-full border border-gray-200 rounded-xl dark:border-gray-800"
|
||||
width={338}
|
||||
height={192}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
28
src/components/ui/images/TwoColumnImageGrid.tsx
Normal file
28
src/components/ui/images/TwoColumnImageGrid.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import Image from "next/image";
|
||||
import React from "react";
|
||||
|
||||
export default function TwoColumnImageGrid() {
|
||||
return (
|
||||
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2">
|
||||
<div>
|
||||
<Image
|
||||
src="/images/grid-image/image-02.png"
|
||||
alt=" grid"
|
||||
className="w-full border border-gray-200 rounded-xl dark:border-gray-800"
|
||||
width={517}
|
||||
height={295}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Image
|
||||
src="/images/grid-image/image-03.png"
|
||||
alt=" grid"
|
||||
className="w-full border border-gray-200 rounded-xl dark:border-gray-800"
|
||||
width={517}
|
||||
height={295}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
95
src/components/ui/modal/index.tsx
Normal file
95
src/components/ui/modal/index.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
"use client";
|
||||
import React, { useRef, useEffect } from "react";
|
||||
|
||||
interface ModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
showCloseButton?: boolean;
|
||||
isFullscreen?: boolean;
|
||||
}
|
||||
|
||||
export const Modal: React.FC<ModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
children,
|
||||
className,
|
||||
showCloseButton = true,
|
||||
isFullscreen = false,
|
||||
}) => {
|
||||
const modalRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const handleEscape = (event: KeyboardEvent) => {
|
||||
if (event.key === "Escape") {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
if (isOpen) {
|
||||
document.addEventListener("keydown", handleEscape);
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("keydown", handleEscape);
|
||||
};
|
||||
}, [isOpen, onClose]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
document.body.style.overflow = "hidden";
|
||||
} else {
|
||||
document.body.style.overflow = "unset";
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.body.style.overflow = "unset";
|
||||
};
|
||||
}, [isOpen]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const contentClasses = isFullscreen
|
||||
? "w-full h-full"
|
||||
: "relative w-full rounded-xl bg-white dark:bg-gray-900";
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 flex items-center justify-center overflow-y-auto modal z-99999">
|
||||
{!isFullscreen && (
|
||||
<div
|
||||
className="fixed inset-0 h-full w-full bg-black/50 backdrop-blur-[2px]"
|
||||
onClick={onClose}
|
||||
></div>
|
||||
)}
|
||||
<div
|
||||
ref={modalRef}
|
||||
className={`${contentClasses} ${className}`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{showCloseButton && (
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="absolute right-3 top-3 z-999 flex h-9.5 w-9.5 items-center justify-center rounded-full bg-gray-100 text-gray-400 transition-colors hover:bg-gray-200 hover:text-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white sm:right-6 sm:top-6 sm:h-11 sm:w-11"
|
||||
>
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M6.04289 16.5413C5.65237 16.9318 5.65237 17.565 6.04289 17.9555C6.43342 18.346 7.06658 18.346 7.45711 17.9555L11.9987 13.4139L16.5408 17.956C16.9313 18.3466 17.5645 18.3466 17.955 17.956C18.3455 17.5655 18.3455 16.9323 17.955 16.5418L13.4129 11.9997L17.955 7.4576C18.3455 7.06707 18.3455 6.43391 17.955 6.04338C17.5645 5.65286 16.9313 5.65286 16.5408 6.04338L11.9987 10.5855L7.45711 6.0439C7.06658 5.65338 6.43342 5.65338 6.04289 6.0439C5.65237 6.43442 5.65237 7.06759 6.04289 7.45811L10.5845 11.9997L6.04289 16.5413Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
27
src/components/ui/parse/SafeHTMLRenderer.tsx
Normal file
27
src/components/ui/parse/SafeHTMLRenderer.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
"use client";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
// Component này đóng vai trò như một "vùng an toàn"
|
||||
export function SafeHTMLRenderer({ html }: { html: string }) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (containerRef.current) {
|
||||
// 1. Giải mã HTML Entities (Biến < thành <, " thành ", ...)
|
||||
const txt = document.createElement("textarea");
|
||||
txt.innerHTML = html;
|
||||
let decoded = txt.value;
|
||||
|
||||
// 2. Xử lý xóa thẻ <pre> bọc ngoài nếu API trả về dư thừa
|
||||
decoded = decoded.replace(/<pre[^>]*>/g, "").replace(/<\/pre>/g, "");
|
||||
|
||||
// 3. Khởi tạo Shadow DOM (nếu chưa có)
|
||||
const shadowRoot = containerRef.current.shadowRoot || containerRef.current.attachShadow({ mode: "open" });
|
||||
|
||||
// 4. Render nội dung vào trong vùng cô lập
|
||||
shadowRoot.innerHTML = decoded;
|
||||
}
|
||||
}, [html]);
|
||||
|
||||
return <div ref={containerRef} className="w-full h-auto" />;
|
||||
}
|
||||
64
src/components/ui/table/index.tsx
Normal file
64
src/components/ui/table/index.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import React, { ReactNode, TdHTMLAttributes, HTMLAttributes } from "react";
|
||||
|
||||
// Props for Table
|
||||
interface TableProps extends HTMLAttributes<HTMLTableElement> {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
// Props for TableHeader
|
||||
interface TableHeaderProps extends HTMLAttributes<HTMLTableSectionElement> {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
// Props for TableBody
|
||||
interface TableBodyProps extends HTMLAttributes<HTMLTableSectionElement> {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
// Props for TableRow
|
||||
interface TableRowProps extends HTMLAttributes<HTMLTableRowElement> {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
// Props for TableCell - Hỗ trợ colSpan, rowSpan...
|
||||
interface TableCellProps extends TdHTMLAttributes<HTMLTableCellElement> {
|
||||
children: ReactNode;
|
||||
isHeader?: boolean;
|
||||
}
|
||||
|
||||
// Table Component
|
||||
const Table: React.FC<TableProps> = ({ children, className, ...props }) => {
|
||||
return <table className={`min-w-full ${className}`} {...props}>{children}</table>;
|
||||
};
|
||||
|
||||
// TableHeader Component
|
||||
const TableHeader: React.FC<TableHeaderProps> = ({ children, className, ...props }) => {
|
||||
return <thead className={className} {...props}>{children}</thead>;
|
||||
};
|
||||
|
||||
// TableBody Component
|
||||
const TableBody: React.FC<TableBodyProps> = ({ children, className, ...props }) => {
|
||||
return <tbody className={className} {...props}>{children}</tbody>;
|
||||
};
|
||||
|
||||
// TableRow Component
|
||||
const TableRow: React.FC<TableRowProps> = ({ children, className, ...props }) => {
|
||||
return <tr className={className} {...props}>{children}</tr>;
|
||||
};
|
||||
|
||||
// TableCell Component
|
||||
const TableCell: React.FC<TableCellProps> = ({
|
||||
children,
|
||||
isHeader = false,
|
||||
className,
|
||||
...props
|
||||
}) => {
|
||||
const CellTag = isHeader ? "th" : "td";
|
||||
return (
|
||||
<CellTag className={className} {...(props as any)}>
|
||||
{children}
|
||||
</CellTag>
|
||||
);
|
||||
};
|
||||
|
||||
export { Table, TableHeader, TableBody, TableRow, TableCell };
|
||||
28
src/components/ui/video/VideosExample.tsx
Normal file
28
src/components/ui/video/VideosExample.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import React from "react";
|
||||
import YouTubeEmbed from "./YouTubeEmbed";
|
||||
import ComponentCard from "@/components/common/ComponentCard";
|
||||
|
||||
export default function VideosExample() {
|
||||
return (
|
||||
<div>
|
||||
<div className="grid grid-cols-1 gap-5 sm:gap-6 xl:grid-cols-2">
|
||||
<div className="space-y-5 sm:space-y-6">
|
||||
<ComponentCard title="Video Ratio 16:9">
|
||||
<YouTubeEmbed videoId="dQw4w9WgXcQ" />
|
||||
</ComponentCard>
|
||||
<ComponentCard title="Video Ratio 4:3">
|
||||
<YouTubeEmbed videoId="dQw4w9WgXcQ" aspectRatio="4:3" />
|
||||
</ComponentCard>
|
||||
</div>
|
||||
<div className="space-y-5 sm:space-y-6">
|
||||
<ComponentCard title="Video Ratio 21:9">
|
||||
<YouTubeEmbed videoId="dQw4w9WgXcQ" aspectRatio="21:9" />
|
||||
</ComponentCard>
|
||||
<ComponentCard title="Video Ratio 1:1">
|
||||
<YouTubeEmbed videoId="dQw4w9WgXcQ" aspectRatio="1:1" />
|
||||
</ComponentCard>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
41
src/components/ui/video/YouTubeEmbed.tsx
Normal file
41
src/components/ui/video/YouTubeEmbed.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import React from "react";
|
||||
|
||||
type AspectRatio = "16:9" | "4:3" | "21:9" | "1:1";
|
||||
|
||||
interface YouTubeEmbedProps {
|
||||
videoId: string;
|
||||
aspectRatio?: AspectRatio;
|
||||
title?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const YouTubeEmbed: React.FC<YouTubeEmbedProps> = ({
|
||||
videoId,
|
||||
aspectRatio = "16:9",
|
||||
title = "YouTube video",
|
||||
className = "",
|
||||
}) => {
|
||||
const aspectRatioClass = {
|
||||
"16:9": "aspect-video",
|
||||
"4:3": "aspect-4/3",
|
||||
"21:9": "aspect-21/9",
|
||||
"1:1": "aspect-square",
|
||||
}[aspectRatio];
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`overflow-hidden rounded-lg ${aspectRatioClass} ${className}`}
|
||||
>
|
||||
<iframe
|
||||
src={`https://www.youtube.com/embed/${videoId}`}
|
||||
title={title}
|
||||
frameBorder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
className="w-full h-full"
|
||||
></iframe>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default YouTubeEmbed;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user