init
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 />;
|
||||
}
|
||||
@@ -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 />;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export default function FullWidthPageLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return <div>{children}</div>;
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user