diff --git a/app/(admin)/admin/dashboard/page.tsx b/app/(admin)/admin/dashboard/page.tsx deleted file mode 100644 index 3c31f0b..0000000 --- a/app/(admin)/admin/dashboard/page.tsx +++ /dev/null @@ -1,34 +0,0 @@ -"use client"; - -import DashboardContainer from "@/components/main/dashboard/dashboard-container"; -import { motion } from "framer-motion"; -import { useEffect, useState } from "react"; - -export default function AdminPage() { - const [mounted, setMounted] = useState(false); - - useEffect(() => { - setMounted(true); - }, []); - - if (!mounted) { - return ( -
-
-
- ); - } - - return ( - -
- -
-
- ); -} diff --git a/app/(admin)/admin/layout.tsx b/app/(admin)/admin/layout.tsx deleted file mode 100644 index 459794f..0000000 --- a/app/(admin)/admin/layout.tsx +++ /dev/null @@ -1,11 +0,0 @@ -"use client"; - -import { AdminLayout } from "@/components/layout/admin-layout"; - -export default function AdminPageLayout({ - children, -}: { - children: React.ReactNode; -}) { - return {children}; -} diff --git a/app/(admin)/admin/content/audio-visual/components/audio-visual-tabs.tsx b/app/[locale]/(admin)/admin/content/audio-visual/components/audio-visual-tabs.tsx similarity index 100% rename from app/(admin)/admin/content/audio-visual/components/audio-visual-tabs.tsx rename to app/[locale]/(admin)/admin/content/audio-visual/components/audio-visual-tabs.tsx diff --git a/app/(admin)/admin/content/audio-visual/components/columns.tsx b/app/[locale]/(admin)/admin/content/audio-visual/components/columns.tsx similarity index 100% rename from app/(admin)/admin/content/audio-visual/components/columns.tsx rename to app/[locale]/(admin)/admin/content/audio-visual/components/columns.tsx diff --git a/app/(admin)/admin/content/audio-visual/components/pending-approval-columns.tsx b/app/[locale]/(admin)/admin/content/audio-visual/components/pending-approval-columns.tsx similarity index 100% rename from app/(admin)/admin/content/audio-visual/components/pending-approval-columns.tsx rename to app/[locale]/(admin)/admin/content/audio-visual/components/pending-approval-columns.tsx diff --git a/app/(admin)/admin/content/audio-visual/components/pending-approval-table.tsx b/app/[locale]/(admin)/admin/content/audio-visual/components/pending-approval-table.tsx similarity index 100% rename from app/(admin)/admin/content/audio-visual/components/pending-approval-table.tsx rename to app/[locale]/(admin)/admin/content/audio-visual/components/pending-approval-table.tsx diff --git a/app/(admin)/admin/content/audio-visual/components/table-video.tsx b/app/[locale]/(admin)/admin/content/audio-visual/components/table-video.tsx similarity index 100% rename from app/(admin)/admin/content/audio-visual/components/table-video.tsx rename to app/[locale]/(admin)/admin/content/audio-visual/components/table-video.tsx diff --git a/app/(admin)/admin/content/audio-visual/create/page.tsx b/app/[locale]/(admin)/admin/content/audio-visual/create/page.tsx similarity index 100% rename from app/(admin)/admin/content/audio-visual/create/page.tsx rename to app/[locale]/(admin)/admin/content/audio-visual/create/page.tsx diff --git a/app/(admin)/admin/content/audio-visual/detail/[id]/page.tsx b/app/[locale]/(admin)/admin/content/audio-visual/detail/[id]/page.tsx similarity index 100% rename from app/(admin)/admin/content/audio-visual/detail/[id]/page.tsx rename to app/[locale]/(admin)/admin/content/audio-visual/detail/[id]/page.tsx diff --git a/app/(admin)/admin/content/audio-visual/layout.tsx b/app/[locale]/(admin)/admin/content/audio-visual/layout.tsx similarity index 100% rename from app/(admin)/admin/content/audio-visual/layout.tsx rename to app/[locale]/(admin)/admin/content/audio-visual/layout.tsx diff --git a/app/(admin)/admin/content/audio-visual/page.tsx b/app/[locale]/(admin)/admin/content/audio-visual/page.tsx similarity index 100% rename from app/(admin)/admin/content/audio-visual/page.tsx rename to app/[locale]/(admin)/admin/content/audio-visual/page.tsx diff --git a/app/(admin)/admin/content/audio-visual/update/[id]/page.tsx b/app/[locale]/(admin)/admin/content/audio-visual/update/[id]/page.tsx similarity index 100% rename from app/(admin)/admin/content/audio-visual/update/[id]/page.tsx rename to app/[locale]/(admin)/admin/content/audio-visual/update/[id]/page.tsx diff --git a/app/(admin)/admin/content/audio/components/audio-tabs.tsx b/app/[locale]/(admin)/admin/content/audio/components/audio-tabs.tsx similarity index 100% rename from app/(admin)/admin/content/audio/components/audio-tabs.tsx rename to app/[locale]/(admin)/admin/content/audio/components/audio-tabs.tsx diff --git a/app/(admin)/admin/content/audio/components/columns.tsx b/app/[locale]/(admin)/admin/content/audio/components/columns.tsx similarity index 100% rename from app/(admin)/admin/content/audio/components/columns.tsx rename to app/[locale]/(admin)/admin/content/audio/components/columns.tsx diff --git a/app/(admin)/admin/content/audio/components/pending-approval-columns.tsx b/app/[locale]/(admin)/admin/content/audio/components/pending-approval-columns.tsx similarity index 100% rename from app/(admin)/admin/content/audio/components/pending-approval-columns.tsx rename to app/[locale]/(admin)/admin/content/audio/components/pending-approval-columns.tsx diff --git a/app/(admin)/admin/content/audio/components/pending-approval-table.tsx b/app/[locale]/(admin)/admin/content/audio/components/pending-approval-table.tsx similarity index 100% rename from app/(admin)/admin/content/audio/components/pending-approval-table.tsx rename to app/[locale]/(admin)/admin/content/audio/components/pending-approval-table.tsx diff --git a/app/(admin)/admin/content/audio/components/table-audio.tsx b/app/[locale]/(admin)/admin/content/audio/components/table-audio.tsx similarity index 100% rename from app/(admin)/admin/content/audio/components/table-audio.tsx rename to app/[locale]/(admin)/admin/content/audio/components/table-audio.tsx diff --git a/app/(admin)/admin/content/audio/create/page.tsx b/app/[locale]/(admin)/admin/content/audio/create/page.tsx similarity index 100% rename from app/(admin)/admin/content/audio/create/page.tsx rename to app/[locale]/(admin)/admin/content/audio/create/page.tsx diff --git a/app/(admin)/admin/content/audio/detail/[id]/page.tsx b/app/[locale]/(admin)/admin/content/audio/detail/[id]/page.tsx similarity index 100% rename from app/(admin)/admin/content/audio/detail/[id]/page.tsx rename to app/[locale]/(admin)/admin/content/audio/detail/[id]/page.tsx diff --git a/app/(admin)/admin/content/audio/layout.tsx b/app/[locale]/(admin)/admin/content/audio/layout.tsx similarity index 100% rename from app/(admin)/admin/content/audio/layout.tsx rename to app/[locale]/(admin)/admin/content/audio/layout.tsx diff --git a/app/(admin)/admin/content/audio/page.tsx b/app/[locale]/(admin)/admin/content/audio/page.tsx similarity index 100% rename from app/(admin)/admin/content/audio/page.tsx rename to app/[locale]/(admin)/admin/content/audio/page.tsx diff --git a/app/(admin)/admin/content/audio/update/[id]/page.tsx b/app/[locale]/(admin)/admin/content/audio/update/[id]/page.tsx similarity index 100% rename from app/(admin)/admin/content/audio/update/[id]/page.tsx rename to app/[locale]/(admin)/admin/content/audio/update/[id]/page.tsx diff --git a/app/(admin)/admin/content/document/components/columns.tsx b/app/[locale]/(admin)/admin/content/document/components/columns.tsx similarity index 100% rename from app/(admin)/admin/content/document/components/columns.tsx rename to app/[locale]/(admin)/admin/content/document/components/columns.tsx diff --git a/app/(admin)/admin/content/document/components/document-tabs.tsx b/app/[locale]/(admin)/admin/content/document/components/document-tabs.tsx similarity index 100% rename from app/(admin)/admin/content/document/components/document-tabs.tsx rename to app/[locale]/(admin)/admin/content/document/components/document-tabs.tsx diff --git a/app/(admin)/admin/content/document/components/pending-approval-columns.tsx b/app/[locale]/(admin)/admin/content/document/components/pending-approval-columns.tsx similarity index 100% rename from app/(admin)/admin/content/document/components/pending-approval-columns.tsx rename to app/[locale]/(admin)/admin/content/document/components/pending-approval-columns.tsx diff --git a/app/(admin)/admin/content/document/components/pending-approval-table.tsx b/app/[locale]/(admin)/admin/content/document/components/pending-approval-table.tsx similarity index 100% rename from app/(admin)/admin/content/document/components/pending-approval-table.tsx rename to app/[locale]/(admin)/admin/content/document/components/pending-approval-table.tsx diff --git a/app/(admin)/admin/content/document/components/table-teks.tsx b/app/[locale]/(admin)/admin/content/document/components/table-teks.tsx similarity index 100% rename from app/(admin)/admin/content/document/components/table-teks.tsx rename to app/[locale]/(admin)/admin/content/document/components/table-teks.tsx diff --git a/app/(admin)/admin/content/document/create/page.tsx b/app/[locale]/(admin)/admin/content/document/create/page.tsx similarity index 100% rename from app/(admin)/admin/content/document/create/page.tsx rename to app/[locale]/(admin)/admin/content/document/create/page.tsx diff --git a/app/(admin)/admin/content/document/detail/[id]/page.tsx b/app/[locale]/(admin)/admin/content/document/detail/[id]/page.tsx similarity index 100% rename from app/(admin)/admin/content/document/detail/[id]/page.tsx rename to app/[locale]/(admin)/admin/content/document/detail/[id]/page.tsx diff --git a/app/(admin)/admin/content/document/layout.tsx b/app/[locale]/(admin)/admin/content/document/layout.tsx similarity index 100% rename from app/(admin)/admin/content/document/layout.tsx rename to app/[locale]/(admin)/admin/content/document/layout.tsx diff --git a/app/(admin)/admin/content/document/page.tsx b/app/[locale]/(admin)/admin/content/document/page.tsx similarity index 100% rename from app/(admin)/admin/content/document/page.tsx rename to app/[locale]/(admin)/admin/content/document/page.tsx diff --git a/app/(admin)/admin/content/document/update/[id]/page.tsx b/app/[locale]/(admin)/admin/content/document/update/[id]/page.tsx similarity index 100% rename from app/(admin)/admin/content/document/update/[id]/page.tsx rename to app/[locale]/(admin)/admin/content/document/update/[id]/page.tsx diff --git a/app/(admin)/admin/content/image/components/columns.tsx b/app/[locale]/(admin)/admin/content/image/components/columns.tsx similarity index 100% rename from app/(admin)/admin/content/image/components/columns.tsx rename to app/[locale]/(admin)/admin/content/image/components/columns.tsx diff --git a/app/(admin)/admin/content/image/components/image-tabs.tsx b/app/[locale]/(admin)/admin/content/image/components/image-tabs.tsx similarity index 100% rename from app/(admin)/admin/content/image/components/image-tabs.tsx rename to app/[locale]/(admin)/admin/content/image/components/image-tabs.tsx diff --git a/app/(admin)/admin/content/image/components/pending-approval-columns.tsx b/app/[locale]/(admin)/admin/content/image/components/pending-approval-columns.tsx similarity index 100% rename from app/(admin)/admin/content/image/components/pending-approval-columns.tsx rename to app/[locale]/(admin)/admin/content/image/components/pending-approval-columns.tsx diff --git a/app/(admin)/admin/content/image/components/pending-approval-table.tsx b/app/[locale]/(admin)/admin/content/image/components/pending-approval-table.tsx similarity index 100% rename from app/(admin)/admin/content/image/components/pending-approval-table.tsx rename to app/[locale]/(admin)/admin/content/image/components/pending-approval-table.tsx diff --git a/app/(admin)/admin/content/image/components/table-image.tsx b/app/[locale]/(admin)/admin/content/image/components/table-image.tsx similarity index 100% rename from app/(admin)/admin/content/image/components/table-image.tsx rename to app/[locale]/(admin)/admin/content/image/components/table-image.tsx diff --git a/app/(admin)/admin/content/image/create/page.tsx b/app/[locale]/(admin)/admin/content/image/create/page.tsx similarity index 100% rename from app/(admin)/admin/content/image/create/page.tsx rename to app/[locale]/(admin)/admin/content/image/create/page.tsx diff --git a/app/(admin)/admin/content/image/detail/[id]/page.tsx b/app/[locale]/(admin)/admin/content/image/detail/[id]/page.tsx similarity index 100% rename from app/(admin)/admin/content/image/detail/[id]/page.tsx rename to app/[locale]/(admin)/admin/content/image/detail/[id]/page.tsx diff --git a/app/(admin)/admin/content/image/layout.tsx b/app/[locale]/(admin)/admin/content/image/layout.tsx similarity index 100% rename from app/(admin)/admin/content/image/layout.tsx rename to app/[locale]/(admin)/admin/content/image/layout.tsx diff --git a/app/(admin)/admin/content/image/page-1.tsx b/app/[locale]/(admin)/admin/content/image/page-1.tsx similarity index 100% rename from app/(admin)/admin/content/image/page-1.tsx rename to app/[locale]/(admin)/admin/content/image/page-1.tsx diff --git a/app/(admin)/admin/content/image/page.tsx b/app/[locale]/(admin)/admin/content/image/page.tsx similarity index 100% rename from app/(admin)/admin/content/image/page.tsx rename to app/[locale]/(admin)/admin/content/image/page.tsx diff --git a/app/(admin)/admin/content/image/update/[id]/page.tsx b/app/[locale]/(admin)/admin/content/image/update/[id]/page.tsx similarity index 100% rename from app/(admin)/admin/content/image/update/[id]/page.tsx rename to app/[locale]/(admin)/admin/content/image/update/[id]/page.tsx diff --git a/app/[locale]/(admin)/admin/dashboard/page.tsx b/app/[locale]/(admin)/admin/dashboard/page.tsx new file mode 100644 index 0000000..61f32a6 --- /dev/null +++ b/app/[locale]/(admin)/admin/dashboard/page.tsx @@ -0,0 +1,32 @@ +"use client"; + +import DashboardContainer from "@/components/main/dashboard/dashboard-container"; +import { motion } from "framer-motion"; +import { useEffect, useState } from "react"; + +export default function AdminPage() { + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + }, []); + + if (!mounted) { + return ( +
+
+
+ ); + } + + return ( + + + + ); +} diff --git a/app/[locale]/(admin)/layout.tsx b/app/[locale]/(admin)/layout.tsx new file mode 100644 index 0000000..aa42048 --- /dev/null +++ b/app/[locale]/(admin)/layout.tsx @@ -0,0 +1,23 @@ +import LayoutProvider from "@/providers/layout.provider"; +import LayoutContentProvider from "@/providers/content.provider"; +import DashCodeSidebar from "@/components/partials/sidebar"; +import DashCodeFooter from "@/components/partials/footer"; +import ThemeCustomize from "@/components/partials/customizer"; +import DashCodeHeader from "@/components/partials/header"; +import MountedProvider from "@/providers/mounted.provider"; + +const layout = async ({ children }: { children: React.ReactNode }) => { + return ( + + + + + + {children} + + + + ); +}; + +export default layout; diff --git a/app/auth/layout.tsx b/app/[locale]/auth/layout.tsx similarity index 100% rename from app/auth/layout.tsx rename to app/[locale]/auth/layout.tsx diff --git a/app/auth/page.tsx b/app/[locale]/auth/page.tsx similarity index 100% rename from app/auth/page.tsx rename to app/[locale]/auth/page.tsx diff --git a/app/auth/register/page.tsx b/app/[locale]/auth/register/page.tsx similarity index 100% rename from app/auth/register/page.tsx rename to app/[locale]/auth/register/page.tsx diff --git a/app/content/audio/detail/[id]/page.tsx b/app/[locale]/content/audio/detail/[id]/page.tsx similarity index 100% rename from app/content/audio/detail/[id]/page.tsx rename to app/[locale]/content/audio/detail/[id]/page.tsx diff --git a/app/content/image/detail/[id]/page.tsx b/app/[locale]/content/image/detail/[id]/page.tsx similarity index 100% rename from app/content/image/detail/[id]/page.tsx rename to app/[locale]/content/image/detail/[id]/page.tsx diff --git a/app/content/layout.tsx b/app/[locale]/content/layout.tsx similarity index 100% rename from app/content/layout.tsx rename to app/[locale]/content/layout.tsx diff --git a/app/content/text/detail/[id]/page.tsx b/app/[locale]/content/text/detail/[id]/page.tsx similarity index 100% rename from app/content/text/detail/[id]/page.tsx rename to app/[locale]/content/text/detail/[id]/page.tsx diff --git a/app/content/video/comment/[id]/page.tsx b/app/[locale]/content/video/comment/[id]/page.tsx similarity index 100% rename from app/content/video/comment/[id]/page.tsx rename to app/[locale]/content/video/comment/[id]/page.tsx diff --git a/app/content/video/detail/[id]/page.tsx b/app/[locale]/content/video/detail/[id]/page.tsx similarity index 100% rename from app/content/video/detail/[id]/page.tsx rename to app/[locale]/content/video/detail/[id]/page.tsx diff --git a/app/favicon.ico b/app/[locale]/favicon.ico similarity index 100% rename from app/favicon.ico rename to app/[locale]/favicon.ico diff --git a/app/[locale]/globals.css b/app/[locale]/globals.css new file mode 100644 index 0000000..8ea266a --- /dev/null +++ b/app/[locale]/globals.css @@ -0,0 +1,295 @@ +@import "tailwindcss"; +@import "tw-animate-css"; + +@custom-variant dark (&:is(.dark *)); + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --font-sans: var(--font-geist-sans); + --font-mono: var(--font-geist-mono); + --color-sidebar-ring: var(--sidebar-ring); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar: var(--sidebar); + --color-chart-5: var(--chart-5); + --color-chart-4: var(--chart-4); + --color-chart-3: var(--chart-3); + --color-chart-2: var(--chart-2); + --color-chart-1: var(--chart-1); + --color-ring: var(--ring); + --color-input: var(--input); + --color-border: var(--border); + --color-destructive: var(--destructive); + --color-accent-foreground: var(--accent-foreground); + --color-accent: var(--accent); + --color-muted-foreground: var(--muted-foreground); + --color-muted: var(--muted); + --color-secondary-foreground: var(--secondary-foreground); + --color-secondary: var(--secondary); + --color-primary-foreground: var(--primary-foreground); + --color-primary: var(--primary); + --color-popover-foreground: var(--popover-foreground); + --color-popover: var(--popover); + --color-card-foreground: var(--card-foreground); + --color-card: var(--card); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); +} + +:root { + --radius: 0.625rem; + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); +} + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 222.2 84% 4.9%; + + --muted: 210 40% 96.1%; + --muted-foreground: 215.4 16.3% 46.9%; + + --popover: 0 0% 100%; + --popover-foreground: 222.2 84% 4.9%; + + --card: 0 0% 100%; + --card-foreground: 222.2 84% 4.9%; + + --border: 214.3 31.8% 91.4%; + --input: 214.3 31.8% 91.4%; + + --secondary: 210 40% 96.1%; + --secondary-foreground: 215.3 19.3% 34.5%; + + --accent: 210 40% 96.1%; + --accent-foreground: 222.2 47.4% 11.2%; + + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 210 40% 98%; + + --success: 154 52% 55%; + --success-foreground: 138.5 76.5% 96.7%; + + --warning: 16 93% 70%; + --warning-foreground: 33.3 100% 96.5%; + + --info: 185 96% 51%; + --info-foreground: 183.2 100% 96.3%; + + --ring: 222.2 84% 4.9%; + + --radius: 0.5rem; + + --default-50: 210 40% 98%; + --default-100: 210 40% 96.1%; + --default-200: 214.3 31.8% 91.4%; + --default-300: 212.7 26.8% 83.9%; + --default-400: 215 20.2% 65.1%; + --default-500: 215.4 16.3% 46.9%; + --default-600: 215.3 19.3% 34.5%; + --default-700: 215.3 25% 26.7%; + --default-800: 217.2 32.6% 17.5%; + --default-900: 222.2 47.4% 11.2%; + --default-950: 222.2 84% 4.9%; + --default: 222.2 47.4% 11.2%; + --default-foreground: 210 40% 98%; + + --primary-50: 213.8 100% 96.9%; + --primary-100: 214.3 94.6% 92.7%; + --primary-200: 213.3 96.9% 87.3%; + --primary-300: 211.7 96.4% 78.4%; + --primary-400: 213.1 93.9% 67.8%; + --primary-500: 217.2 91.2% 59.8%; + --primary-600: 221.2 83.2% 53.3%; + --primary-700: 224.3 76.3% 48%; + --primary-800: 225.9 70.7% 40.2%; + --primary-900: 224.4 64.3% 32.9%; + --primary-950: 226.2 57% 21%; + --primary: 221.2 83.2% 53.3%; + --primary-foreground: 210 40% 98%; + + --sidebar: 0 0% 100%; + --header: 0 0% 100%; + --menu-arrow: 228, 45%, 98%; + --menu-arrow-active: 212.7 26.8% 83.9%; + --menu-foreground: 215, 20%, 65%; + } + + .dark { + --background: 222.2 47.4% 11.2%; + --foreground: 210 40% 98%; + + --muted: 217.2 32.6% 17.5%; + --muted-foreground: 215 20.2% 65.1%; + + --popover: 222.2 84% 4.9%; + --popover-foreground: 210 40% 98%; + + --card: 215 27.9% 16.9%; + --card-foreground: 210 40% 98%; + + --border: 217.2 32.6% 17.5%; + --input: 217.2 32.6% 17.5%; + + --primary: 217.2 91.2% 59.8%; + --primary-foreground: 222.2 47.4% 11.2%; + + --secondary: 215.3 25% 26.7%; + --secondary-foreground: 210 40% 98%; + + --accent: 217.2 32.6% 17.5%; + --accent-foreground: 210 40% 98%; + + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 210 40% 98%; + + --success: 154 52% 55%; + --success-foreground: 138.5 76.5% 96.7%; + + --warning: 16 93% 70%; + --warning-foreground: 33.3 100% 96.5%; + + --info: 185 96% 51%; + --info-foreground: 183.2 100% 96.3%; + + --ring: 212.7 26.8% 83.9%; + + --default-950: 210 40% 98%; + --default-900: 210 40% 96.1%; + --default-800: 214.3 31.8% 91.4%; + --default-700: 212.7 26.8% 83.9%; + --default-600: 215 20.2% 65.1%; + --default-500: 215.4 16.3% 46.9%; + --default-400: 215.3 19.3% 34.5%; + --default-300: 215.3 25% 26.7%; + --default-200: 217.2 32.6% 17.5%; + --default-100: 222.2 47.4% 11.2%; + --default-50: 222.2 84% 4.9%; + --default: 213.8 100% 96.9%; + --default-foreground: 222.2 47.4% 11.2%; + + --sidebar: 215 27.9% 16.9%; + --header: 215 27.9% 16.9%; + --menu-arrow: 215.3 25% 26.7%; + --menu-arrow-active: 215.3 19.3% 34.5%; + --menu-foreground: 214.3 31.8% 91.4%; + } +} + +@layer base { + * { + @apply border-border; + } + + body { + @apply text-foreground; + } + + .input-group :not(:first-child) input { + border-top-left-radius: 0 !important; + border-bottom-left-radius: 0 !important; + } + + .input-group.merged :not(:first-child) input { + border-left-width: 0 !important; + padding-left: 0px !important; + } + + .input-group :not(:last-child) input { + border-top-right-radius: 0 !important; + border-bottom-right-radius: 0 !important; + } + + .input-group.merged :not(:last-child) input { + border-right: 0px !important; + padding-right: 0px !important; + } + + .no-scrollbar::-webkit-scrollbar { + width: 0px; + } + + .no-scrollbar::-webkit-scrollbar-thumb { + background-color: transparent; + } +} \ No newline at end of file diff --git a/app/[locale]/layout.tsx b/app/[locale]/layout.tsx new file mode 100644 index 0000000..6c2a9e7 --- /dev/null +++ b/app/[locale]/layout.tsx @@ -0,0 +1,92 @@ +import type { Metadata } from "next"; +import { Inter } from "next/font/google"; +import "./globals.css"; +import { ThemeProvider } from "@/providers/theme-provider"; +import MountedProvider from "@/providers/mounted.provider"; +import { Toaster } from "@/components/ui/toaster"; +import { Toaster as SonnerToaster } from "@/components/ui/sonner"; +const inter = Inter({ + subsets: ["latin"], + display: 'swap', + preload: true, + fallback: ['system-ui', 'arial'] +}); +// language +import { getLangDir } from "rtl-detect"; +import { NextIntlClientProvider } from "next-intl"; +import { getMessages } from "next-intl/server"; +import DirectionProvider from "@/providers/direction-provider"; +import AuthProvider from "@/providers/auth.provider"; + +export const metadata: Metadata = { + title: "NetidHub", + description: "NetidHub Platform", + metadataBase: new URL(process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000'), + openGraph: { + title: "NetidHub", + description: "NetidHub Platform", + }, + twitter: { + card: 'summary_large_image', + title: "NetidHub", + description: "NetidHub Platform", + }, + other: { + 'X-DNS-Prefetch-Control': 'on', + }, +}; + +export default async function RootLayout({ + children, + params, +}: Readonly<{ + children: React.ReactNode; + params: Promise<{ locale: string }>; +}>) { + const { locale } = await params; + const messages = await getMessages(); + const direction = getLangDir(locale); + return ( + + + {/* DNS Prefetch for external domains */} + + + + {/* Preconnect to external domains */} + + + + {/* Preload critical fonts */} + + + + + + + + + {children} + + + + + + + + + ); +} diff --git a/app/[locale]/page.tsx b/app/[locale]/page.tsx new file mode 100644 index 0000000..2e7cd5f --- /dev/null +++ b/app/[locale]/page.tsx @@ -0,0 +1,21 @@ +import Category from "@/components/landing-page/category"; +import Footer from "@/components/landing-page/footer"; +import Header from "@/components/landing-page/header"; +import MediaUpdate from "@/components/landing-page/media-update"; +import Navbar from "@/components/landing-page/navbar"; + +export default function Home() { + return ( +
+
+ +
+
+
+ + +
+
+ ); +} diff --git a/app/public/for-you/layout.tsx b/app/[locale]/public/for-you/layout.tsx similarity index 100% rename from app/public/for-you/layout.tsx rename to app/[locale]/public/for-you/layout.tsx diff --git a/app/public/for-you/page.tsx b/app/[locale]/public/for-you/page.tsx similarity index 100% rename from app/public/for-you/page.tsx rename to app/[locale]/public/for-you/page.tsx diff --git a/app/public/publication/bumn/page.tsx b/app/[locale]/public/publication/bumn/page.tsx similarity index 100% rename from app/public/publication/bumn/page.tsx rename to app/[locale]/public/publication/bumn/page.tsx diff --git a/app/public/publication/kl/page.tsx b/app/[locale]/public/publication/kl/page.tsx similarity index 100% rename from app/public/publication/kl/page.tsx rename to app/[locale]/public/publication/kl/page.tsx diff --git a/app/public/publication/layout.tsx b/app/[locale]/public/publication/layout.tsx similarity index 100% rename from app/public/publication/layout.tsx rename to app/[locale]/public/publication/layout.tsx diff --git a/app/public/publication/pemerintah-daerah/page.tsx b/app/[locale]/public/publication/pemerintah-daerah/page.tsx similarity index 100% rename from app/public/publication/pemerintah-daerah/page.tsx rename to app/[locale]/public/publication/pemerintah-daerah/page.tsx diff --git a/app/public/schedule/layout.tsx b/app/[locale]/public/schedule/layout.tsx similarity index 100% rename from app/public/schedule/layout.tsx rename to app/[locale]/public/schedule/layout.tsx diff --git a/app/public/schedule/page.tsx b/app/[locale]/public/schedule/page.tsx similarity index 100% rename from app/public/schedule/page.tsx rename to app/[locale]/public/schedule/page.tsx diff --git a/app/[locale]/theme.css b/app/[locale]/theme.css new file mode 100644 index 0000000..3800566 --- /dev/null +++ b/app/[locale]/theme.css @@ -0,0 +1,63 @@ +.theme-dark { + --sidebar: 222.2 84% 4.9%; + --header: 222.2 84% 4.9%; +} +.theme-rose { + --sidebar: 341, 64%, 43%; + --header: 341, 64%, 43%; + --secondary: 339, 60%, 51%; + --menu-arrow: 339, 60%, 51%; + --menu-arrow-active: 336, 67%, 60%; + +} + +.theme-gray { + --sidebar: 210, 10%, 23%; + --header: 210, 10%, 23%; + + --secondary: 207, 14%, 31%; + --menu-arrow: 207, 14%, 31%; + --menu-arrow-active: 203, 16%, 43%; +} +.theme-steel-blue { + --sidebar: 226, 36%, 39%; + --header: 226, 36%, 39%; + --secondary: 224, 40%, 48%; + --menu-arrow: 224, 40%, 48%; + --menu-arrow-active: 216, 47%, 60%; +} +.theme-purple { + --sidebar: 299, 56%, 19%; + --header: 299, 56%, 19%; + --secondary: 299, 40%, 28%; + --menu-arrow: 299, 40%, 28%; + --menu-arrow-active: 298, 44%, 33%; +} + +.theme-redwood { + --sidebar: 345, 24%, 29%; + --header: 345, 24%, 29%; + --secondary: 346, 26%, 35%; + --menu-arrow: 346, 26%, 35%; + --menu-arrow-active: 344, 27%, 42%; +} +.theme-green { + --sidebar: 164, 64%, 21%; + --header: 164, 64%, 21%; + + --secondary: 164, 68%, 24%; + --menu-arrow: 164, 68%, 24%; + --menu-arrow-active: 163, 69%, 30%; +} +.theme-ocean-blue { + --sidebar: 206, 92%, 35%; + --header: 206, 92%, 35%; + + --secondary: 205, 94%, 39%; + --menu-arrow: 205, 94%, 39%; + --menu-arrow-active: 203, 85%, 48%; +} +.theme-transparent{ + --header: transparent; + box-shadow: none; +} \ No newline at end of file diff --git a/app/globals.css b/app/globals.css deleted file mode 100644 index dc98be7..0000000 --- a/app/globals.css +++ /dev/null @@ -1,122 +0,0 @@ -@import "tailwindcss"; -@import "tw-animate-css"; - -@custom-variant dark (&:is(.dark *)); - -@theme inline { - --color-background: var(--background); - --color-foreground: var(--foreground); - --font-sans: var(--font-geist-sans); - --font-mono: var(--font-geist-mono); - --color-sidebar-ring: var(--sidebar-ring); - --color-sidebar-border: var(--sidebar-border); - --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); - --color-sidebar-accent: var(--sidebar-accent); - --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); - --color-sidebar-primary: var(--sidebar-primary); - --color-sidebar-foreground: var(--sidebar-foreground); - --color-sidebar: var(--sidebar); - --color-chart-5: var(--chart-5); - --color-chart-4: var(--chart-4); - --color-chart-3: var(--chart-3); - --color-chart-2: var(--chart-2); - --color-chart-1: var(--chart-1); - --color-ring: var(--ring); - --color-input: var(--input); - --color-border: var(--border); - --color-destructive: var(--destructive); - --color-accent-foreground: var(--accent-foreground); - --color-accent: var(--accent); - --color-muted-foreground: var(--muted-foreground); - --color-muted: var(--muted); - --color-secondary-foreground: var(--secondary-foreground); - --color-secondary: var(--secondary); - --color-primary-foreground: var(--primary-foreground); - --color-primary: var(--primary); - --color-popover-foreground: var(--popover-foreground); - --color-popover: var(--popover); - --color-card-foreground: var(--card-foreground); - --color-card: var(--card); - --radius-sm: calc(var(--radius) - 4px); - --radius-md: calc(var(--radius) - 2px); - --radius-lg: var(--radius); - --radius-xl: calc(var(--radius) + 4px); -} - -:root { - --radius: 0.625rem; - --background: oklch(1 0 0); - --foreground: oklch(0.145 0 0); - --card: oklch(1 0 0); - --card-foreground: oklch(0.145 0 0); - --popover: oklch(1 0 0); - --popover-foreground: oklch(0.145 0 0); - --primary: oklch(0.205 0 0); - --primary-foreground: oklch(0.985 0 0); - --secondary: oklch(0.97 0 0); - --secondary-foreground: oklch(0.205 0 0); - --muted: oklch(0.97 0 0); - --muted-foreground: oklch(0.556 0 0); - --accent: oklch(0.97 0 0); - --accent-foreground: oklch(0.205 0 0); - --destructive: oklch(0.577 0.245 27.325); - --border: oklch(0.922 0 0); - --input: oklch(0.922 0 0); - --ring: oklch(0.708 0 0); - --chart-1: oklch(0.646 0.222 41.116); - --chart-2: oklch(0.6 0.118 184.704); - --chart-3: oklch(0.398 0.07 227.392); - --chart-4: oklch(0.828 0.189 84.429); - --chart-5: oklch(0.769 0.188 70.08); - --sidebar: oklch(0.985 0 0); - --sidebar-foreground: oklch(0.145 0 0); - --sidebar-primary: oklch(0.205 0 0); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.97 0 0); - --sidebar-accent-foreground: oklch(0.205 0 0); - --sidebar-border: oklch(0.922 0 0); - --sidebar-ring: oklch(0.708 0 0); -} - -.dark { - --background: oklch(0.145 0 0); - --foreground: oklch(0.985 0 0); - --card: oklch(0.205 0 0); - --card-foreground: oklch(0.985 0 0); - --popover: oklch(0.205 0 0); - --popover-foreground: oklch(0.985 0 0); - --primary: oklch(0.922 0 0); - --primary-foreground: oklch(0.205 0 0); - --secondary: oklch(0.269 0 0); - --secondary-foreground: oklch(0.985 0 0); - --muted: oklch(0.269 0 0); - --muted-foreground: oklch(0.708 0 0); - --accent: oklch(0.269 0 0); - --accent-foreground: oklch(0.985 0 0); - --destructive: oklch(0.704 0.191 22.216); - --border: oklch(1 0 0 / 10%); - --input: oklch(1 0 0 / 15%); - --ring: oklch(0.556 0 0); - --chart-1: oklch(0.488 0.243 264.376); - --chart-2: oklch(0.696 0.17 162.48); - --chart-3: oklch(0.769 0.188 70.08); - --chart-4: oklch(0.627 0.265 303.9); - --chart-5: oklch(0.645 0.246 16.439); - --sidebar: oklch(0.205 0 0); - --sidebar-foreground: oklch(0.985 0 0); - --sidebar-primary: oklch(0.488 0.243 264.376); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.269 0 0); - --sidebar-accent-foreground: oklch(0.985 0 0); - --sidebar-border: oklch(1 0 0 / 10%); - --sidebar-ring: oklch(0.556 0 0); -} - -@layer base { - * { - @apply border-border outline-ring/50; - } - body { - @apply bg-background text-foreground; - } -} diff --git a/app/layout.tsx b/app/layout.tsx index c827eeb..a5dccd6 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,30 +1,30 @@ import type { Metadata } from "next"; -import { Geist, Geist_Mono } from "next/font/google"; -import "./globals.css"; +import { Inter } from "next/font/google"; +import "./[locale]/globals.css"; -// const geistSans = Geist({ -// variable: "--font-geist-sans", -// subsets: ["latin"], -// }); - -// const geistMono = Geist_Mono({ -// variable: "--font-geist-mono", -// subsets: ["latin"], -// }); +const inter = Inter({ + subsets: ["latin"], + display: 'swap', + preload: true, + fallback: ['system-ui', 'arial'] +}); export const metadata: Metadata = { title: "NetidHub", - description: "NetidHub", + description: "NetidHub Platform", + metadataBase: new URL(process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000'), }; export default function RootLayout({ children, -}: Readonly<{ +}: { children: React.ReactNode; -}>) { +}) { return ( - - {children} + + + {children} + ); } diff --git a/app/page.tsx b/app/page.tsx index 2e7cd5f..7bf6945 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,21 +1,5 @@ -import Category from "@/components/landing-page/category"; -import Footer from "@/components/landing-page/footer"; -import Header from "@/components/landing-page/header"; -import MediaUpdate from "@/components/landing-page/media-update"; -import Navbar from "@/components/landing-page/navbar"; +import AutoRedirect from '@/components/auto-redirect'; -export default function Home() { - return ( -
-
- -
-
-
- - -
-
- ); +export default function RootPage() { + return ; } diff --git a/components.json b/components.json index 335484f..15f2b02 100644 --- a/components.json +++ b/components.json @@ -1,21 +1,17 @@ { "$schema": "https://ui.shadcn.com/schema.json", - "style": "new-york", + "style": "default", "rsc": true, "tsx": true, "tailwind": { - "config": "", + "config": "tailwind.config.ts", "css": "app/globals.css", - "baseColor": "neutral", + "baseColor": "slate", "cssVariables": true, "prefix": "" }, "aliases": { "components": "@/components", - "utils": "@/lib/utils", - "ui": "@/components/ui", - "lib": "@/lib", - "hooks": "@/hooks" - }, - "iconLibrary": "lucide" + "utils": "@/lib/utils" + } } \ No newline at end of file diff --git a/components/auto-redirect.tsx b/components/auto-redirect.tsx new file mode 100644 index 0000000..6aae662 --- /dev/null +++ b/components/auto-redirect.tsx @@ -0,0 +1,26 @@ +'use client'; + +import { useEffect } from 'react'; +import { useRouter } from '@/components/navigation'; + +export default function AutoRedirect() { + const router = useRouter(); + + useEffect(() => { + // Get current pathname without locale + const pathname = window.location.pathname; + const segments = pathname.split('/').filter(Boolean); + + // Check if first segment is a locale + const locales = ['in', 'en', 'ar']; + const hasLocale = segments.length > 0 && locales.includes(segments[0]); + + if (!hasLocale) { + // Redirect to default locale with current path + const newPath = `/in${pathname}`; + router.replace(newPath); + } + }, [router]); + + return null; +} diff --git a/components/blocks/earning-block.tsx b/components/blocks/earning-block.tsx new file mode 100644 index 0000000..6884bf1 --- /dev/null +++ b/components/blocks/earning-block.tsx @@ -0,0 +1,115 @@ +"use client" +import dynamic from "next/dynamic"; +const Chart = dynamic(() => import("react-apexcharts"), { ssr: false }); +import { Card, CardContent } from '@/components/ui/card'; +import { useTheme } from "next-themes"; +import { cn } from "@/lib/utils"; +import { useTranslations } from "next-intl"; +interface EarningBlockProps { + title?: string, + className?: string; + colors?: string[]; + series?: number[]; + labels?: string[]; + height?: number; + chartType?: 'donut' | 'pie' | 'radialBar'; + total?: number | string; + percentage?: string; +} + +const EarningBlock = ({ + title = "Earnings", + total = "$0", + percentage = "+08%", + series = [70, 30], + chartType = "donut", + height = 200, + labels = ["Success", "Return"], + colors = ["#ffbf99", "#5cffff"], + className = "", +}: EarningBlockProps) => { + const { theme: mode } = useTheme(); + const options: any = { + labels: labels, + dataLabels: { + enabled: false, + }, + tooltip: { + theme: mode === "dark" ? "dark" : "light", + }, + colors: [...colors], + legend: { + position: "bottom", + fontSize: "12px", + fontFamily: "Outfit", + fontWeight: 400, + labels: { + colors: mode === "dark" ? "#cbd5e1" : "#0f172a", + } + }, + + plotOptions: { + pie: { + donut: { + size: "70%", + labels: { + show: true, + name: { + show: false, + fontSize: "14px", + fontWeight: "bold", + fontFamily: "Inter", + }, + value: { + show: true, + fontSize: "16px", + fontFamily: "Outfit", + color: mode === "dark" ? "#cbd5e1" : "#0f172a", + formatter(val: string) { + return `${parseInt(val)}%`; + }, + }, + total: { + show: true, + fontSize: "10px", + label: "", + formatter() { + return "70"; + } + } + } + } + } + } + }; + const t = useTranslations("EcommerceDashboard"); + return ( + + +
+
+
{title}
+
+ {total} +
+
+ {percentage} + {t("statistics_graph_desc", { defaultValue: "Statistics Graph Desc" })} +
+
+
+ +
+
+
+
+ ); +}; + +export default EarningBlock; \ No newline at end of file diff --git a/components/blocks/orders-block.tsx b/components/blocks/orders-block.tsx new file mode 100644 index 0000000..dc78273 --- /dev/null +++ b/components/blocks/orders-block.tsx @@ -0,0 +1,149 @@ +"use client"; +import dynamic from "next/dynamic"; +const Chart = dynamic(() => import("react-apexcharts")); +import { cn } from "@/lib/utils"; +import { Card, CardContent } from "@/components/ui/card"; +import { useTheme } from "next-themes"; +interface OrdersnBlockProps { + className?: string; + series?: number[]; + chartColor?: string; + chartType?: 'area' | 'bar' | 'line' | 'pie' | 'donut' | 'radialBar' + opacity?: number, + title?: string, + total?: number | string, + height?: number, + percentageContent?: React.ReactNode +} + +const OrdersBlock = ({ + series = [15, 30, 15, 30, 20, 35], + chartColor = "#0f172a", + chartType = "bar", + opacity = 1, + className, + title = "Order Block", + total, + height = 42, + percentageContent = -60% +}: OrdersnBlockProps) => { + const { theme: mode } = useTheme(); + const chartSeries = [ + { + data: series + } + ]; + + const options: any = { + chart: { + toolbar: { + autoSelected: "pan", + show: false, + }, + offsetX: 0, + offsetY: 0, + zoom: { + enabled: false, + }, + sparkline: { + enabled: true, + } + }, + + plotOptions: { + bar: { + columnWidth: "60%", + barHeight: "100%", + }, + }, + dataLabels: { + enabled: false, + }, + stroke: { + width: 2, + curve: "straight", + }, + markers: { + size: 3, + colors: chartColor, + strokeColors: chartColor, + strokeWidth: 2, + shape: "circle", + radius: 2, + hover: { + sizeOffset: 1, + }, + }, + colors: [chartColor], + tooltip: { + theme: mode === "dark" ? "dark" : "light", + }, + grid: { + show: false, + padding: { + left: 0, + right: 0, + }, + }, + yaxis: { + show: false, + }, + fill: { + type: "solid", + opacity: [opacity], + }, + legend: { + show: false, + }, + xaxis: { + low: 0, + offsetX: 0, + offsetY: 0, + show: false, + labels: { + low: 0, + offsetX: 0, + show: false, + }, + axisBorder: { + low: 0, + offsetX: 0, + show: false, + } + } + }; + return ( + + + { + title && ( +
+ {title} +
+ ) + } + {total && ( +
+ {total} +
+ ) + } +
+ {percentageContent} + From last Week +
+
+ +
+
+
+ ); +}; + +export default OrdersBlock; \ No newline at end of file diff --git a/components/blocks/progress-block.tsx b/components/blocks/progress-block.tsx new file mode 100644 index 0000000..b75be00 --- /dev/null +++ b/components/blocks/progress-block.tsx @@ -0,0 +1,102 @@ +"use client"; +import dynamic from "next/dynamic"; +const Chart = dynamic(() => import("react-apexcharts"), { ssr: false }); +import { cn } from "@/lib/utils"; +import { useTheme } from "next-themes"; +import { Card, CardContent } from "../ui/card"; + +interface ProgressBlockProps { + title?: string, + className?: string; + colors?: string[]; + series?: number[]; + labels?: string[]; + height?: number; + chartType?: 'donut' | 'pie' | 'radialBar'; +} + +const ProgressBlock = ({ + className, + height = 200, + title, + labels = ["Complete", "Left"], + series = [70, 30], + chartType = "donut", + colors = ["#0CE7FA", "#E2F6FD"] }: ProgressBlockProps) => { + const { theme: mode } = useTheme(); + + const options: any = { + labels: labels, + dataLabels: { + enabled: false, + }, + + colors: [...colors], + legend: { + position: "bottom", + fontSize: "12px", + fontFamily: "Outfit", + fontWeight: 400, + show: false, + }, + tooltip: { + theme: mode === "dark" ? "dark" : "light", + }, + plotOptions: { + pie: { + donut: { + size: "40%", + labels: { + show: true, + name: { + show: false, + fontSize: "14px", + fontWeight: "bold", + fontFamily: "Inter", + }, + value: { + show: true, + fontSize: "16px", + fontFamily: "Outfit", + color: mode === "dark" ? "#cbd5e1" : "#0f172a", + formatter(val: string) { + return `${parseInt(val)}%`; + }, + }, + total: { + show: true, + fontSize: "10px", + label: "", + formatter() { + return "70"; + } + } + } + } + } + } + }; + + return ( + + + { + title && +
+ {title} +
+ } + +
+
+ ); +}; + +export default ProgressBlock; + diff --git a/components/blocks/statistics-block.tsx b/components/blocks/statistics-block.tsx new file mode 100644 index 0000000..cca2ea1 --- /dev/null +++ b/components/blocks/statistics-block.tsx @@ -0,0 +1,127 @@ +"use client" +import dynamic from "next/dynamic"; +import { cn } from "@/lib/utils"; +import { Card, CardContent } from "@/components/ui/card"; +import { useTheme } from "next-themes"; +const Chart = dynamic(() => import("react-apexcharts")); +interface StatsBlock { + className?: string; + title: string; + total?: number | string; + series?: number[]; + chartColor?: string; + chartType?: 'area' | 'bar' | 'line' | 'pie' | 'donut' | 'radialBar'; + opacity?: number +} + +const defaultData = [800, 600, 1000, 800, 600, 1000, 800, 900]; + + +const StatisticsBlock = ({ + title = " Static Block", + total, + className, + series = defaultData, + chartColor = "#00EBFF", + chartType = "area", + opacity = 0.1 + +}: StatsBlock) => { + const { theme: mode } = useTheme(); + const chartSeries = [ + { + data: series + } + ]; + + const options: any = { + chart: { + toolbar: { + autoSelected: "pan", + show: false, + }, + offsetX: 0, + offsetY: 0, + zoom: { + enabled: false, + }, + sparkline: { + enabled: true, + }, + }, + + dataLabels: { + enabled: false, + }, + stroke: { + curve: "smooth", + width: 2, + }, + colors: [chartColor], + + tooltip: { + theme: mode === "dark" ? "dark" : "light", + }, + grid: { + show: false, + padding: { + left: 0, + right: 0, + }, + }, + yaxis: { + show: false, + }, + fill: { + type: "solid", + opacity: [opacity], + }, + legend: { + show: false, + }, + xaxis: { + low: 0, + offsetX: 0, + offsetY: 0, + show: false, + labels: { + low: 0, + offsetX: 0, + show: false, + }, + axisBorder: { + low: 0, + offsetX: 0, + show: false, + } + } + }; + return ( + + + +
+
+ +
+
+
+ {title} +
+
+ {total} +
+
+
+
+
+ ); +}; + +export { StatisticsBlock }; \ No newline at end of file diff --git a/components/blocks/status-block.tsx b/components/blocks/status-block.tsx new file mode 100644 index 0000000..f053ff3 --- /dev/null +++ b/components/blocks/status-block.tsx @@ -0,0 +1,155 @@ +"use client" +import dynamic from "next/dynamic"; +import { cn } from "@/lib/utils"; +import { Card, CardContent } from "@/components/ui/card"; +const Chart = dynamic(() => import("react-apexcharts")); +import { useTheme } from "next-themes"; +interface StatusBlockProps { + className?: string; + icon?: React.ReactNode; + title?: string; + total?: number | string; + series?: number[]; + chartColor?: string; + iconWrapperClass?: string; + chartType?: 'area' | 'bar' | 'line' | 'pie' | 'donut' | 'radialBar' + reverse?: boolean + opacity?: number +} + +const StatusBlock = ({ + title, + total, + className, + icon, + series = [800, 600, 1000, 800, 600, 1000, 800, 900], + chartColor = "#0ce7fa", + iconWrapperClass, + chartType = "area", + reverse = false, + opacity = 0.1 +}: StatusBlockProps) => { + const { theme: mode } = useTheme(); + const chartSeries = [ + { + data: series + } + ]; + + const options: any = { + chart: { + toolbar: { + autoSelected: "pan", + show: false, + }, + offsetX: 0, + offsetY: 0, + zoom: { + enabled: false, + }, + sparkline: { + enabled: true, + }, + }, + plotOptions: { + bar: { + columnWidth: "60%", + barHeight: "100%", + }, + }, + dataLabels: { + enabled: false, + }, + stroke: { + curve: "smooth", + width: 2, + }, + colors: [chartColor], + + tooltip: { + theme: mode === "dark" ? "dark" : "light", + }, + + grid: { + show: false, + padding: { + left: 0, + right: 0, + }, + }, + yaxis: { + show: false, + }, + fill: { + type: "solid", + opacity: [opacity], + }, + legend: { + show: false, + }, + xaxis: { + low: 0, + offsetX: 0, + offsetY: 0, + show: false, + labels: { + low: 0, + offsetX: 0, + show: false, + }, + axisBorder: { + low: 0, + offsetX: 0, + show: false, + } + } + }; + return ( + + +
+ { + icon && +
+
+ {icon} +
+
+ } + + {(title || total) && ( +
+ {title && ( +
+ {title} +
+ )} + {total && ( +
+ {total} +
+ )} +
+ )} + +
+
+ +
+
+
+ + ); +}; + +export { StatusBlock }; \ No newline at end of file diff --git a/components/blocks/upgrade-block.tsx b/components/blocks/upgrade-block.tsx new file mode 100644 index 0000000..4dc9d51 --- /dev/null +++ b/components/blocks/upgrade-block.tsx @@ -0,0 +1,20 @@ + +import { cn } from "@/lib/utils" + +interface UpgradeProps { + image?: any; + children: React.ReactNode; + className?: string; +} +const UpgradeBlock = ({ children, className }: UpgradeProps) => { + return ( +
+ {children} +
+ ); +}; + + +export { UpgradeBlock }; \ No newline at end of file diff --git a/components/blocks/welcome-block.tsx b/components/blocks/welcome-block.tsx new file mode 100644 index 0000000..f4f3e5a --- /dev/null +++ b/components/blocks/welcome-block.tsx @@ -0,0 +1,27 @@ + +import { cn } from "@/lib/utils" +interface WelcomeProps { + image?: any; + children: React.ReactNode; + badge?: string; + className?: string; +} +const WelcomeBlock = ({ children, className, }: WelcomeProps) => { + return ( +
+ {children} +
+ ); +}; + +const BlockBadge = ({ children, className }: { children: React.ReactNode, className?: string }) => { + return ( +
+ {children} +
+ ) +} + +export { WelcomeBlock, BlockBadge }; \ No newline at end of file diff --git a/components/code-snippet.tsx b/components/code-snippet.tsx new file mode 100644 index 0000000..e9ce7a1 --- /dev/null +++ b/components/code-snippet.tsx @@ -0,0 +1,70 @@ +"use client"; +import React, { useState, useEffect } from "react"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; + +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible"; +import { Label } from "@/components/ui/label"; +import { Switch } from "@/components/ui/switch"; +import { CopyButton } from "./copy-button"; +import SyntaxHighlighter from "react-syntax-highlighter"; +import { atomOneDark } from "react-syntax-highlighter/dist/esm/styles/hljs"; +const CardSnippet = ({ title, code, children }: { title: string, code: string, children: React.ReactNode }) => { + const [show, setShow] = useState(false); + const toggle = () => { + setShow(!show); + }; + + return ( + + + {title && ( + {title} + )} + {code && ( +
+ + +
+ )} +
+ + {children} + + +
+
+ + {`${code}`} + +
+
+
+
+ ); +}; + +export default CardSnippet; \ No newline at end of file diff --git a/components/copy-button.tsx b/components/copy-button.tsx new file mode 100644 index 0000000..6421c4d --- /dev/null +++ b/components/copy-button.tsx @@ -0,0 +1,69 @@ +"use client" + +import * as React from "react" +import { CheckIcon, ClipboardIcon } from "lucide-react" + +import { Event, trackEvent } from "@/lib/events" +import { cn } from "@/lib/utils" +import { Button, ButtonProps } from "@/components/ui/button" +import { + TooltipProvider, + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip" + +export function CopyButton({ + event, + name, + code, + className, + tooltip = "Copy code", + ...props +}: { + event: Event["name"] + name: string + code: string + tooltip?: string +} & ButtonProps) { + const [hasCopied, setHasCopied] = React.useState(false) + + React.useEffect(() => { + setTimeout(() => { + setHasCopied(false) + }, 2000) + }, [hasCopied]) + + return ( + + + + + + + {tooltip} + + + + ) +} diff --git a/components/dascode-logo.tsx b/components/dascode-logo.tsx new file mode 100644 index 0000000..4a64528 --- /dev/null +++ b/components/dascode-logo.tsx @@ -0,0 +1,18 @@ +import React from 'react' +type IconProps = React.HTMLAttributes +const DashCodeLogo = (props: IconProps) => { + return ( + <> + + + + + + + + ) +} + +export default DashCodeLogo \ No newline at end of file diff --git a/components/dashboard-dropdown.tsx b/components/dashboard-dropdown.tsx new file mode 100644 index 0000000..e755c19 --- /dev/null +++ b/components/dashboard-dropdown.tsx @@ -0,0 +1,38 @@ + +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { MoreHorizontal } from "lucide-react"; +import { useTranslations } from "next-intl"; +const DashboardDropdown = () => { + const t = useTranslations("AnalyticsDashboard"); + return ( + + + + + + + {t("last_28_days", { defaultValue: "Last 28 Days" })} + + + {t("last_months", { defaultValue: "Last Months" })} + + + {t("last_year", { defaultValue: "Last Year" })} + + + + ); +}; + +export default DashboardDropdown; \ No newline at end of file diff --git a/components/date-range-picker.tsx b/components/date-range-picker.tsx new file mode 100644 index 0000000..755fea8 --- /dev/null +++ b/components/date-range-picker.tsx @@ -0,0 +1,59 @@ +"use client"; + +import * as React from "react"; +import { format } from "date-fns"; +import { Calendar as CalendarIcon } from "lucide-react"; + +import { cn } from "@/lib/utils"; +import { Button } from "@/components/ui/button"; +import { Calendar } from "@/components/ui/calendar"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { useTheme } from "next-themes"; + +export default function DateRangePicker({ className }: { className?: string }) { + const [date, setDate] = React.useState(null); + const { theme: mode } = useTheme(); + + return ( +
+ + + + + + + + +
+ ); +} diff --git a/components/delete-confirmation-dialog.tsx b/components/delete-confirmation-dialog.tsx new file mode 100644 index 0000000..5f48309 --- /dev/null +++ b/components/delete-confirmation-dialog.tsx @@ -0,0 +1,70 @@ +import React, { useState, useTransition } from "react"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; +import { toast } from "react-hot-toast"; +import { Loader2 } from "lucide-react"; + +const DeleteConfirmationDialog = ({ + open, + onClose, + onConfirm, + defaultToast = true, + toastMessage = "Successfully deleted", +}: { + open: boolean; + onClose: () => void; + onConfirm?: () => Promise; + defaultToast?: boolean; + toastMessage?: string; +}) => { + const [isPending, startTransition] = useTransition(); + + const handleConfirm = () => { + if (!onConfirm) { + onClose(); + return; + } + onConfirm(); + + onClose(); + if (defaultToast) { + toast.success(toastMessage, { + position: "top-right", + }); + } + }; + + return ( + + + + Are you absolutely sure? + + This action cannot be undone. This will permanently delete your + account and remove your data from our servers. + + + + Cancel + startTransition(handleConfirm)} + > + {isPending && } + {isPending ? "Deleting.." : "Continue"} + + + + + ); +}; + +export default DeleteConfirmationDialog; diff --git a/components/editor/index.tsx b/components/editor/index.tsx new file mode 100644 index 0000000..d98ed47 --- /dev/null +++ b/components/editor/index.tsx @@ -0,0 +1,101 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import dynamic from 'next/dynamic'; +import SimpleEditor from './simple-editor'; + +// Dynamic import untuk CKEditor dengan error handling +const CKEditorWrapper = dynamic( + () => import('./ckeditor-wrapper'), + { + ssr: false, + loading: () => ( +
+
Loading editor...
+
+ ) + } +); + +interface EditorProps { + data?: string; + onChange?: (data: string) => void; + onReady?: (editor: any) => void; + onBlur?: (event: any, editor: any) => void; + onFocus?: (event: any, editor: any) => void; + config?: any; + disabled?: boolean; + className?: string; + fallbackToSimple?: boolean; +} + +export default function Editor({ + data = '', + onChange, + onReady, + onBlur, + onFocus, + config = {}, + disabled = false, + className = '', + fallbackToSimple = true +}: EditorProps) { + const [useSimpleEditor, setUseSimpleEditor] = useState(false); + const [isMounted, setIsMounted] = useState(false); + + useEffect(() => { + setIsMounted(true); + }, []); + + useEffect(() => { + // Check if CKEditor is available + const checkCKEditor = async () => { + try { + await import('@ckeditor/ckeditor5-react'); + await import('ckeditor5'); + setUseSimpleEditor(false); + } catch (error) { + console.warn('CKEditor not available, falling back to simple editor:', error); + if (fallbackToSimple) { + setUseSimpleEditor(true); + } + } + }; + + if (isMounted) { + checkCKEditor(); + } + }, [isMounted, fallbackToSimple]); + + if (!isMounted) { + return ( +
+
Loading editor...
+
+ ); + } + + if (useSimpleEditor) { + return ( + + ); + } + + return ( + + ); +} diff --git a/components/editor/simple-editor.tsx b/components/editor/simple-editor.tsx new file mode 100644 index 0000000..602ca20 --- /dev/null +++ b/components/editor/simple-editor.tsx @@ -0,0 +1,142 @@ +'use client'; + +import { useState, useRef, useEffect } from 'react'; +import { Button } from '@/components/ui/button'; +import { + Bold, + Italic, + Underline, + List, + ListOrdered, + Quote, + Link, + Undo, + Redo, + Type, + AlignLeft, + AlignCenter, + AlignRight +} from 'lucide-react'; + +interface SimpleEditorProps { + data?: string; + onChange?: (data: string) => void; + placeholder?: string; + className?: string; + disabled?: boolean; +} + +export default function SimpleEditor({ + data = '', + onChange, + placeholder = 'Start typing...', + className = '', + disabled = false +}: SimpleEditorProps) { + const [content, setContent] = useState(data); + const editorRef = useRef(null); + const [isMounted, setIsMounted] = useState(false); + + useEffect(() => { + setIsMounted(true); + }, []); + + useEffect(() => { + setContent(data); + }, [data]); + + const handleInput = () => { + if (editorRef.current) { + const html = editorRef.current.innerHTML; + setContent(html); + onChange?.(html); + } + }; + + const execCommand = (command: string, value?: string) => { + document.execCommand(command, false, value); + editorRef.current?.focus(); + handleInput(); + }; + + const insertLink = () => { + const url = prompt('Enter URL:'); + if (url) { + execCommand('createLink', url); + } + }; + + const toolbarButtons = [ + { command: 'bold', icon: Bold, label: 'Bold' }, + { command: 'italic', icon: Italic, label: 'Italic' }, + { command: 'underline', icon: Underline, label: 'Underline' }, + { command: 'insertUnorderedList', icon: List, label: 'Bullet List' }, + { command: 'insertOrderedList', icon: ListOrdered, label: 'Numbered List' }, + { command: 'formatBlock', icon: Quote, label: 'Quote', value: 'blockquote' }, + { command: 'justifyLeft', icon: AlignLeft, label: 'Align Left' }, + { command: 'justifyCenter', icon: AlignCenter, label: 'Align Center' }, + { command: 'justifyRight', icon: AlignRight, label: 'Align Right' }, + { command: 'undo', icon: Undo, label: 'Undo' }, + { command: 'redo', icon: Redo, label: 'Redo' }, + ]; + + if (!isMounted) { + return ( +
+
Loading editor...
+
+ ); + } + + return ( +
+ {/* Toolbar */} +
+ {toolbarButtons.map((button) => { + const IconComponent = button.icon; + return ( + + ); + })} + +
+ + {/* Editor Content */} +
+
+ ); +} diff --git a/components/layout/admin-layout.tsx b/components/layout/admin-layout.tsx deleted file mode 100644 index 537bcdb..0000000 --- a/components/layout/admin-layout.tsx +++ /dev/null @@ -1,89 +0,0 @@ -"use client"; - -import { useEffect, useState } from "react"; -import React, { ReactNode } from "react"; -import { ThemeProvider } from "./theme-context"; -import { Breadcrumbs } from "./breadcrumbs"; -import { BurgerButtonIcon } from "../icons"; - -import { motion, AnimatePresence } from "framer-motion"; -import { SidebarProvider } from "./sidebar-context"; -import { RetractingSidebar } from "../landing-page/retracting-sidedar"; - -export const AdminLayout = ({ children }: { children: ReactNode }) => { - const [isOpen, setIsOpen] = useState(true); - const [hasMounted, setHasMounted] = useState(false); - - const updateSidebarData = (newData: boolean) => { - setIsOpen(newData); - }; - - // Hooks - useEffect(() => { - setHasMounted(true); - }, []); - - // Render loading state until mounted - if (!hasMounted) { - return ( -
-
-
- ); - } - - return ( - - -
-
- - - - - {/* Header */} - -
-
- - -
-
-
- - {/* Main Content */} - -
{children}
-
-
-
-
-
-
-
- ); -}; diff --git a/components/layout/sidebar-context.tsx b/components/layout/sidebar-context.tsx deleted file mode 100644 index a566005..0000000 --- a/components/layout/sidebar-context.tsx +++ /dev/null @@ -1,58 +0,0 @@ -'use client' -import React, { createContext, useContext, useEffect, useState } from 'react'; - -interface SidebarContextType { - isOpen: boolean; - toggleSidebar: () => void; -} - -const SidebarContext = createContext(undefined); - -export const SidebarProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { - const [isOpen, setIsOpen] = useState(() => { - if (typeof window !== 'undefined') { - const storedValue = localStorage.getItem('sidebarOpen'); - return storedValue ? JSON.parse(storedValue) : false; - } - }); - - const toggleSidebar = () => { - setIsOpen(!isOpen); - }; - - useEffect(() => { - localStorage.setItem('sidebarOpen', JSON.stringify(isOpen)); - }, [isOpen]); - - useEffect(() => { - const handleResize = () => { - setIsOpen(window.innerWidth > 768); // Ganti 768 dengan lebar yang sesuai dengan breakpoint Anda - }; - - handleResize(); // Pastikan untuk memanggil fungsi handleResize saat komponen dimuat - - window.addEventListener('resize', handleResize); - - return () => { - window.removeEventListener('resize', handleResize); - }; - }, []); - - return ( - - {children} - - ); -}; - -export const useSidebar = () => { - const context = useContext(SidebarContext); - if (!context) { - throw new Error('useSidebar must be used within a SidebarProvider'); - } - return context; -}; - -export const useSidebarContext = () => { - return useContext(SidebarContext); -}; \ No newline at end of file diff --git a/components/layout/theme-context.tsx b/components/layout/theme-context.tsx deleted file mode 100644 index b498a01..0000000 --- a/components/layout/theme-context.tsx +++ /dev/null @@ -1,67 +0,0 @@ -"use client"; - -import React, { createContext, useContext, useEffect, useState } from 'react'; - -type Theme = 'light' | 'dark'; - -interface ThemeContextType { - theme: Theme; - toggleTheme: () => void; - setTheme: (theme: Theme) => void; -} - -const ThemeContext = createContext(undefined); - -export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { - const [theme, setThemeState] = useState('light'); - const [mounted, setMounted] = useState(false); - - useEffect(() => { - setMounted(true); - // Get theme from localStorage or default to 'light' - const savedTheme = localStorage.getItem('theme') as Theme; - if (savedTheme) { - setThemeState(savedTheme); - } else { - // Check system preference - const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; - setThemeState(systemTheme); - } - }, []); - - useEffect(() => { - if (!mounted) return; - - // Update document class and localStorage - document.documentElement.classList.remove('light', 'dark'); - document.documentElement.classList.add(theme); - localStorage.setItem('theme', theme); - }, [theme, mounted]); - - const toggleTheme = () => { - setThemeState(prev => prev === 'light' ? 'dark' : 'light'); - }; - - const setTheme = (newTheme: Theme) => { - setThemeState(newTheme); - }; - - // Prevent hydration mismatch - if (!mounted) { - return <>{children}; - } - - return ( - - {children} - - ); -}; - -export const useTheme = () => { - const context = useContext(ThemeContext); - if (context === undefined) { - throw new Error('useTheme must be used within a ThemeProvider'); - } - return context; -}; \ No newline at end of file diff --git a/components/loader.tsx b/components/loader.tsx new file mode 100644 index 0000000..aeb8123 --- /dev/null +++ b/components/loader.tsx @@ -0,0 +1,34 @@ + +'use client' +import React from "react"; +import { Loader2 } from "lucide-react"; +import DashCodeLogo from "./dascode-logo"; +import { useMounted } from "@/hooks/use-mounted"; +import Image from "next/image"; +const Loader = () => { + const mounted = useMounted() + return ( + mounted ? null :
+
+ {/* */} + + {/*

+ DashCode +

*/} +
+ + + Loading... + +
+ ); +}; + +export default Loader; + diff --git a/components/logo.tsx b/components/logo.tsx new file mode 100644 index 0000000..941e434 --- /dev/null +++ b/components/logo.tsx @@ -0,0 +1,42 @@ +"use client"; +import React from "react"; +import DashCodeLogo from "./dascode-logo"; +import { Link } from "@/i18n/routing"; +import { useConfig } from "@/hooks/use-config"; +import { useMenuHoverConfig } from "@/hooks/use-menu-hover"; +import { useMediaQuery } from "@/hooks/use-media-query"; + +const Logo = () => { + const [config] = useConfig(); + const [hoverConfig] = useMenuHoverConfig(); + const { hovered } = hoverConfig; + const isDesktop = useMediaQuery("(min-width: 1280px)"); + + if (config.sidebar === "compact") { + return ( + + + + ); + } + if (config.sidebar === "two-column" || !isDesktop) return null; + + return ( + + {/* + {(!config?.collapsed || hovered) && ( +

D

+ )} */} + logo + + ); +}; + +export default Logo; diff --git a/components/main/dashboard/dashboard-container.tsx b/components/main/dashboard/dashboard-container.tsx index 4f2e0e1..61d8d44 100644 --- a/components/main/dashboard/dashboard-container.tsx +++ b/components/main/dashboard/dashboard-container.tsx @@ -125,18 +125,19 @@ export default function DashboardContainer() { return (
{/* Stats Cards */} -
+
{/* User Profile Card */}
-

{username}

-
+

Welcome back,

+

{username}

+

Admin Dashboard

@@ -146,7 +147,7 @@ export default function DashboardContainer() { {/* Total Posts */}
-

- {summary?.totalAll} +

+ {summary?.totalAll || 0}

-

Total Posts

+

Total Posts

{/* Total Views */}
-

- {summary?.totalViews} +

+ {summary?.totalViews || 0}

-

Total Views

+

Total Views

{/* Total Shares */}
-

- {summary?.totalShares} +

+ {summary?.totalShares || 0}

-

Total Shares

+

Total Shares

{/* Total Comments */}
-

- {summary?.totalComments} +

+ {summary?.totalComments || 0}

-

Total Comments

+

Total Comments

{/* Content Section */} -
+
{/* Analytics Chart */}
-

+

Analytics Overview

@@ -250,25 +251,28 @@ export default function DashboardContainer() { handleChange(option.value, checked as boolean) } /> - {option.label} + {option.label} ))}
+
+

Chart will be displayed here

+
{/* Recent Articles */}
-

+

Recent Content

- + @@ -279,7 +283,7 @@ export default function DashboardContainer() { {article?.map((list: any) => ( @@ -291,10 +295,10 @@ export default function DashboardContainer() { className="h-20 w-20 object-cover rounded-lg shadow-sm flex-shrink-0" />
-

+

{list?.title}

-

+

{convertDateFormat(list?.createdAt)}

diff --git a/components/maps/Map copy.js b/components/maps/Map copy.js new file mode 100644 index 0000000..1c0197d --- /dev/null +++ b/components/maps/Map copy.js @@ -0,0 +1,365 @@ +import Cookies from "js-cookie"; +import React, { Component } from "react"; +import Geocode from "react-geocode"; +import Autocomplete from "react-google-autocomplete"; +import { + GoogleMap, + InfoWindow, + Marker, + withGoogleMap, + withScriptjs, +} from "react-google-maps"; +import { GoogleMapsAPI } from "./client-config"; + +Geocode.setApiKey(GoogleMapsAPI); +Geocode.enableDebug(); + +class Map extends Component { + constructor(props) { + super(props); + this.state = { + address: "", + city: "", + area: "", + state: "", + mapPosition: { + lat: this.props.center.lat, + lng: this.props.center.lng, + }, + markerPosition: { + lat: this.props.center.lat, + lng: this.props.center.lng, + }, + }; + } + + /** + * Get the current address from the default map position and set those values in the state + */ + componentDidMount() { + Geocode.fromLatLng( + this.state.mapPosition.lat, + this.state.mapPosition.lng, + ).then( + (response) => { + const address = response.results[0].formatted_address; + const addressArray = response.results[0].address_components; + const city = this.getCity(addressArray); + const area = this.getArea(addressArray); + const state = this.getState(addressArray); + + console.log("city", city, area, state); + + this.setState({ + address: address || "", + area: area || "", + city: city || "", + state: state || "", + }); + }, + (error) => { + console.error(error); + }, + ); + } + + /** + * Component should only update ( meaning re-render ), when the user selects the address, or drags the pin + * + * @param nextProps + * @param nextState + * @return {boolean} + */ + shouldComponentUpdate(nextProps, nextState) { + if ( + this.state.markerPosition.lat !== this.props.center.lat || + this.state.address !== nextState.address || + this.state.city !== nextState.city || + this.state.area !== nextState.area || + this.state.state !== nextState.state + ) { + return true; + } + + if (this.props.center.lat == nextProps.center.lat) { + return false; + } + } + + /** + * Get the city and set the city input value to the one selected + * + * @param addressArray + * @return {string} + */ + getCity = (addressArray) => { + let city = ""; + + for (const element of addressArray) { + if ( + element.types[0] && + element.types[0] == "administrative_area_level_2" + ) { + city = element.long_name; + return city; + } + } + }; + /** + * Get the area and set the area input value to the one selected + * + * @param addressArray + * @return {string} + */ + getArea = (addressArray) => { + let area = ""; + + for (const element of addressArray) { + if (element.types[0]) { + for (let j = 0; j < element.types.length; j++) { + if ( + element.types[j] == "sublocality_level_1" || + element.types[j] == "locality" + ) { + area = element.long_name; + return area; + } + } + } + } + }; + /** + * Get the address and set the address input value to the one selected + * + * @param addressArray + * @return {string} + */ + getState = (addressArray) => { + let state = ""; + + for (let i = 0; i < addressArray.length; i++) { + for (const element of addressArray) { + if ( + element.types[0] && + element.types[0] == "administrative_area_level_1" + ) { + state = element.long_name; + return state; + } + } + } + }; + /** + * And function for city,state and address input + * @param event + */ + onChange = (event) => { + this.setState({ + [event.target.name]: event.target.value, + }); + }; + /** + * This Event triggers when the marker window is closed + * + * @param event + */ + onInfoWindowClose = () => {}; + /** + * When the marker is dragged you get the lat and long using the functions available from event object. + * Use geocode to get the address, city, area and state from the lat and lng positions. + * And then set those values in the state. + * + * @param event + */ + onMarkerDragEnd = (event) => { + const newLat = event.latLng.lat(); + const newLng = event.latLng.lng(); + + Geocode.fromLatLng(newLat, newLng).then( + (response) => { + const address = response.results[0].formatted_address; + const addressArray = response.results[0].address_components; + const city = this.getCity(addressArray); + const area = this.getArea(addressArray); + const state = this.getState(addressArray); + + this.setState({ + address: address || "", + area: area || "", + city: city || "", + state: state || "", + markerPosition: { + lat: newLat, + lng: newLng, + }, + mapPosition: { + lat: newLat, + lng: newLng, + }, + }); + Cookies.set("map_lat", `${newLat}`, { + expires: 1 + }); + Cookies.set("map_long", `${newLng}`, { + expires: 1 + }); + $(".input-location-schedule").val(address); + }, + (error) => { + console.error(error); + }, + ); + }; + /** + * When the user types an address in the search box + * @param place + */ + onPlaceSelected = (place) => { + console.log("plc", place); + const address = place.formatted_address; + const addressArray = place.address_components; + const city = this.getCity(addressArray); + const area = this.getArea(addressArray); + const state = this.getState(addressArray); + const latValue = place.geometry.location.lat(); + const lngValue = place.geometry.location.lng(); + + // Set these values in the state. + this.setState({ + address: address || "", + area: area || "", + city: city || "", + state: state || "", + markerPosition: { + lat: latValue, + lng: lngValue, + }, + mapPosition: { + lat: latValue, + lng: lngValue, + }, + }); + Cookies.set("map_lat", `${latValue}`, { + expires: 1 + }); + Cookies.set("map_long", `${lngValue}`, { + expires: 1 + }); + $(".input-location-schedule").val(address); + }; + render() { + const AsyncMap = withScriptjs( + withGoogleMap(() => ( + + + {/* InfoWindow on top of marker */} + +
+ + {this.state.address} + +
+
+ {/* Marker */} + + + {/* For Auto complete Search Box */} +
+ )), + ); + + let map; + + if (this.props.center.lat == undefined) { + map = ( +
+ ); + } else { + map = ( +
+ {/*
+
+ + +
+
+ + +
+
*/} + + } + containerElement={ +
+ } + mapElement={ +
+ } + /> +
+ ); + } + + return map; + } +} + +export default Map; diff --git a/components/maps/Map.tsx b/components/maps/Map.tsx new file mode 100644 index 0000000..cac3b81 --- /dev/null +++ b/components/maps/Map.tsx @@ -0,0 +1,52 @@ +import React from "react"; +import Geocode from "react-geocode"; +import { GoogleMap, Marker, useLoadScript } from "@react-google-maps/api"; +import { GoogleMapsAPI } from "./client-config"; + +Geocode.setApiKey(GoogleMapsAPI); + +interface MapProps { + lat: number; + lng: number; + draggable?: boolean; + onLocationChange?: (location: string) => void; // Tambahkan onLocationChange +} + +function Map({ lat, lng, draggable, onLocationChange }: MapProps) { + const containerStyle = { width: "100%", height: "400px" }; + const [selected, setSelected] = React.useState<{ + lat: number; + lng: number; + } | null>(null); + + const onMarkerDragEnd = async (e: google.maps.MapMouseEvent) => { + const lat = e.latLng?.lat() ?? 0; + const lng = e.latLng?.lng() ?? 0; + + try { + const response = await Geocode.fromLatLng(lat.toString(), lng.toString()); + const address = response.results[0].formatted_address; + + setSelected({ lat, lng }); + if (onLocationChange) { + onLocationChange(address); // Panggil callback jika tersedia + } + } catch (error) { + console.error(error); + } + }; + + return ( + + + + ); +} diff --git a/components/maps/MapHome.tsx b/components/maps/MapHome.tsx new file mode 100644 index 0000000..691d4fa --- /dev/null +++ b/components/maps/MapHome.tsx @@ -0,0 +1,29 @@ +import React, { Component } from "react"; +import Places from "./Maps"; +interface MapHomeProps { + newLat?: any; + newLng?: any; + draggable?: boolean; + setLocation: (location: string) => void; +} + +class MapHome extends Component { + render() { + const { newLat, newLng, draggable, setLocation } = this.props; + + const lat = newLat || -6.2393033; + const lng = newLng || 106.8013579; + + return ( +
+ +
+ ); + } +} + +export default MapHome; diff --git a/components/maps/Maps.tsx b/components/maps/Maps.tsx new file mode 100644 index 0000000..853b74c --- /dev/null +++ b/components/maps/Maps.tsx @@ -0,0 +1,159 @@ +import { GoogleMap, Marker, useLoadScript } from "@react-google-maps/api"; +import Cookies from "js-cookie"; +import { useEffect, useState } from "react"; +import usePlacesAutocomplete, { + getGeocode, + getLatLng, +} from "use-places-autocomplete"; +import { GoogleMapsAPI } from "./client-config"; +import Geocode from "react-geocode"; +import { PlacesCombobox } from "@/components/ui/combobox"; + +Geocode.setApiKey(GoogleMapsAPI); + +export default function Places(props: { + center: { lat: number; lng: number }; + draggable?: boolean; + onLocationChange?: (location: string) => void; +}) { + const { isLoaded } = useLoadScript({ + googleMapsApiKey: GoogleMapsAPI, + libraries: ["places"], + language: "id", + }); + + const { center, draggable, onLocationChange } = props; + + if (!isLoaded) return
Loading...
; + + return ( + + ); +} + +interface MapProps { + lat: number; + lng: number; + draggable?: boolean; + onLocationChange?: (location: string) => void; +} + +function Map(props: MapProps) { + const containerStyle = { + width: "100%", + height: "400px", + }; + + const center = { + lat: -6.1754, + lng: 106.8272, + }; + + const [selected, setSelected] = useState<{ lat: number; lng: number } | null>( + null + ); + + const { lat, lng, draggable, onLocationChange } = props; + + useEffect(() => { + if (lat !== undefined && lng !== undefined) { + setSelected({ lat, lng }); + getAddressFromLatLong(lat, lng); + } + }, [lat, lng]); + + const onMarkerDragEnd = (e: google.maps.MapMouseEvent) => { + const lat = e.latLng?.lat() ?? 0; + const lng = e.latLng?.lng() ?? 0; + console.log(lat, lng); + getAddressFromLatLong(lat, lng); + if (onLocationChange) { + onLocationChange(`Latitude: ${lat}, Longitude: ${lng}`); + } + }; + + async function getAddressFromLatLong(lat: number, lng: number) { + try { + const response = await Geocode.fromLatLng(lat.toString(), lng.toString()); + const address = response.results[0].formatted_address; + Cookies.set("map_lat", `${lat}`, { expires: 1 }); + Cookies.set("map_long", `${lng}`, { expires: 1 }); + console.log("Address:", address); + if (onLocationChange) { + onLocationChange(address); + } + } catch (error) { + console.error(error); + } + } + + return ( + <> +
+ +
+ + {selected && ( + + )} + + + ); +} + +interface PlacesAutocompleteProps { + setSelected: (coords: { lat: number; lng: number }) => void; +} + +function PlacesAutocomplete({ setSelected }: PlacesAutocompleteProps) { + const { + ready, + value, + setValue, + suggestions: { status, data }, + clearSuggestions, + } = usePlacesAutocomplete(); + + const handleSelect = async (address: string) => { + setValue(address, false); + clearSuggestions(); + + try { + const results = await getGeocode({ address }); + const { lat, lng } = await getLatLng(results[0]); + + setSelected({ lat, lng }); + console.log("Selected Lat/Lng:", { lat, lng }); + Cookies.set("map_lat", `${lat}`, { expires: 1 }); + Cookies.set("map_long", `${lng}`, { expires: 1 }); + } catch (error) { + console.error("Error fetching coordinates:", error); + } + }; + + return ( + setValue(newValue)} + onSelect={handleSelect} + suggestions={data} + status={status} + disabled={!ready} + placeholder="Cari Alamat" + className="border" + /> + ); +} diff --git a/components/maps/client-config.tsx b/components/maps/client-config.tsx new file mode 100644 index 0000000..5b6e64a --- /dev/null +++ b/components/maps/client-config.tsx @@ -0,0 +1 @@ +export const GoogleMapsAPI = "AIzaSyCuQHorDceMCzlSgrB9AEY5ns8KeriFsME"; diff --git a/components/nav.tsx b/components/nav.tsx new file mode 100644 index 0000000..69551ea --- /dev/null +++ b/components/nav.tsx @@ -0,0 +1,61 @@ +'use client' +import React from 'react' +import { cn } from '@/lib/utils' +import { Icon } from '@/components/ui/icon' +import { Button } from "@/components/ui/button" +interface navProps { + dotStyle?: boolean + links: { + title: string + href?: string + active: boolean + label?: string + icon?: any + }[] +} +const Nav = ({ links, dotStyle = false }: navProps) => { + if (dotStyle) { + return ( + + ) + } + return ( + + ) +} + +export default Nav diff --git a/components/navigation.ts b/components/navigation.ts index 86e4b05..9ac0445 100644 --- a/components/navigation.ts +++ b/components/navigation.ts @@ -1,5 +1,8 @@ -// import {createSharedPathnamesNavigation} from 'next-intl/navigation'; -// import {locales} from '@/config'; +import { createSharedPathnamesNavigation } from 'next-intl/navigation'; +import { locales } from '@/config'; -// export const {Link, redirect, usePathname, useRouter} = -// createSharedPathnamesNavigation({locales,}); +export const { Link, redirect, usePathname, useRouter } = + createSharedPathnamesNavigation({ + locales: locales, + defaultLocale: "in", + }); diff --git a/components/page-title.tsx b/components/page-title.tsx new file mode 100644 index 0000000..d5cc348 --- /dev/null +++ b/components/page-title.tsx @@ -0,0 +1,269 @@ +"use client"; +import React, { useEffect, useState } from "react"; +import DateRangePicker from "@/components/date-range-picker"; +import { usePathname } from "@/components/navigation"; +import { cn, getCookiesDecrypt } from "@/lib/utils"; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "./ui/dialog"; +import { Button } from "./ui/button"; +import { Label } from "./ui/label"; +import { Input } from "./ui/input"; +import pdfGenerator from "@/utils/pdf-generator"; +import { + tableauSignin, + tableauViewImage, +} from "@/service/tableau/tableau-service"; + +const PageTitle = ({ + title, + className, +}: { + title?: string; + className?: string; +}) => { + const [reportDate, setReportDate] = useState(); + const pathname = usePathname(); + const name = pathname?.split("/").slice(1).join(" "); + const roleId = getCookiesDecrypt("urie"); + + useEffect(() => { + console.log("role", roleId); + }, [roleId]); + + const downloadReport = async () => { + console.log(reportDate); + const formattedDate = `${reportDate.year}-${String( + reportDate.month + ).padStart(2, "0")}-${String(reportDate.day).padStart(2, "0")}`; + + const resLogin = await tableauSignin(); + const token = resLogin?.data?.data?.credentials?.token; + + const resFrontCover = await tableauViewImage( + token, + "5b35ec9c-add5-45e7-b71a-31a28db9954d", + reportDate + ); + const resAssignmentDetail = await tableauViewImage( + token, + "b2e7e184-8311-49e1-8314-a6b2d8a8c6f8", + reportDate + ); + // const resMonitoringContent = await tableauViewImage( + // token, + // "34683f52-1029-49e4-950d-2c94159ebd8f", + // reportDate + // ); + // const resMonitoringArticle = await tableauViewImage( + // token, + // "34683f52-1029-49e4-950d-2c94159ebd8f", + // reportDate + // ); + const resInteractionContent = await tableauViewImage( + token, + "8f038669-4135-4693-96e5-e6a957a1cc4f", + reportDate + ); + const resTotalInteractionContent = await tableauViewImage( + token, + "e019e7f7-8d3b-4303-aa20-3fdbb318df1e", + reportDate + ); + const resInteractionContentCategory1 = await tableauViewImage( + token, + "c1dcc6c8-17dc-4e30-85db-7938e30b4418", + reportDate + ); + const resInteractionContentCategory2 = await tableauViewImage( + token, + "68b95c1d-aee1-4073-afb7-05188eccee67", + reportDate + ); + const resInteractionContentCategory3 = await tableauViewImage( + token, + "7de1f07f-9a09-45a8-8ff6-96484106492d", + reportDate + ); + const resInteractionDistribution = await tableauViewImage( + token, + "0781d7e6-e133-416c-bf41-e5b2fa5996dd", + reportDate + ); + const resAssignments = await tableauViewImage( + token, + "b2e7e184-8311-49e1-8314-a6b2d8a8c6f8", + reportDate + ); + const resUserCount = await tableauViewImage( + token, + "36ff675d-790c-4420-b0c7-0a6441f59cb1", + reportDate + ); + const resUserCountDistribution = await tableauViewImage( + token, + "86370e20-b13d-4cb0-a54a-c25dc13bb427", + reportDate + ); + const resBackCover = await tableauViewImage( + token, + "516c8790-ccd5-44dc-bb66-b0e146d7168b", + reportDate + ); + const blobFrontCover = new Blob([resFrontCover.data], { + type: "image/png", + }); + const blobAssignmentDetail = new Blob([resAssignmentDetail.data], { + type: "image/png", + }); + // const blobMonitoringContent = new Blob([resMonitoringContent.data], { type: "image/png" }); + // const blobMonitoringArticle = new Blob([resMonitoringArticle.data], { type: "image/png" }); + const blobInteractionContent = new Blob([resInteractionContent.data], { + type: "image/png", + }); + const blobTotalInteractionContent = new Blob( + [resTotalInteractionContent.data], + { type: "image/png" } + ); + const blobInteractionContentCategory1 = new Blob( + [resInteractionContentCategory1.data], + { type: "image/png" } + ); + const blobInteractionContentCategory2 = new Blob( + [resInteractionContentCategory2.data], + { type: "image/png" } + ); + const blobInteractionContentCategory3 = new Blob( + [resInteractionContentCategory3.data], + { type: "image/png" } + ); + const blobInteractionDistribution = new Blob( + [resInteractionDistribution.data], + { type: "image/png" } + ); + const blobAssignments = new Blob([resAssignments.data], { + type: "image/png", + }); + const blobUserCount = new Blob([resUserCount.data], { type: "image/png" }); + const blobUserCountDistribution = new Blob( + [resUserCountDistribution.data], + { type: "image/png" } + ); + const blobBackCover = new Blob([resBackCover.data], { type: "image/png" }); + + console.log(blobFrontCover); + + await pdfGenerator([ + blobFrontCover, + blobAssignmentDetail, + // blobMonitoringContent, + // blobMonitoringArticle, + blobInteractionContent, + blobTotalInteractionContent, + blobInteractionContentCategory1, + blobInteractionContentCategory2, + blobInteractionContentCategory3, + blobInteractionDistribution, + blobAssignments, + blobUserCount, + blobUserCountDistribution, + blobBackCover, + ]); + }; + + async function mergeImagesToCanvas(blobs: Blob[]) { + try { + const images = await Promise.all( + blobs.map((blob) => { + return new Promise((resolve) => { + const url = URL.createObjectURL(blob); + const img = new Image(); + img.onload = () => { + resolve(img); + URL.revokeObjectURL(url); + }; + img.src = url; + }); + }) + ); + + // Hitung total tinggi dari semua gambar (stacked vertically) + const width = Math.max(...images.map((img) => img.width)); + const height = images.reduce((sum, img) => sum + img.height, 0); + + const canvas = document.getElementById("pdf-canvas") as HTMLCanvasElement; + const ctx = canvas.getContext("2d")!; + canvas.width = width; + canvas.height = height; + + let y = 0; + for (const img of images) { + ctx.drawImage(img, 0, y, img.width, img.height); + y += img.height; + } + + // Simpan hasil sebagai gambar (PNG) atau bisa print + const dataURL = canvas.toDataURL("image/png"); + + // Simpan atau cetak (print sebagai PDF) + const link = document.createElement("a"); + link.href = dataURL; + link.download = "merged-image.png"; + link.click(); + + // Atau tampilkan dan user bisa "Print to PDF" + window.open(dataURL, "_blank"); + } catch (error) { + console.log("Error :::", error); + } + } + + return Number(roleId) == 2 || Number(roleId) == 11 || Number(roleId) == 12 ? ( + "" + ) : ( +
+
+ Dashboard +
+ {/* + + + + + + Download Report + +
+
+ + + setReportDate(e.target.value)} + className="w-full" + /> +
+
+ + + +
+
*/} +
+ ); +}; + +export default PageTitle; diff --git a/components/partials/auth/copyright.tsx b/components/partials/auth/copyright.tsx new file mode 100644 index 0000000..826356c --- /dev/null +++ b/components/partials/auth/copyright.tsx @@ -0,0 +1,8 @@ + + +const Copyright = () => { + const currentYear = new Date().getFullYear(); + return <>Copyright {currentYear}, Dashcode All Rights Reserved.; +}; + +export default Copyright; diff --git a/components/partials/auth/forgot-pass.tsx b/components/partials/auth/forgot-pass.tsx new file mode 100644 index 0000000..9e114ea --- /dev/null +++ b/components/partials/auth/forgot-pass.tsx @@ -0,0 +1,74 @@ +"use client"; +import React, { useState } from "react"; + +import { Label } from "@/components/ui/label"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +type Inputs = { + example: string; + exampleRequired: string; +}; +import { useForm, SubmitHandler } from "react-hook-form"; +import { error, loading } from "@/config/swal"; +import withReactContent from "sweetalert2-react-content"; +import Swal from "sweetalert2"; +import { useRouter } from "@/i18n/routing"; +import { forgotPassword } from "@/service/landing/landing"; + +const ForgotPass = () => { + const [username, setUsername] = useState(); + const MySwal = withReactContent(Swal); + const router = useRouter(); + + const { + register, + handleSubmit, + watch, + formState: { errors }, + } = useForm(); + const onSubmit: SubmitHandler = (data) => console.log(data); + + async function handleCheckUsername() { + loading(); + const response = await forgotPassword(username); + + if (response.error) { + error(response.message); + return false; + } + + successSubmit(); + return false; + } + + function successSubmit() { + MySwal.fire({ + title: "Email berhasil dikirim. Silahkan cek email Anda.", + icon: "success", + confirmButtonColor: "#3085d6", + confirmButtonText: "OK", + }).then((result: any) => { + if (result.isConfirmed) { + router.push("/admin"); + } + }); + } + + return ( +
+
+ + setUsername(e.target.value)} /> +
+ + + +
+ ); +}; + +export default ForgotPass; diff --git a/components/partials/auth/lock.tsx b/components/partials/auth/lock.tsx new file mode 100644 index 0000000..1752db0 --- /dev/null +++ b/components/partials/auth/lock.tsx @@ -0,0 +1,40 @@ +"use client"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { useForm, SubmitHandler } from "react-hook-form"; + +type Inputs = { + example: string; + exampleRequired: string; +}; + +const LockScreen = () => { + const { + register, + handleSubmit, + watch, + formState: { errors }, + } = useForm(); + const onSubmit: SubmitHandler = (data) => console.log(data); + console.log(watch("example")); + + return ( +
+
+ + +
+ + +
+ ); +}; +export default LockScreen; diff --git a/components/partials/auth/login-form.tsx b/components/partials/auth/login-form.tsx new file mode 100644 index 0000000..ec9e36e --- /dev/null +++ b/components/partials/auth/login-form.tsx @@ -0,0 +1,652 @@ +"use client"; +import React, { useEffect, useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import Cookies from "js-cookie"; +import { Icon } from "@/components/ui/icon"; +import { useForm, SubmitHandler } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import { cn, getCookiesDecrypt, setCookiesEncrypt } from "@/lib/utils"; +import { Eye, EyeOff, Loader2 } from "lucide-react"; +import { + getProfile, + login, + postEmailValidation, + postSetupEmail, + requestOTP, + verifyOTPByUsername, +} from "@/service/auth"; +import { toast } from "sonner"; +import { useRouter } from "@/components/navigation"; +import { warning } from "@/lib/swal"; +import { Link } from "@/i18n/routing"; +import { useTranslations } from "next-intl"; +import { + InputOTP, + InputOTPGroup, + InputOTPSeparator, + InputOTPSlot, +} from "@/components/ui/input-otp"; +import { error, loading } from "@/config/swal"; +import { data } from "jquery"; +import { + Dialog, + DialogContent, + DialogFooter, + DialogTrigger, +} from "@/components/ui/dialog"; +import { getUserNotifications, listRole } from "@/service/landing/landing"; + +// Schema validasi menggunakan zod +const schema = z.object({ + username: z.string().min(1, { message: "Judul diperlukan" }), + password: z + .string() + .min(4, { message: "Password must be at least 4 characters." }), +}); + +// Tipe untuk form values +type LoginFormValues = { + username: string; + password: string; +}; + +const LoginForm = () => { + const [isPending, startTransition] = React.useTransition(); + const router = useRouter(); + const [passwordType, setPasswordType] = React.useState("password"); + const t = useTranslations("LandingPage"); + + const [step, setStep] = useState(1); + const [otpValue, setOtpValue] = useState(""); + const [userIdentity] = useState(); + const [email, setEmail] = useState(); + const [category, setCategory] = useState("5"); + const roleId = getCookiesDecrypt("urie"); + + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [oldEmail, setOldEmail] = useState(""); + const [oldEmailValidate, setOldEmailValidate] = useState(""); + const [role, setRole] = useState(); + const [menuActive, setMenuActive] = useState(); + const [notifications, setNotifications] = useState([]); + const [notificationsUpdate, setNotificationsUpdate] = useState([]); + const [newEmail, setNewEmail] = useState(""); + const [newEmailValidate, setNewEmailValidate] = useState(""); + const [otpValidate, setOtpValidate] = useState(""); + const [showPassword, setShowPassword] = useState(false); + + const togglePasswordType = () => { + setPasswordType((prevType) => + prevType === "password" ? "text" : "password" + ); + }; + + const { + register, + handleSubmit, + getValues, + formState: { errors }, + } = useForm({ + resolver: zodResolver(schema), + mode: "all", + }); + + const handleTypeOTP = (event: React.KeyboardEvent) => { + const { key } = event; + const target = event.currentTarget; + + if (key === "Enter") { + event.preventDefault(); + + const inputs = Array.from(target.form?.querySelectorAll("input") || []); + const currentIndex = inputs.indexOf(target); + const nextInput = inputs[currentIndex + 1] as HTMLElement | undefined; + + if (nextInput) { + nextInput.focus(); + } + } + }; + + const onSubmit: SubmitHandler = async (data) => { + try { + const response = await login({ + ...data, + grantType: "password", + clientId: "mediahub-app", + }); + + console.log("LOGIN: ", response); + + if (response?.error) { + toast.error("Username / Password Tidak Sesuai"); + } else { + const { access_token } = response?.data; + const { refresh_token } = response?.data; + const dateTime = new Date(); + const newTime = dateTime.getTime() + 10 * 60 * 1000; + + Cookies.set("access_token", access_token, { + expires: 1, + }); + Cookies.set("refresh_token", refresh_token, { + expires: 1, + }); + Cookies.set("time_refresh", new Date(newTime).toISOString(), { + expires: 1, + }); + + Cookies.set("is_first_login", String(true), { + secure: true, + sameSite: "strict", + }); + const profile = await getProfile(access_token); + console.log("PROFILE : ", profile?.data?.data); + + Cookies.set("home_path", profile?.data?.data?.homePath, { + expires: 1, + }); + Cookies.set( + "profile_picture", + profile?.data?.data?.profilePictureUrl, + { + expires: 1, + } + ); + Cookies.set("state", profile?.data?.data?.userLevel?.name, { + expires: 1, + }); + Cookies.set( + "state-prov", + profile.data?.data?.userLevel?.province?.provName, + { + expires: 1, + } + ); + setCookiesEncrypt("uie", profile?.data?.data?.id, { + expires: 1, + }); + setCookiesEncrypt("urie", profile?.data?.data?.roleId, { + expires: 1, + }); + setCookiesEncrypt("urne", profile?.data?.data?.role?.name, { + expires: 1, + }); + setCookiesEncrypt("ulie", profile?.data?.data?.userLevel?.id, { + expires: 1, + }); + setCookiesEncrypt( + "uplie", + profile?.data?.data?.userLevel?.parentLevelId, + { + expires: 1, + } + ); + setCookiesEncrypt( + "ulne", + profile?.data?.data?.userLevel?.levelNumber, + { + expires: 1, + } + ); + setCookiesEncrypt("ufne", profile?.data?.data?.fullname, { + expires: 1, + }); + setCookiesEncrypt("ulnae", profile?.data?.data?.userLevel?.name, { + expires: 1, + }); + setCookiesEncrypt("uinse", profile?.data?.data?.instituteId, { + expires: 1, + }); + console.log("ssaddd", profile?.data?.data?.roleId); + if ( + Number(profile?.data?.data?.roleId) == 2 || + Number(profile?.data?.data?.roleId) == 3 || + Number(profile?.data?.data?.roleId) == 4 || + Number(profile?.data?.data?.roleId) == 9 || + Number(profile?.data?.data?.roleId) == 10 || + Number(profile?.data?.data?.roleId) == 11 || + Number(profile?.data?.data?.roleId) == 12 || + Number(profile?.data?.data?.roleId) == 18 || + Number(profile?.data?.data?.roleId) == 19 + ) { + if (profile?.data?.data?.roleId === 18) { + window.location.href = "/in/dashboard/executive-data"; + // router.push('/admin/dashboard'); + Cookies.set("status", "login", { + expires: 1, + }); + } else if (profile?.data?.data?.roleId === 2) { + window.location.href = "/in/dashboard/executive"; + Cookies.set("status", "login", { + expires: 1, + }); + } else if ( + profile?.data?.data?.userLevel?.id == 794 || + profile?.data?.data?.userLevel?.parentLevelId == 761 + ) { + window.location.href = "/in/dashboard"; + Cookies.set("status", "login", { + expires: 1, + }); + } else { + window.location.href = "/in/dashboard"; + // router.push('/admin/dashboard'); + Cookies.set("status", "login", { + expires: 1, + }); + } + } else { + window.location.href = "/"; + Cookies.set("status", "login", { + expires: 1, + }); + } + } + } catch (err: any) { + toast.error(err.message || "An unexpected error occurred."); + } + // startTransition( () => { + + // }); + }; + + const handleEmailValidation = async () => { + const data = getValues(); + + // loading(); + const response = await postEmailValidation(data); + + if (response?.error) { + error(response?.message); + return false; + } + const msg = response?.data?.message; + if (msg == "Continue to setup email") { + setStep(2); + } else if (msg == "Email is valid and OTP has been sent") { + setStep(3); + } else if (msg == "Username & password valid") { + onSubmit(data); + } else { + setStep(1); + } + }; + + + const handleSetupEmail = async () => { + const values = getValues(); + const data = { + username: values.username, + password: values.password, + oldEmail: oldEmail, + newEmail: newEmail, + }; + + // loading(); + const response = await postSetupEmail(data); + // close(); + if (response?.error) { + error(response.message); + return false; + } + const msg = response?.data?.message; + if (msg == "Email is valid and OTP has been sent") { + setStep(3); + } else if (msg == "The old email is not same") { + error("Email is invalid"); + } + }; + + const checkEmail = (state: any, e: any) => { + const regEmail = + /^(([^\s"(),.:;<>@[\\\]]+(\.[^\s"(),.:;<>@[\\\]]+)*)|(".+"))@((\[(?:\d{1,3}\.){3}\d{1,3}])|(([\dA-Za-z\-]+\.)+[A-Za-z]{2,}))$/; + + if (regEmail.test(e)) { + if (state == "old") { + setOldEmailValidate(""); + setOldEmail(e); + } else { + setNewEmailValidate(""); + setNewEmail(e); + } + } else { + if (state == "old") { + setOldEmailValidate("Email tidak valid"); + setOldEmail(""); + } else { + setNewEmailValidate("Email tidak valid"); + setNewEmail(""); + } + } + }; + + const handleLoginOTP = async () => { + // const otp = `${otp1}${otp2}${otp3}${otp4}${otp5}${otp6}`; + const values = getValues(); + + if (otpValue.length === 6) { + loading(); + const response = await verifyOTPByUsername(values.username, otpValue); + + if (response?.error) { + error(response.message); + return false; + } + + close(); + + if (response?.message === "success") { + onSubmit(values); + } else { + setOtpValidate("Kode OTP Tidak Valid"); + } + } + }; + + let menu = ""; + + useEffect(() => { + async function initState() { + setMenuActive(menu); + const res = await listRole(); + setRole(res?.data?.data); + } + + async function getNotif() { + if (roleId != undefined) { + const response = await getUserNotifications(0, 2); + setNotifications(response?.data?.data?.content); + console.log("respon:", response); + } + } + + async function getNotifUpdate() { + if (roleId != undefined) { + const response = await getUserNotifications(0, 3); + setNotificationsUpdate(response?.data?.data?.content); + console.log("Notiffff:", response); + } + } + + initState(); + getNotif(); + getNotifUpdate(); + }, []); + + return ( + <> + {step !== 3 && ( +
+ {step === 1 && ( + <> +
+

+ {t("logInPlease", { defaultValue: "Log In Please" })} +

+
+ {t("acc", { defaultValue: "Acc" })} + + + + {t("register", { defaultValue: "Register" })} + + + +
+

+ {t("categoryReg", { defaultValue: "Category Reg" })} +

+

+ {t("selectOne", { defaultValue: "Select One" })} +

+
+
+ {role?.map((row: any) => ( +
+ + setCategory(event.target.value) + } + /> + +
+ ))} +
+
+ + + {t("next", { defaultValue: "Next" })}{" "} + + +
+
+
+
+ +
+ + + {errors.username?.message && ( +
+ {errors.username.message} +
+ )} +
+ +
+ +
+ + +
+ {errors.password?.message && ( +
+ {errors.password.message} +
+ )} +
+ +
+
+ + +
+ + {t("forgotPass", { defaultValue: "Forgot Pass" })} + +
+ + + + )} + + {step === 2 && ( + <> +
+

+ Anda perlu memasukkan email baru untuk bisa Login. +

+
+
+
+ + checkEmail("old", e.target.value)} + id="oldEmail" + type="email" + className={cn("", { + "border-destructive": errors.username, + })} + /> +

{oldEmailValidate}

+ {errors.username?.message && ( +
+ {errors.username.message} +
+ )} +
+ +
+ + checkEmail("new", e.target.value)} + id="newEmail" + type="email" + className={cn("", { + "border-destructive": errors.username, + })} + /> +

{newEmailValidate}

+ {errors.username?.message && ( +
+ {errors.username.message} +
+ )} +
+
+ + + )} +
+ )} + + {step === 3 && ( +
+
+

+ {t("pleaseEnterOtp", { defaultValue: "Please Enter Otp" })} +

+
+ +
+ setOtpValue(val)} + > + + + + + + + + + + + + + + + +
+ + {otpValidate && ( +

+ {otpValidate} +

+ )} + + +
+ )} + + ); +}; + +export default LoginForm; diff --git a/components/partials/auth/logo.tsx b/components/partials/auth/logo.tsx new file mode 100644 index 0000000..cace5c3 --- /dev/null +++ b/components/partials/auth/logo.tsx @@ -0,0 +1,24 @@ +'use client' +import Image from 'next/image'; +import { useTheme } from "next-themes"; + +const Logo = () => { + const { theme: mode } = useTheme(); + return ( +
+ +
+ ); +} + +export default Logo; \ No newline at end of file diff --git a/components/partials/auth/reg-form.tsx b/components/partials/auth/reg-form.tsx new file mode 100644 index 0000000..f326018 --- /dev/null +++ b/components/partials/auth/reg-form.tsx @@ -0,0 +1,72 @@ + +"use client"; +import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { useForm, SubmitHandler } from "react-hook-form"; + +type Inputs = { + example: string; + exampleRequired: string; +}; + +const RegForm = () => { + const { + register, + handleSubmit, + watch, + formState: { errors }, + } = useForm(); + const onSubmit: SubmitHandler = (data) => console.log(data); + console.log(watch("example")); + + return ( +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+ ); +}; +export default RegForm; diff --git a/components/partials/auth/social.tsx b/components/partials/auth/social.tsx new file mode 100644 index 0000000..9401d66 --- /dev/null +++ b/components/partials/auth/social.tsx @@ -0,0 +1,75 @@ +import Image from "next/image"; + +const Social = ({ locale }: { locale: string }) => { + return ( + <> +
    +
  • + + + +
  • +
  • + + + +
  • +
  • + + + +
  • +
  • +
    { + // "use server"; + // await signIn("google", { redirectTo: `/${locale}/dashboard/analytics` }); + // }} + > + +
    +
  • +
+ + ); +}; + +export default Social; diff --git a/components/partials/customizer/buy-button.tsx b/components/partials/customizer/buy-button.tsx new file mode 100644 index 0000000..26f0893 --- /dev/null +++ b/components/partials/customizer/buy-button.tsx @@ -0,0 +1,28 @@ +'use client' +import { useTheme } from 'next-themes'; +import React from 'react' +import { Button } from '@/components/ui/button'; +import Link from 'next/link'; +import { useMediaQuery } from '@/hooks/use-media-query'; + +const BuyButton = () => { + const { theme: mode } = useTheme(); + const isMobile = useMediaQuery("(min-width: 768px)"); + return ( + <> + + + + ) +} + +export default BuyButton \ No newline at end of file diff --git a/components/partials/customizer/color-schema.tsx b/components/partials/customizer/color-schema.tsx new file mode 100644 index 0000000..954777a --- /dev/null +++ b/components/partials/customizer/color-schema.tsx @@ -0,0 +1,64 @@ +"use client"; + +import * as React from "react"; +import { useTheme } from "next-themes"; +import { hexToRGB } from "@/lib/utils"; +import { cn } from "@/lib/utils"; +import { Label } from "@/components/ui/label"; +import { Icon } from "@/components/ui/icon" + +import { Check } from "lucide-react"; + +const allThemes = [ + { key: "light", label: "Light", icon: "heroicons:sun" }, + { key: "dark", label: "Dark", icon: "heroicons:moon" }, + { key: "system", label: "System", icon: "heroicons:window" }, + +]; + +const ColorSchema = () => { + const { theme, setTheme, resolvedTheme: mode } = useTheme(); + + return ( +
+
+ Color Scheme +
+ +
+ {allThemes?.map((themeOption) => ( +
+ + +
+ ))} +
+
+ ); +}; + +export default ColorSchema; diff --git a/components/partials/customizer/copy-cutomizer.tsx b/components/partials/customizer/copy-cutomizer.tsx new file mode 100644 index 0000000..eed7e78 --- /dev/null +++ b/components/partials/customizer/copy-cutomizer.tsx @@ -0,0 +1,22 @@ +'use client' + +import { CopyButton } from "@/components/copy-button" +import { useConfig } from "@/hooks/use-config" + +const CopyCustomizer = () => { + const [config] = useConfig() + const code = JSON.stringify(config, null, 2) + return ( + + ) +} + +export default CopyCustomizer \ No newline at end of file diff --git a/components/partials/customizer/data.tsx b/components/partials/customizer/data.tsx new file mode 100644 index 0000000..ebcf352 --- /dev/null +++ b/components/partials/customizer/data.tsx @@ -0,0 +1,2104 @@ +export const verticalLayoutSvg = ( + + + + + + + + + + + + + + + + + + + + + + + + +); +export const horizontalLayoutSvg = ( + + + + + + + + + + + +); +export const semiBoxLayoutSvg = ( + + + + + + + + + + + + + + + + + +); +export const compactLayoutSvg = ( + + + + + + + + + + + + + + + + + +); +export const classicSidebarSvg = ( + + + + + + + + + + + + + + + + + +); +export const draggableSidebarSvg = ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +); +export const twoColumnSidebarSvg = ( + + + + + + + + + + + + + + + + + + + + + + + +); +export const compactSidebarSvg = ( + + + + + + + + + + + + + + + + + +); +export const pinnedSidebarSvg = ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +); +export const wideContentWidthSvg = ( + + + + + + + + + + + +); +export const boxedContentWidthSvg = ( + + + + + + + + + + + + + + + + + +); +export const defaultSkinSvg = ( + + + + + + + + + + + + + + + + + +); +export const borderedSkinSvg = ( + + + + + + + + + + + + + + + + + +); +export const defaultSidebarColorSvg = ( + + + + + + + + + + + + + + + + + +); +export const colorSidebarColorSvg = ( + + + + + + + + + + + + + + + + + +); +export const gradientSidebarColorSvg = ( + + + + + + + + + + + + + + + + + + + + + + + +); +export const lightTopbarColorSvg = ( + + + + + + + + + + + + + + + + + + + + + + + +); +export const colorTopbarColorSvg = ( + + + + + + + + + + + + + + + + + +); diff --git a/components/partials/customizer/footer-style.tsx b/components/partials/customizer/footer-style.tsx new file mode 100644 index 0000000..2c79f97 --- /dev/null +++ b/components/partials/customizer/footer-style.tsx @@ -0,0 +1,42 @@ +"use client"; +import { useConfig } from "@/hooks/use-config"; +import { Check } from "lucide-react"; +import { cn } from "@/lib/utils"; +import { Label } from "@/components/ui/label"; +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; + +const FooterStyle = () => { + const [config, setConfig] = useConfig(); + + const { footer } = config; + + return ( +
+
Footer Type
+ + { + setConfig({ ...config, footer: value as any }); + }} + + > + {["sticky", "hidden", "default"].map((value, index) => { + return ( +
+ + +
+ ); + })} +
+
+ ); +}; + +export default FooterStyle; diff --git a/components/partials/customizer/full-screen.tsx b/components/partials/customizer/full-screen.tsx new file mode 100644 index 0000000..cc7492e --- /dev/null +++ b/components/partials/customizer/full-screen.tsx @@ -0,0 +1,52 @@ +'use client' +import { FC, MouseEventHandler } from "react"; +import { Button } from "@/components/ui/button"; +import { Maximize } from "lucide-react"; + + +type CustomDocument = Document & { + mozCancelFullScreen?: () => void; +}; +const FullScreenToggle: FC = () => { + const toggleFullScreen: MouseEventHandler = () => { + const doc = document; + const docEl = doc.documentElement; + + const requestFullScreen = + docEl.requestFullscreen || + docEl.requestFullscreen || + docEl.requestFullscreen || + docEl.requestFullscreen; + const cancelFullScreen = + doc.exitFullscreen || + (doc as CustomDocument).mozCancelFullScreen || + doc.exitFullscreen || + doc.exitFullscreen; + + if ( + !doc.fullscreenElement && + !doc.fullscreenElement && + !doc.fullscreenElement && + !doc.fullscreenElement + ) { + requestFullScreen?.call(docEl); + } else { + cancelFullScreen?.call(doc); + } + }; + + return ( + + ); +}; + +export default FullScreenToggle; \ No newline at end of file diff --git a/components/partials/customizer/header-color.tsx b/components/partials/customizer/header-color.tsx new file mode 100644 index 0000000..dc5aa76 --- /dev/null +++ b/components/partials/customizer/header-color.tsx @@ -0,0 +1,186 @@ +'use client' +import React from 'react' +import { cn } from "@/lib/utils" +import { Label } from "@/components/ui/label" +import { useConfig } from '@/hooks/use-config' +import { Icon } from "@/components/ui/icon" +import { Check } from 'lucide-react'; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip" + +import { + lightTopbarColorSvg, + colorTopbarColorSvg, + borderedSkinSvg, +} from "./data"; + +const HeaderColor = () => { + const [config, setConfig] = useConfig() + const [show, setShow] = React.useState( + config.headerTheme !== 'light' && config.headerTheme === 'transparent' + ) + + return ( +
+
Topbar Color
+
+
+ + +
+
+ + +
+
+ + +
+
+ + {show && ( +
+ {[ + "dark", + "rose", + "steel-blue", + "purple", + "redwood", + "green", + "ocean-blue", + "gray", + ].map((color) => ( +
+ + + + + + +

{color}

+
+
+
+
+ ))} +
+ )} +
+ ); +} + +export default HeaderColor \ No newline at end of file diff --git a/components/partials/customizer/header-style.tsx b/components/partials/customizer/header-style.tsx new file mode 100644 index 0000000..3c3b848 --- /dev/null +++ b/components/partials/customizer/header-style.tsx @@ -0,0 +1,40 @@ +"use client"; +import { useConfig } from "@/hooks/use-config"; +import { Check } from "lucide-react"; +import { cn } from "@/lib/utils"; +import { Label } from "@/components/ui/label" +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group" +import { navBarType } from "@/lib/type"; +const HeaderStyle = () => { + + const [config, setConfig] = useConfig(); + + const { navbar } = config; + + + return ( +
+
Navbar Type
+ { + setConfig({ ...config, navbar: value as navBarType }); + }} + > + {["floating", "sticky", "hidden", "default"].map((value, index) => { + return ( +
+ + +
+ ); + })} +
+
+ ); +}; + +export default HeaderStyle; diff --git a/components/partials/customizer/index.tsx b/components/partials/customizer/index.tsx new file mode 100644 index 0000000..83bbf61 --- /dev/null +++ b/components/partials/customizer/index.tsx @@ -0,0 +1,128 @@ +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Sheet, + SheetClose, + SheetContent, + SheetDescription, + SheetFooter, + SheetHeader, + SheetTitle, + SheetTrigger, +} from "@/components/ui/sheet"; +import { Link } from '@/i18n/routing'; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Icon } from "@/components/ui/icon"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import ColorSchema from "./color-schema"; +import SetSkin from "./set-skin"; +import MenuHidden from "./menu-hidden"; +import SearchBarToggle from "./search-bar-toggle"; +import TeamSwitcherToggle from "./team-switcher-toggle"; +import SetContentWidth from "./set-content-width"; +import SetLayout from "./set-layout"; +import SetSidebar from "./set-sidebar"; +import SidebarColor from "./sidebar-color"; +import HeaderColor from "./header-color"; +import SidebarBg from "./sidebar-bg"; +import HeaderStyle from "./header-style"; +import FooterStyle from "./footer-style"; +import ResetConfig from "./reset-config"; +import FullScreenToggle from "./full-screen"; +import CopyCustomizer from "./copy-cutomizer"; +import BuyButton from "./buy-button"; + +const ThemeCustomize = ({ }) => { + return ( + + + + + + + +
+

+ Template Customizer +

+

+ Customize and preview in real time +

+
+
+ + +
+
+
+ +
+ + + + Theme Style + + + Theme Color + + + +
+ + +
+
+ + +
+ + + +
+ +
+ + +
+ +
+
+ + + + +
+ +
+
+
+
+
+ {/* + + */} +
+
+ ); +}; + +export default ThemeCustomize; diff --git a/components/partials/customizer/menu-hidden.tsx b/components/partials/customizer/menu-hidden.tsx new file mode 100644 index 0000000..7f8b4d8 --- /dev/null +++ b/components/partials/customizer/menu-hidden.tsx @@ -0,0 +1,32 @@ +'use client' +import React from 'react' +import { Checkbox } from "@/components/ui/checkbox" +import { useConfig } from '@/hooks/use-config' + +const MenuHidden = () => { + const [config, setConfig] = useConfig() + + if (config.layout === 'horizontal') return null + + return ( +
+ + setConfig({ ...config, menuHidden: !config.menuHidden }) + } + + /> + +
+ ); +} + +export default MenuHidden \ No newline at end of file diff --git a/components/partials/customizer/reset-config.tsx b/components/partials/customizer/reset-config.tsx new file mode 100644 index 0000000..449ea14 --- /dev/null +++ b/components/partials/customizer/reset-config.tsx @@ -0,0 +1,26 @@ +'use client' +import React from 'react' +import { RefreshCcw } from 'lucide-react'; +import { useConfig, defaultConfig } from '@/hooks/use-config'; + + +const ResetConfig = () => { + const [config, setConfig] = useConfig() + return ( + <> + { + setConfig({ + + + ...defaultConfig + + }) + }} + + className=" h-4 w-4 cursor-pointer text-default-600 hover:rotate-180 duration-150 me-4" /> + + ) +} + +export default ResetConfig \ No newline at end of file diff --git a/components/partials/customizer/search-bar-toggle.tsx b/components/partials/customizer/search-bar-toggle.tsx new file mode 100644 index 0000000..8edae12 --- /dev/null +++ b/components/partials/customizer/search-bar-toggle.tsx @@ -0,0 +1,29 @@ +'use client' +import React from 'react' +import { Checkbox } from "@/components/ui/checkbox" +import { useConfig } from '@/hooks/use-config' + +const SearchBarToggle = () => { + const [config, setConfig] = useConfig() + if (config.menuHidden || config.layout === 'horizontal') return null + return ( +
+ + setConfig({ ...config, showSearchBar: !config.showSearchBar }) + } + /> + +
+ ); +} + +export default SearchBarToggle \ No newline at end of file diff --git a/components/partials/customizer/set-content-width.tsx b/components/partials/customizer/set-content-width.tsx new file mode 100644 index 0000000..df3ba6b --- /dev/null +++ b/components/partials/customizer/set-content-width.tsx @@ -0,0 +1,71 @@ +"use client"; +import React from "react"; +import { cn } from "@/lib/utils"; +import { Label } from "@/components/ui/label"; +import { useConfig } from "@/hooks/use-config"; +import { Icon } from "@/components/ui/icon"; +import { wideContentWidthSvg, boxedContentWidthSvg } from "./data"; +import { contentType } from "@/lib/type"; + +const allContent: { key: contentType; label: string }[] = [ + { key: "wide", label: "Wide" }, + { key: "boxed", label: "Boxed" }, +]; +const SetContentWidth = () => { + const [config, setConfig] = useConfig(); + + return ( +
+
Content Width
+
+ {allContent?.map(({ key, label }, index) => ( +
+ + + +
+ ))} +
+
+ ); +}; + +export default SetContentWidth; diff --git a/components/partials/customizer/set-layout.tsx b/components/partials/customizer/set-layout.tsx new file mode 100644 index 0000000..fd63e55 --- /dev/null +++ b/components/partials/customizer/set-layout.tsx @@ -0,0 +1,91 @@ +'use client' +import React from 'react' +import { cn } from "@/lib/utils" +import { Label } from "@/components/ui/label" +import { useConfig } from '@/hooks/use-config' +import { Icon } from "@/components/ui/icon" +import Image from 'next/image' +import { layoutType } from '@/lib/type' +import { + verticalLayoutSvg, + horizontalLayoutSvg, + semiBoxLayoutSvg, + compactLayoutSvg, +} from "./data"; + +const allLayouts: { key: layoutType, label: string, icon: string }[] = [ + { key: "vertical", label: "Vertical", icon: "heroicons:chart-bar", }, + { + key: "horizontal", + label: "Horizontal", + icon: "heroicons:chart-pie", + + }, + { key: "semi-box", label: "SemiBox", icon: "heroicons:chart-pie" }, + { key: "compact", label: "Compact", icon: "heroicons:chart-pie" }, +]; + + +const SetLayout = () => { + const [config, setConfig] = useConfig() + + + + + return ( +
+
Layout
+
+ {allLayouts?.map(({ key, label, icon }, index) => ( +
+ + + +
+ ))} +
+
+ ); +} + +export default SetLayout \ No newline at end of file diff --git a/components/partials/customizer/set-sidebar.tsx b/components/partials/customizer/set-sidebar.tsx new file mode 100644 index 0000000..88928d6 --- /dev/null +++ b/components/partials/customizer/set-sidebar.tsx @@ -0,0 +1,80 @@ +'use client' +import React from 'react' +import { cn } from "@/lib/utils" +import { Label } from "@/components/ui/label" +import { useConfig } from '@/hooks/use-config' +import { Icon } from "@/components/ui/icon" +import { sidebarType } from '@/lib/type' +import { + classicSidebarSvg, + draggableSidebarSvg, + twoColumnSidebarSvg, + compactSidebarSvg, +} from "./data"; + +const allSidebars: { key: sidebarType, label: string, icon: string }[] = [ + { key: "classic", label: "Classic", icon: "heroicons:chart-bar" }, + { key: "draggable", label: "Draggable", icon: "heroicons:chart-pie" }, + { key: "two-column", label: "Two Column", icon: "heroicons:chart-pie" }, + { key: "compact", label: "Compact", icon: "heroicons:chart-pie" }, +]; + + +const SetSidebar = () => { + const [config, setConfig] = useConfig() + + return ( +
+
Sidebar
+
+ {allSidebars?.map(({ key, label, icon }, index) => ( +
+ + + +
+ ))} +
+
+ ); +} + +export default SetSidebar \ No newline at end of file diff --git a/components/partials/customizer/set-skin.tsx b/components/partials/customizer/set-skin.tsx new file mode 100644 index 0000000..0b1cf33 --- /dev/null +++ b/components/partials/customizer/set-skin.tsx @@ -0,0 +1,71 @@ +'use client' +import React from 'react' +import { cn } from "@/lib/utils" +import { Label } from "@/components/ui/label" +import { useConfig } from '@/hooks/use-config' +import { Icon } from "@/components/ui/icon" +import { + borderedSkinSvg, + defaultSkinSvg, +} from "./data"; +import { skinType } from '@/lib/type' + +const allSkin: { key: skinType; label: string; }[] = [ + { key: "default", label: "Default" }, + { key: "bordered", label: "Bordered" }, +]; + +const SetSkin = () => { + const [config, setConfig] = useConfig() + + return ( +
+
Skins
+
+ {allSkin?.map(({ key, label }, index) => ( +
+ + + +
+ ))} +
+
+ ); +} + +export default SetSkin \ No newline at end of file diff --git a/components/partials/customizer/sidebar-bg.tsx b/components/partials/customizer/sidebar-bg.tsx new file mode 100644 index 0000000..c1dc6be --- /dev/null +++ b/components/partials/customizer/sidebar-bg.tsx @@ -0,0 +1,73 @@ +'use client' +import { useConfig } from '@/hooks/use-config'; +import React, { useState } from 'react' +import { Icon } from "@/components/ui/icon" +import { cn } from "@/lib/utils"; +const SidebarBg = () => { + const [config, setConfig] = useConfig(); + const { sidebarBgImage } = config; + const [selectedFiles, setSelectedFiles] = useState([ + "/images/all-img/img-2.jpeg", + "/images/all-img/img-1.jpeg", + ]); + const handleFileChange = (e: any) => { + const file = e.target.files[0]; + setSelectedFiles([...selectedFiles, URL.createObjectURL(file)]); + }; + + const handleClear = () => { + setConfig({ + ...config, + sidebarBgImage: undefined, + }) + }; + return ( +
+
+ Choose a Sidebar Image +
+
+ + {selectedFiles.map((file, index) => ( + + ))} + +
+
+ ); +} + +export default SidebarBg \ No newline at end of file diff --git a/components/partials/customizer/sidebar-color.tsx b/components/partials/customizer/sidebar-color.tsx new file mode 100644 index 0000000..9698e8f --- /dev/null +++ b/components/partials/customizer/sidebar-color.tsx @@ -0,0 +1,153 @@ +'use client' +import React from 'react' +import { cn } from "@/lib/utils" +import { Label } from "@/components/ui/label" +import { useConfig } from '@/hooks/use-config' +import { Icon } from "@/components/ui/icon" +import { Check } from 'lucide-react'; +import { + defaultSidebarColorSvg, + colorSidebarColorSvg, + gradientSidebarColorSvg, +} from "./data"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip" +const SidebarColor = () => { + const [config, setConfig] = useConfig() + const [show, setShow] = React.useState( + config.sidebarTheme !== 'light' + ) + + return ( +
+
+ Sidebar Color +
+
+
+ + +
+
+ + +
+
+ + {show && ( +
+ {[ + "dark", + "rose", + "steel-blue", + "purple", + "redwood", + "green", + "ocean-blue", + "gray", + + ].map((color) => ( +
+ + + + + + +

{color}

+
+
+
+ +
+ ))} +
+ )} +
+ ); +} + +export default SidebarColor \ No newline at end of file diff --git a/components/partials/customizer/team-switcher-toggle.tsx b/components/partials/customizer/team-switcher-toggle.tsx new file mode 100644 index 0000000..f94e606 --- /dev/null +++ b/components/partials/customizer/team-switcher-toggle.tsx @@ -0,0 +1,28 @@ +'use client' +import React from 'react' +import { Checkbox } from "@/components/ui/checkbox" +import { useConfig } from '@/hooks/use-config' + +const TeamSwitcherToggle = () => { + const [config, setConfig] = useConfig() + if (config.menuHidden || config.layout === 'horizontal') return null + return ( +
+ + setConfig({ ...config, showSwitcher: !config.showSwitcher }) + } + /> + +
+ ); +} + +export default TeamSwitcherToggle \ No newline at end of file diff --git a/components/partials/footer/footer-content.tsx b/components/partials/footer/footer-content.tsx new file mode 100644 index 0000000..6d0e771 --- /dev/null +++ b/components/partials/footer/footer-content.tsx @@ -0,0 +1,49 @@ +'use client' + +import React from 'react' +import { useConfig } from '@/hooks/use-config' +import { cn } from "@/lib/utils" +import { useMediaQuery } from '@/hooks/use-media-query' + + +const FooterContent = ({ children }: { children: React.ReactNode }) => { + const [config] = useConfig() + const isMobile = useMediaQuery("(min-width: 768px)") + if (!isMobile) { + return
+ {children} +
+ } + + if (config.sidebar === 'two-column') { + return ( +
{children}
+ ) + } + + return ( +
{children}
+ ) +} + +export default FooterContent \ No newline at end of file diff --git a/components/partials/footer/index.tsx b/components/partials/footer/index.tsx new file mode 100644 index 0000000..e709130 --- /dev/null +++ b/components/partials/footer/index.tsx @@ -0,0 +1,60 @@ +import React from "react"; +import FooterContent from "./footer-content"; +import { Link } from "@/components/navigation"; +import Image from "next/image"; +import { Icon } from "@/components/ui/icon"; + +const DashCodeFooter = () => { + return ( + +
+
+
+ Hak Cipta © {new Date().getFullYear()} Divisi Humas Polri Hub. All rights Reserved. +
+
+
+ +
+ + + + 10 + + + Messages +
+ + +
+ {"Image"} +
+ + +
+ + + + 2 + + + + Notifications + +
+ +
+
+ ); +}; + +export default DashCodeFooter; diff --git a/components/partials/header/cart.tsx b/components/partials/header/cart.tsx new file mode 100644 index 0000000..3ca8b94 --- /dev/null +++ b/components/partials/header/cart.tsx @@ -0,0 +1,126 @@ +import { Button } from "@/components/ui/button" +import { + Sheet, + SheetClose, + SheetContent, + SheetDescription, + SheetFooter, + SheetHeader, + SheetTitle, + SheetTrigger, +} from "@/components/ui/sheet" +import { Icon } from "@/components/ui/icon"; +import { Badge } from "@/components/ui/badge"; +import Image from "next/image"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import ProductCounterButton from "@/components/ecommarce/product-counter-button"; + +export function Cart() { + const cartProducts = [ + { + id: 1, + productImage: "/images/e-commerce/product-card/classical-black-tshirt.png" + }, + { + id: 2, + productImage: "/images/e-commerce/product-card/black-t-shirt.png" + }, + { + id: 3, + productImage: "/images/e-commerce/product-card/check-shirt.png" + }, + { + id: 4, + productImage: "/images/e-commerce/product-card/gray-jumper.png" + }, + { + id: 5, + productImage: "/images/e-commerce/product-card/gray-t-shirt.png" + }, + { + id: 6, + productImage: "/images/e-commerce/product-card/pink-blazer.png" + } + ] + return ( + + + + + + + Cart + + Total Price $0 + + + { + cartProducts.length > 0 ? ( + +
+ {cartProducts.map((product, i) => ( +
+
+
+ +
+
+
+
+ Classical Black T-Shirt Classical Black T-Shirt +
+
+ Price: $489 +
+
+ + +
+
+
+ ))} +
+
+ ) + : ( +
+
+ +
+
+ ) + } + + + +
+
Subtotal
+
$0
+
+
+ + +
+
+
+
+ ) +} diff --git a/components/partials/header/data.ts b/components/partials/header/data.ts new file mode 100644 index 0000000..9386184 --- /dev/null +++ b/components/partials/header/data.ts @@ -0,0 +1,180 @@ + + +export const notifications = [ + { + id: 1, + title: "Your order is placed", + role: "Frontend Developer", + desc: + "Amet minim mollit non deser unt ullamco est sit aliqua.", + avatar: 'ss.png', + status: "online", + unreadmessage: false, + date: "2 days ago", + }, + { + id: 2, + title: "Congratulations Darlene 🎉", + role: "UI/UX Designer", + desc: + "Won the monthly best seller badge", + avatar: 'ss.png', + status: "online", + unreadmessage: true, + date: "11 am", + }, + { + id: 3, + title: "Joaquina Weisenborn", + role: "Town planner", + desc: + "Soufflé soufflé caramels sweet roll. Jelly lollipop sesame snaps bear claw jelly beans sugar plum sugar plum.", + avatar: 'ss.png', + status: "busy", + unreadmessage: true, + date: "12 pm", + }, + { + id: 4, + title: "Brooklyn Simmons", + role: "Data scientist", + desc: + "Added you to Top Secret Project group...", + avatar: 'ss.png', + status: "online", + unreadmessage: true, + date: "1 pm", + }, + { + id: 5, + title: "Margot Henschke", + role: "Dietitian", + desc: + "Cake pie jelly jelly beans. Marzipan lemon drops halvah cake. Pudding cookie lemon drops icing", + avatar: 'ss.png', + status: "busy", + unreadmessage: false, + date: "3 pm", + }, + { + id: 6, + title: "Sal Piggee", + role: "Marketing executive", + desc: + "Toffee caramels jelly-o tart gummi bears cake I love ice cream lollipop. Sweet liquorice croissant candy danish dessert icing. Cake macaroon gingerbread toffee sweet.", + avatar: 'ss.png', + status: "online", + unreadmessage: false, + date: "4 pm", + }, + { + id: 7, + title: "Miguel Guelff", + role: "Special educational needs teacher", + desc: + "Biscuit powder oat cake donut brownie ice cream I love soufflé. I love tootsie roll I love powder tootsie roll.", + avatar: 'ss.png', + status: "online", + unreadmessage: true, + date: "7 pm", + }, + { + id: 8, + title: "Mauro Elenbaas", + role: "Advertising copywriter", + desc: + "Bear claw ice cream lollipop gingerbread carrot cake. Brownie gummi bears chocolate muffin croissant jelly I love marzipan wafer.", + avatar: 'ss.png', + status: "away", + unreadmessage: true, + date: "10 pm", + }, + { + id: 9, + title: "Bridgett Omohundro", + role: "Designer, television/film set", + desc: + "Gummies gummi bears I love candy icing apple pie I love marzipan bear claw. I love tart biscuit I love candy canes pudding chupa chups liquorice croissant.", + avatar: 'ss.png', + status: "offline", + unreadmessage: false, + date: "10 pm", + }, + { + id: 10, + title: "Zenia Jacobs", + role: "Building surveyor", + desc: + "Cake pie jelly jelly beans. Marzipan lemon drops halvah cake. Pudding cookie lemon drops icing", + avatar: 'ss.png', + status: "away", + unreadmessage: false, + date: "10 am", + }, +]; + +export const messages = [ + { + title: "Wade Warren", + desc: "Hi! How are you doing?.....", + active: true, + hasnotifaction: true, + notification_count: 1, + image: undefined, + link: "#", + }, + { + title: "Savannah Nguyen", + desc: "Hi! How are you doing?.....", + active: false, + hasnotifaction: false, + image: undefined, + link: "#", + }, + { + title: "Ralph Edwards", + desc: "Hi! How are you doing?.....", + active: false, + hasnotifaction: true, + notification_count: 8, + image: undefined, + link: "#", + }, + { + title: "Cody Fisher", + desc: "Hi! How are you doing?.....", + active: true, + hasnotifaction: false, + image: undefined, + link: "#", + }, + { + title: "Savannah Nguyen", + desc: "Hi! How are you doing?.....", + active: false, + hasnotifaction: false, + image: undefined, + link: "#", + }, + { + title: "Ralph Edwards", + desc: "Hi! How are you doing?.....", + active: false, + hasnotifaction: true, + notification_count: 8, + image: undefined, + link: "#", + }, + { + title: "Cody Fisher", + desc: "Hi! How are you doing?.....", + active: true, + hasnotifaction: false, + image: undefined, + link: "#", + }, +]; + +export type Message = ( typeof messages) [number] +export type Notification = (typeof notifications) [number] + diff --git a/components/partials/header/header-content.tsx b/components/partials/header/header-content.tsx new file mode 100644 index 0000000..42b8c66 --- /dev/null +++ b/components/partials/header/header-content.tsx @@ -0,0 +1,59 @@ +'use client'; +import React from 'react' +import { useConfig } from '@/hooks/use-config' +import { cn } from '@/lib/utils' + +const HeaderContent = ({ children }: { children: React.ReactNode }) => { + const [config] = useConfig() + const headerTheme = config.headerTheme !== 'light' && config.headerTheme !== 'transparent' ? ` dark theme-${config.headerTheme}` : `theme-${config.headerTheme}` + if (config.sidebar === 'two-column') { + return ( +
+
+ {children} +
+ +
+ ) + } + + return ( +
+
+ + + {children} +
+
+ ) +} + +export default HeaderContent \ No newline at end of file diff --git a/components/partials/header/header-logo.tsx b/components/partials/header/header-logo.tsx new file mode 100644 index 0000000..5cb63e2 --- /dev/null +++ b/components/partials/header/header-logo.tsx @@ -0,0 +1,36 @@ +"use client"; +import React from "react"; +import { Link } from "@/components/navigation"; +import DashCodeLogo from "@/components/dascode-logo"; +import { useConfig } from "@/hooks/use-config"; +import { useMediaQuery } from "@/hooks/use-media-query"; + +const HeaderLogo = () => { + const [config] = useConfig(); + + const isDesktop = useMediaQuery("(min-width: 1280px)"); + + return config.layout === "horizontal" ? ( + + logo + + ) : ( + !isDesktop && ( + + logo + + ) + ); +}; + +export default HeaderLogo; diff --git a/components/partials/header/header-search.tsx b/components/partials/header/header-search.tsx new file mode 100644 index 0000000..7b32dc3 --- /dev/null +++ b/components/partials/header/header-search.tsx @@ -0,0 +1,222 @@ +'use client' +import React from "react"; +import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog"; +import { X } from "lucide-react"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command"; +import { Button } from "@/components/ui/button"; +import { Link } from '@/i18n/routing'; +import { Icon } from "@/components/ui/icon"; +import { useConfig } from "@/hooks/use-config"; +const HeaderSearch = () => { + const [config] = useConfig(); + if (config.layout === 'horizontal') return null + return ( + + + + + + + +
+ +
+ + No results found. +
+ + + + + Calendar + + + + + + Analytics + + + + + + eCommerce + + + + + + Project Page + + + + + + + + Chat + + + + + + Email + + + + + + Sign In + + + + + + Appex Chart + + + + + + + + Accordion + + + + + + Checkboxes + + + + + + Alert + + + + + + Pagination + + + + + + + + Simple Table + + + + + + Tailwind Ui Table + + + + + + Tanstack Table + + + + + + Forms + + + +
+
+
+
+
+ ); +}; + +export default HeaderSearch; diff --git a/components/partials/header/horizontal-menu.tsx b/components/partials/header/horizontal-menu.tsx new file mode 100644 index 0000000..6c09709 --- /dev/null +++ b/components/partials/header/horizontal-menu.tsx @@ -0,0 +1,127 @@ +'use client' +import React from "react"; +import { cn } from "@/lib/utils"; +import { ChevronDown } from "lucide-react"; +import { Link, usePathname } from "@/components/navigation"; +import { useConfig } from '@/hooks/use-config' +import { useTranslations } from 'next-intl'; +import { getHorizontalMenuList } from "@/lib/menus"; +import { Icon } from "@/components/ui/icon"; +import { + Menubar, + MenubarCheckboxItem, + MenubarContent, + MenubarItem, + MenubarMenu, + MenubarRadioGroup, + MenubarRadioItem, + MenubarSeparator, + MenubarShortcut, + MenubarSub, + MenubarSubContent, + MenubarSubTrigger, + MenubarTrigger, +} from "@/components/ui/menubar" +import { useMediaQuery } from "@/hooks/use-media-query"; + +export default function HorizontalMenu() { + + const [config] = useConfig() + + const t = useTranslations("Menu"); + const pathname = usePathname(); + + const menuList = getHorizontalMenuList(pathname, t) + + const [openDropdown, setOpenDropdown] = React.useState(false); + + const isDesktop = useMediaQuery('(min-width: 1280px)') + + if (config.layout !== 'horizontal' || !isDesktop) return null + return ( +
+ + {menuList?.map(({ groupLabel, menus }, index) => ( + + {menus.map(({ href, label, icon, active, id, submenus }, index) => + submenus.length === 0 ? ( + + + + + {label} + + + + ) : ( + + + + {label} + + + + + + {submenus.map( + ({ href, label, icon, children: subChildren }, index) => + subChildren?.length === 0 ? ( + + + + {label} + + + ) : ( + + + + + + {icon && ( + + )} + {label} + + + + {subChildren?.map( + ({ href, label }, index) => ( + + + + + {label} + + + ) + )} + + + + ) + )} + + + + + ) + )} + + ))} + +
+ ); +} + + diff --git a/components/partials/header/index.tsx b/components/partials/header/index.tsx new file mode 100644 index 0000000..f63ec32 --- /dev/null +++ b/components/partials/header/index.tsx @@ -0,0 +1,41 @@ +"use client"; + +import React from "react"; +import HeaderContent from "./header-content"; +import HeaderSearch from "./header-search"; +import ProfileInfo from "./profile-info"; +import Notifications from "./notifications"; +import Messages from "./messages"; +import { Cart } from "./cart"; +import ThemeSwitcher from "./theme-switcher"; +import { SidebarToggle } from "@/components/partials/sidebar/sidebar-toggle"; +import { SheetMenu } from "@/components/partials/sidebar/menu/sheet-menu"; +import HorizontalMenu from "./horizontal-menu"; +import LocalSwitcher from "./locale-switcher"; +import HeaderLogo from "./header-logo"; + +const DashCodeHeader = () => { + return ( + <> + +
+ + + +
+
+ + + {/* + */} + + + +
+
+ + + ); +}; + +export default DashCodeHeader; diff --git a/components/partials/header/locale-switcher.tsx b/components/partials/header/locale-switcher.tsx new file mode 100644 index 0000000..810be89 --- /dev/null +++ b/components/partials/header/locale-switcher.tsx @@ -0,0 +1,129 @@ +"use client"; + +import { useLocale } from "next-intl"; +import { useParams, useSearchParams } from "next/navigation"; +import { locales } from "@/config"; +import { usePathname, useRouter } from "@/i18n/routing"; + +import { useEffect, useState, useTransition } from "react"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import Image from "next/image"; +import Cookies from "js-cookie"; + +export const getLanguage = (): string | null => { + if (typeof window === "undefined") return null; + return localStorage.getItem("language"); +}; + +export const setLanguage = (value: string) => { + if (typeof window === "undefined") return; + localStorage.setItem("language", value); +}; + +export default function LocalSwitcher() { + const [isPending, startTransition] = useTransition(); + const router = useRouter(); + const pathname = usePathname(); + const params = useParams(); + const locale = useLocale() || "in"; + const [selectedLang, setSelectedLang] = useState("in"); + const searchParams = useSearchParams(); + const [hasInitialized, setHasInitialized] = useState(false); + + useEffect(() => { + const storedLang = getLanguage(); + let joinParam = ""; + + if (searchParams) { + joinParam = Array.from(searchParams.entries()) + .map(([key, value]) => `${key}=${value}`) + .join("&"); + } + + const redirectTo = (lang: string) => { + startTransition(() => { + router.replace(pathname + (joinParam ? `?${joinParam}` : ""), { + locale: lang, + }); + }); + }; + + if (!hasInitialized) { + if (!storedLang) { + setLanguage("in"); + setSelectedLang("in"); + redirectTo("in"); + } else { + setSelectedLang(storedLang); + if (locale !== storedLang) { + redirectTo(storedLang); + } + } + setHasInitialized(true); + } + }, []); + + const onSelectChange = (nextLocale: string) => { + setLanguage(nextLocale); + setSelectedLang(nextLocale); + startTransition(() => { + router.replace(pathname, { locale: nextLocale }); + }); + }; + return ( + + ); +} diff --git a/components/partials/header/messages.tsx b/components/partials/header/messages.tsx new file mode 100644 index 0000000..1f037e5 --- /dev/null +++ b/components/partials/header/messages.tsx @@ -0,0 +1,93 @@ + +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Link } from '@/i18n/routing'; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { cn } from "@/lib/utils"; +import { messages, type Message } from "./data"; +import shortImage from "@/public/images/all-img/short-image-2.png"; +import { Icon } from "@/components/ui/icon"; + +const Messages = () => { + return ( + + + + + + +
+
+ Messages +
+
+ + View all + +
+
+
+
+ + {messages?.map((item: Message, index: number) => ( + + +
+ + + {item.title.charAt(0)} + +
+
+
+ {item.title} +
+
+ {item.desc} +
+
+ 3 min ago +
+
+ {item.hasnotifaction && ( + +
+ +
+ )} + + +
+ ))} +
+
+ +
+
+ ); +}; + +export default Messages; diff --git a/components/partials/header/notifications.tsx b/components/partials/header/notifications.tsx new file mode 100644 index 0000000..6568f45 --- /dev/null +++ b/components/partials/header/notifications.tsx @@ -0,0 +1,167 @@ +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Link } from "@/i18n/routing"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { cn } from "@/lib/utils"; +import shortImage from "@/public/images/all-img/short-image-2.png"; +import { Icon } from "@/components/ui/icon"; +import { useEffect, useState } from "react"; +// import { getNotifications } from "@/service/notifications/notifications"; +import { format, formatDate } from "date-fns"; +import { + CalendarCheck, + CheckCheck, + CircleAlert, + Clock7, + MessageCircle, + SquareCheck, + UploadIcon, +} from "lucide-react"; +import { useRouter } from "next/navigation"; + +export type Notification = { + id: number; + notificationTypeId: number; + message: string; + createdAt: string; + isActive: boolean; + isPublic: boolean; + isRead: boolean; + redirectUrl: string; + userGroupIdDst: string | null; + userIdDst: string | null; + userLevelIdDst: string; + userLevelNumberDst: string | null; + userRoleIdDst: string; +}; + +const getNotificationIcon = (notificationTypeId: number) => { + switch (notificationTypeId) { + case 2: + return ; + case 3: + return ; + case 4: + return ; + case 5: + return ; + case 6: + return ; + case 7: + return ; + case 8: + return ; + default: + return ; + } +}; + +const Notifications = () => { + const router = useRouter(); + const [notifications, setNotifications] = useState([]); + const [notificationTotal, setNotificationTotal] = useState(0); + + useEffect(() => { + async function initState() { + // const response = await getNotifications(); + // setNotifications(response?.data?.data?.content); + // setNotificationTotal(response?.data?.data?.totalElements); + // console.log("notif", response?.data?.data?.content); + } + initState(); + }, []); + + const formatDate = (dateString: string) => { + const date = new Date(dateString); + return format(date, "dd/MM/yyyy HH:mm"); + }; + + const handleNotificationClick = (redirectUrl: string) => { + router.push(redirectUrl); + }; + + return ( + + + + + + +
+
+ you have {notificationTotal > 99 ? "99+" : notificationTotal}{" "} + notifications +
+
+ + View all + +
+
+
+
+ + {notifications?.map((item: Notification, index: number) => ( + handleNotificationClick(item?.redirectUrl)} + > +
+
+ {getNotificationIcon(item.notificationTypeId)} +
+
+
+ {item?.message} +
+ {/*
+ {item?.desc} +
*/} +
+ {" "} + {formatDate(item?.createdAt)} +
+
+
+ {/* {item?.unreadmessage && ( +
+ +
+ )} */} +
+ ))} +
+
+
+
+ ); +}; + +export default Notifications; diff --git a/components/partials/header/profile-info.tsx b/components/partials/header/profile-info.tsx new file mode 100644 index 0000000..2f460d3 --- /dev/null +++ b/components/partials/header/profile-info.tsx @@ -0,0 +1,159 @@ +"use client"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuPortal, + DropdownMenuSeparator, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Icon } from "@/components/ui/icon"; +import Image from "next/image"; +import { Link } from "@/i18n/routing"; +import { Button } from "@/components/ui/button"; +import Cookies from "js-cookie"; +import { useEffect, useState } from "react"; +import { useRouter } from "@/components/navigation"; +import { getInfoProfile } from "@/service/auth"; + +type Detail = { + id: number; + userId: any; + firstName: string; + username: string; + fullname: string; + memberIdentity: any; + email: string; + address: string; + phoneNumber: any; + message: string; +}; + +const ProfileInfo = () => { + const username = Cookies.get("access_token"); + const picture = Cookies.get("profile_picture"); + const router = useRouter(); + const [detail, setDetail] = useState(); + + const onLogout = () => { + Object.keys(Cookies.get()).forEach((cookieName) => { + Cookies.remove(cookieName); + }); + + router.push("/"); + }; + + useEffect(() => { + if (!username) { + router.push("/auth"); + } + }, [username]); + + useEffect(() => { + async function initState() { + const response = await getInfoProfile(); + const details = response?.data?.data; + + setDetail(details); + console.log("data", details); + } + + initState(); + }, []); + + return ( +
+ + + {detail !== undefined ? ( +
+ {"Image"} +
+
+ {detail?.fullname} +
+

({detail?.username})

+
+ + + +
+ ) : ( + "" + )} +
+ + {/* + {username + +
+
+ {username} +
+ + {username} + +
+
*/} + + {[ + { + name: "profile & Settings", + icon: "heroicons:user", + href: "/profile", + }, + ].map((item, index) => ( + + + + {item.name} + + + ))} + + + + +
+ + + +
+
+
+
+
+ ); +}; +export default ProfileInfo; diff --git a/components/partials/header/theme-switcher.tsx b/components/partials/header/theme-switcher.tsx new file mode 100644 index 0000000..3168ea5 --- /dev/null +++ b/components/partials/header/theme-switcher.tsx @@ -0,0 +1,72 @@ +"use client"; + +import * as React from "react"; +import { useTheme } from "next-themes"; +import { hexToRGB } from "@/lib/utils"; +import { Button } from "@/components/ui/button"; +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; +import { Moon, Sun } from "lucide-react"; +import { Check } from "lucide-react"; +import { cn } from "@/lib/utils"; +import { Icon } from "@/components/ui/icon"; + +const ThemeButton = () => { + const { theme, setTheme } = useTheme(); + return ( + + + + + + setTheme("light")} + className={cn("p-2 font-medium text-sm text-default-600 cursor-pointer mb-[2px] ", { + "bg-default text-default-foreground": theme === "light", + })} + > + + Light + + + setTheme("dark")} + className={cn("p-2 font-medium text-sm text-default-600 hover:bg-default hover:text-default-foreground dark:hover:bg-background cursor-pointer mb-[2px]", { + "bg-default text-default-foreground": theme === "dark", + })} + > + + Dark + + + setTheme("system")} + className={cn("p-2 font-medium text-sm text-default-600 hover:bg-default hover:text-default-foreground dark:hover:bg-background cursor-pointer mb-[2px]", { + "bg-default text-default-foreground": theme === "system", + })} + > + + system + + + + + ); +}; + +export default ThemeButton; diff --git a/components/partials/sidebar/common/classic-multi-collapse-button.tsx b/components/partials/sidebar/common/classic-multi-collapse-button.tsx new file mode 100644 index 0000000..2852dbf --- /dev/null +++ b/components/partials/sidebar/common/classic-multi-collapse-button.tsx @@ -0,0 +1,136 @@ +"use client"; +import React, { CSSProperties } from 'react' +import { Link, usePathname } from "@/components/navigation"; +import { useState } from "react"; +import { ChevronDown, Dot, LucideIcon } from "lucide-react"; + +import { cn } from "@/lib/utils"; +import { Button } from "@/components/ui/button"; +import { DropdownMenuArrow } from "@radix-ui/react-dropdown-menu"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger +} from "@/components/ui/collapsible"; + +import { Icon } from "@/components/ui/icon"; +import { SubChildren } from '@/lib/menus'; +import { useMobileMenuConfig } from '@/hooks/use-mobile-menu'; + + + + +interface CollapseMenuButtonProps { + icon?: string; + label: string; + active: boolean; + submenus: SubChildren[] + + +} + +export function MultiCollapseMenuButton({ + icon, + label, + active, + submenus, + + + +}: CollapseMenuButtonProps) { + const pathname = usePathname(); + const isSubmenuActive = submenus.some((submenu) => submenu.active || pathname.startsWith(submenu.href)); + const [isCollapsed, setIsCollapsed] = useState(isSubmenuActive); + const [mobileMenuConfig, setMobileMenuConfig] = useMobileMenuConfig(); + return ( + + + +
+ + +
+
+ + {submenus.map(({ href, label, active }, index) => ( + + + + + ))} + +
+ + ) +} diff --git a/components/partials/sidebar/common/collapse-menu-button.tsx b/components/partials/sidebar/common/collapse-menu-button.tsx new file mode 100644 index 0000000..6f7db4f --- /dev/null +++ b/components/partials/sidebar/common/collapse-menu-button.tsx @@ -0,0 +1,357 @@ +"use client"; +import React, { CSSProperties } from 'react' +import { Link, usePathname } from "@/components/navigation"; +import { useState } from "react"; +import { ChevronDown, Dot, LucideIcon } from "lucide-react"; +import { GripVertical } from 'lucide-react'; +import { cn } from "@/lib/utils"; +import { Button } from "@/components/ui/button"; +import { DropdownMenuArrow } from "@radix-ui/react-dropdown-menu"; +import { ScrollArea } from "@/components/ui/scroll-area" + +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger +} from "@/components/ui/collapsible"; +import { + Tooltip, + TooltipTrigger, + TooltipContent, + TooltipProvider +} from "@/components/ui/tooltip"; +import { + DropdownMenu, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuSeparator, + DropdownMenuGroup, + DropdownMenuSub, + DropdownMenuSubTrigger, + DropdownMenuPortal, + DropdownMenuSubContent +} from "@/components/ui/dropdown-menu"; +import { Icon } from "@/components/ui/icon"; +import { Submenu } from "@/lib/menus" + +// for dnd + +import { + useSortable, + arrayMove, + SortableContext, + verticalListSortingStrategy, + horizontalListSortingStrategy, +} from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; +import { useConfig } from '@/hooks/use-config'; +import { MultiCollapseMenuButton } from './classic-multi-collapse-button'; +import { useMediaQuery } from '@/hooks/use-media-query'; +import { useMobileMenuConfig } from '@/hooks/use-mobile-menu'; +import { useMenuHoverConfig } from '@/hooks/use-menu-hover'; + + +interface CollapseMenuButtonProps { + icon: string; + label: string; + active: boolean; + submenus: Submenu[]; + collapsed: boolean | undefined; + id: string + +} + +export function CollapseMenuButton({ + icon, + label, + active, + submenus, + collapsed, + id, + +}: CollapseMenuButtonProps) { + const pathname = usePathname(); + const isSubmenuActive = submenus.some((submenu) => submenu.active || pathname.startsWith(submenu.href)); + const [isCollapsed, setIsCollapsed] = useState(isSubmenuActive); + const [mobileMenuConfig, setMobileMenuConfig] = useMobileMenuConfig() + const [config] = useConfig(); + const [hoverConfig] = useMenuHoverConfig(); + const { hovered } = hoverConfig; + const sidebarTheme = config.sidebarTheme !== 'light' ? `dark theme-${config.sidebarTheme}` : `theme-${config.sidebarTheme}` + const isDesktop = useMediaQuery("(min-width: 1280px)"); + const { transform, transition, setNodeRef, isDragging, attributes, listeners } = useSortable({ + id: id, + + }) + + React.useEffect(() => { + setIsCollapsed(isSubmenuActive); + }, [isSubmenuActive, pathname]); + + + const style: CSSProperties = { + transform: CSS.Transform.toString(transform), + transition: transition, + opacity: isDragging ? 0.8 : 1, + zIndex: isDragging ? 1 : 0, + position: "relative", + }; + + if (config.sidebar === 'compact' && isDesktop) { + return ( + + + + + + {submenus.map(({ href, label, active }, index) => ( + + + + + ))} + + + ) + } + return !collapsed || hovered ? ( + + +
+ + +
+
+ + {submenus.map(({ href, label, active, children: subChildren }, index) => ( + + subChildren?.length === 0 ? ( + + ) : ( + + + + ) + + + ))} + +
+ ) : ( + + + + + + + + + + {label} + + + + + + {label} + + + + + + {submenus.map(({ href, label, icon, active, children }, index) => ( + children?.length === 0 ? ( + + + {icon && ( + + )} +

{label}

+ +
+ ) : ( + + + {label} + + + + + + + {children?.map(({ href, label, active }, index) => ( + + + + {label} + + ))} + + + + + ) + + + ))} +
+
+
+ ); +} diff --git a/components/partials/sidebar/common/collapse-menu-button2.tsx b/components/partials/sidebar/common/collapse-menu-button2.tsx new file mode 100644 index 0000000..cd00121 --- /dev/null +++ b/components/partials/sidebar/common/collapse-menu-button2.tsx @@ -0,0 +1,141 @@ +"use client"; +import React, { CSSProperties } from 'react' +import { Link, usePathname } from "@/components/navigation"; +import { useState } from "react"; +import { ChevronDown, Dot, LucideIcon } from "lucide-react"; + +import { cn } from "@/lib/utils"; +import { Button } from "@/components/ui/button"; +import { DropdownMenuArrow } from "@radix-ui/react-dropdown-menu"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger +} from "@/components/ui/collapsible"; + +import { Icon } from "@/components/ui/icon"; +import { SubChildren } from '@/lib/menus'; + + + + +interface CollapseMenuButtonProps { + icon: string; + label: string; + active: boolean; + submenus: SubChildren[] + + +} + +export function CollapseMenuButton2({ + icon, + label, + active, + submenus, + + + +}: CollapseMenuButtonProps) { + const pathname = usePathname(); + const isSubmenuActive = submenus.some((submenu) => submenu.active || pathname.startsWith(submenu.href)); + const [isCollapsed, setIsCollapsed] = useState(isSubmenuActive); + + + + + + + + return ( + + + +
+ + +
+
+ + {submenus.map(({ href, label, active }, index) => ( + + + + + ))} + +
+ + ) +} diff --git a/components/partials/sidebar/common/menu-icon.tsx b/components/partials/sidebar/common/menu-icon.tsx new file mode 100644 index 0000000..1c774ca --- /dev/null +++ b/components/partials/sidebar/common/menu-icon.tsx @@ -0,0 +1,9 @@ +import React from 'react' + +const MenuIcon = () => { + return ( +
MenuIcon
+ ) +} + +export default MenuIcon \ No newline at end of file diff --git a/components/partials/sidebar/common/menu-item.tsx b/components/partials/sidebar/common/menu-item.tsx new file mode 100644 index 0000000..303193c --- /dev/null +++ b/components/partials/sidebar/common/menu-item.tsx @@ -0,0 +1,159 @@ + +'use client' +import React, { CSSProperties } from 'react' +import { Button } from "@/components/ui/button"; +import { Icon } from '@/components/ui/icon' +import { type Menu } from '@/lib/menus'; +import { Link } from '@/components/navigation'; +import { cn } from "@/lib/utils"; +import { GripVertical } from 'lucide-react'; + +// for dnd + +import { + useSortable, + arrayMove, + SortableContext, + verticalListSortingStrategy, + horizontalListSortingStrategy, +} from "@dnd-kit/sortable"; +interface MenuItemProps { + id: string + href: string + label: string + icon: string + active: boolean + collapsed: boolean + +} +import { CSS } from "@dnd-kit/utilities"; +import { useConfig } from '@/hooks/use-config'; +import { useMediaQuery } from '@/hooks/use-media-query'; +import { useMobileMenuConfig } from '@/hooks/use-mobile-menu'; +import { useMenuHoverConfig } from '@/hooks/use-menu-hover'; +const MenuItem = ({ href, label, icon, active, id, collapsed }: MenuItemProps) => { + const [config] = useConfig(); + const [hoverConfig] = useMenuHoverConfig(); + const { hovered } = hoverConfig; + const isDesktop = useMediaQuery("(min-width: 1280px)"); + const [mobileMenuConfig, setMobileMenuConfig] = useMobileMenuConfig(); + + const { transform, transition, setNodeRef, isDragging, attributes, listeners } = useSortable({ + id: id, + + }) + + const style: CSSProperties = { + transform: CSS.Transform.toString(transform), + transition: transition, + opacity: isDragging ? 0.8 : 1, + zIndex: isDragging ? 1 : 0, + position: "relative", + }; + if (config.sidebar === 'draggable' && isDesktop) { + return ( + + ) + } + + if (config.sidebar === 'compact' && isDesktop) { + return ( + + ) + } + return ( + + ) +} + +export default MenuItem \ No newline at end of file diff --git a/components/partials/sidebar/common/menu-label.tsx b/components/partials/sidebar/common/menu-label.tsx new file mode 100644 index 0000000..f128766 --- /dev/null +++ b/components/partials/sidebar/common/menu-label.tsx @@ -0,0 +1,16 @@ +'use client' +import { cn } from "@/lib/utils" +import { useConfig } from '@/hooks/use-config' +import React from 'react' + +const MenuLabel = ({ label, className }: { label: string, className?: string }) => { + const [config] = useConfig() + if (config.sidebar === 'compact') return null + return ( +

+ {label} +

+ ) +} + +export default MenuLabel \ No newline at end of file diff --git a/components/partials/sidebar/common/menu-widget.tsx b/components/partials/sidebar/common/menu-widget.tsx new file mode 100644 index 0000000..216ee16 --- /dev/null +++ b/components/partials/sidebar/common/menu-widget.tsx @@ -0,0 +1,31 @@ +'use client' +import { Button } from '@/components/ui/button' +import { useConfig } from '@/hooks/use-config' +import Image from 'next/image' +import React from 'react' + +const MenuWidget = () => { + const [config] = useConfig(); + if (config.sidebar === 'compact') return null + return ( +
+
+ + {/* +
+
Unlimited Access
+
+ Upgrade your system to business plan +
+
+
+ +
*/} +
+
+ ) +} + +export default MenuWidget \ No newline at end of file diff --git a/components/partials/sidebar/common/search-bar.tsx b/components/partials/sidebar/common/search-bar.tsx new file mode 100644 index 0000000..5af5224 --- /dev/null +++ b/components/partials/sidebar/common/search-bar.tsx @@ -0,0 +1,78 @@ +'use client' +import { Input } from '@/components/ui/input' +import { useConfig } from '@/hooks/use-config' +import React from 'react' +import { + InputGroup, + InputGroupButton, + InputGroupText, +} from "@/components/ui/input-group"; +import { Search } from 'lucide-react'; +import { Button } from "@/components/ui/button"; +import { motion, AnimatePresence } from "framer-motion"; +import { + HoverCard, + HoverCardContent, + HoverCardTrigger, +} from "@/components/ui/hover-card" +import { useMenuHoverConfig } from '@/hooks/use-menu-hover'; +const SearchBar = () => { + const [config] = useConfig() + const [hoverConfig] = useMenuHoverConfig(); + const { hovered } = hoverConfig; + + if (config.showSearchBar === false || config.sidebar === 'compact') return null + + + return ( + + + {(config.collapsed && !hovered) ? : + + + + + + + } + + + + ) +} + +export default SearchBar + + +const CollapsedSearchBar = () => { + return ( + + + + + + + + + + + + + + ) +} diff --git a/components/partials/sidebar/common/team-switcher.tsx b/components/partials/sidebar/common/team-switcher.tsx new file mode 100644 index 0000000..52d1161 --- /dev/null +++ b/components/partials/sidebar/common/team-switcher.tsx @@ -0,0 +1,268 @@ +"use client" + +import * as React from "react" +import { ChevronsUpDown, Check, CirclePlus } from 'lucide-react'; + +import { cn } from "@/lib/utils" +import { + Avatar, + AvatarFallback, + AvatarImage, +} from "@/components/ui/avatar" +import { Button } from "@/components/ui/button" +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, + CommandSeparator, +} from "@/components/ui/command" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { useConfig } from "@/hooks/use-config"; +import { useMediaQuery } from "@/hooks/use-media-query"; +import { motion, AnimatePresence } from "framer-motion"; +import { useMenuHoverConfig } from "@/hooks/use-menu-hover"; + +const groups = [ + { + label: "Personal Account", + teams: [ + { + label: "Designing Workspace", + value: "personal", + }, + ], + }, + { + label: "Teams", + teams: [ + { + label: "Core Workspace", + value: "acme-inc", + }, + { + label: "Dev.Workspace", + value: "monsters", + }, + ], + }, +] + +type Team = (typeof groups)[number]["teams"][number] + +type PopoverTriggerProps = React.ComponentPropsWithoutRef + +interface TeamSwitcherProps extends PopoverTriggerProps { } + +const scaleVariants = { + collapsed: { scale: 0.8 }, + expanded: { scale: 1 } +}; + +export default function TeamSwitcher({ className }: TeamSwitcherProps) { + const [config] = useConfig(); + const [hoverConfig] = useMenuHoverConfig(); + const { hovered } = hoverConfig; + const [data, setData] = React.useState(); + const [open, setOpen] = React.useState(false) + const [showNewTeamDialog, setShowNewTeamDialog] = React.useState(false) + const [selectedTeam, setSelectedTeam] = React.useState( + groups[0].teams[0] + ) + const isDesktop = useMediaQuery("(min-width: 1280px)") + if (config.showSwitcher === false || config.sidebar === 'compact') return null + + + return ( + + + + + + + + + + {(config.collapsed && !hovered) ? : } + + + + + + + + No team found. + {groups.map((group) => ( + + {group.teams.map((team) => ( + { + setSelectedTeam(team) + setOpen(false) + }} + className="text-sm font-normal" + > + + {team.label} + + + ))} + + ))} + + + + + + { + setOpen(false) + setShowNewTeamDialog(true) + }} + > + + Create Team + + + + + + + + + + Create team + + Add a new team to manage products and customers. + + +
+
+
+ + +
+
+ + +
+
+
+ + + + +
+
+ ) +} diff --git a/components/partials/sidebar/common/team-workspace-switcher.tsx b/components/partials/sidebar/common/team-workspace-switcher.tsx new file mode 100644 index 0000000..c1dff0f --- /dev/null +++ b/components/partials/sidebar/common/team-workspace-switcher.tsx @@ -0,0 +1,285 @@ +"use client" + +import * as React from "react" +import { ChevronsUpDown, Check, CirclePlus } from 'lucide-react'; + +import { cn, getCookiesDecrypt } from "@/lib/utils" +import { + Avatar, + AvatarFallback, + AvatarImage, +} from "@/components/ui/avatar" +import { Button } from "@/components/ui/button" +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, + CommandSeparator, +} from "@/components/ui/command" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { useConfig } from "@/hooks/use-config"; +import { useMediaQuery } from "@/hooks/use-media-query"; +import { motion, AnimatePresence } from "framer-motion"; +import { useMenuHoverConfig } from "@/hooks/use-menu-hover"; +import { getInfoProfile } from "@/service/auth"; +// import { getUserRolePlacements, saveUserRolePlacements } from "@/service/management-user/management-user"; + +// var groups = [ +// { +// label: "Wilayah Tugas", +// teams: [], +// }, +// ] + +// type Team = (typeof groups)[number]["teams"][number] + +type PopoverTriggerProps = React.ComponentPropsWithoutRef + +interface TeamSwitcherProps extends PopoverTriggerProps { } + +const scaleVariants = { + collapsed: { scale: 0.8 }, + expanded: { scale: 1 } +}; + +export default function TeamWorkspaceSwitcher({ className }: TeamSwitcherProps) { + const [config] = useConfig(); + const [hoverConfig] = useMenuHoverConfig(); + const { hovered } = hoverConfig; + const [detail, setDetail] = React.useState(); + const userId = getCookiesDecrypt("uie"); + const [open, setOpen] = React.useState(false); + const [showNewTeamDialog, setShowNewTeamDialog] = React.useState(false); + const [groups, setGroups] = React.useState() + const [selectedTeam, setSelectedTeam] = React.useState({ label: "", value: "" }); + const isDesktop = useMediaQuery("(min-width: 1280px)") + if (config.showSwitcher === false || config.sidebar === 'compact') return null + + React.useEffect(() => { + async function initState() { + const response = await getInfoProfile(); + const details = response?.data?.data; + + setDetail(details); + console.log("data", details); + } + + async function getPlacement() { + // const response = await getUserRolePlacements(Number(userId)); + // const data = response?.data?.data; + + // var placementArr: any[] = []; + // data?.forEach((row: any) => { + // placementArr.push({ + // label: row.roleName + " | " + row.userLevelName, + // value: Number(row.id), + // }); + // }); + // const groupsTemp = [ + // { + // label: "Wilayah Tugas", + // teams: placementArr, + // } + // ]; + // setGroups(groupsTemp); + } + + initState(); + getPlacement(); + }, []); + + + return ( + + + + + + + + + + {(config.collapsed && !hovered) ? : } + + + + + + + + No team found. + {groups?.map((group: any) => ( + + {group.teams.map((team: any) => ( + { + setSelectedTeam(team) + setOpen(false) + }} + className="text-sm font-normal" + > + + {team.label} + + + ))} + + ))} + + {/* */} + {/* + + + { + setOpen(false) + setShowNewTeamDialog(true) + }} + > + + Create Team + + + + */} + + + + + + Create team + + Add a new team to manage products and customers. + + +
+
+
+ + +
+
+ + +
+
+
+ + + + +
+
+ ) +} diff --git a/components/partials/sidebar/index.tsx b/components/partials/sidebar/index.tsx new file mode 100644 index 0000000..888772a --- /dev/null +++ b/components/partials/sidebar/index.tsx @@ -0,0 +1,21 @@ + +import React from 'react' +import SidebarContent from './sidebar-content' +import Logo from '@/components/logo' +import { Menu } from './menu' + + +const DashCodeSidebar = () => { + return ( + + + + + + + + + ) +} + +export default DashCodeSidebar \ No newline at end of file diff --git a/components/partials/sidebar/menu/icon-nav.tsx b/components/partials/sidebar/menu/icon-nav.tsx new file mode 100644 index 0000000..1644dd5 --- /dev/null +++ b/components/partials/sidebar/menu/icon-nav.tsx @@ -0,0 +1,112 @@ +"use client"; +import React from "react"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import DashCodeLogo from "@/components/dascode-logo"; +import { Group, Submenu } from "@/lib/menus"; +import { Button } from "@/components/ui/button"; +import { Icon } from "@/components/ui/icon"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { Link } from "@/i18n/routing"; +import { cn } from "@/lib/utils"; +import { useConfig } from "@/hooks/use-config"; + +interface IconNavProps { + menuList: Group[]; +} +const IconNav = ({ menuList }: IconNavProps) => { + const [config, setConfig] = useConfig(); + + return ( +
+
+ +
+ + + +
+ ); +}; + +export default IconNav; diff --git a/components/partials/sidebar/menu/index.tsx b/components/partials/sidebar/menu/index.tsx new file mode 100644 index 0000000..c55a90e --- /dev/null +++ b/components/partials/sidebar/menu/index.tsx @@ -0,0 +1,30 @@ +"use client"; + +import React from 'react' + + +import { useConfig } from "@/hooks/use-config"; +import { MenuClassic } from './menu-classic'; +import { MenuTwoColumn } from './menu-two-column'; +import { MenuDragAble } from './menu-dragable'; +import { useMediaQuery } from '@/hooks/use-media-query'; + +export function Menu() { + + const [config, setConfig] = useConfig() + const isDesktop = useMediaQuery('(min-width: 1280px)') + + if (config.sidebar === 'draggable') { + return + } + + if (config.sidebar === 'two-column') { + return + } + + + + return ( + + ); +} diff --git a/components/partials/sidebar/menu/menu-classic.tsx b/components/partials/sidebar/menu/menu-classic.tsx new file mode 100644 index 0000000..12df7bc --- /dev/null +++ b/components/partials/sidebar/menu/menu-classic.tsx @@ -0,0 +1,158 @@ +"use client"; + +import React from 'react' +import { Ellipsis, LogOut } from "lucide-react"; +import { usePathname } from "@/components/navigation"; +import { cn, getCookiesDecrypt } from "@/lib/utils"; +import { getMenuList } from "@/lib/menus"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { + Tooltip, + TooltipTrigger, + TooltipContent, + TooltipProvider +} from "@/components/ui/tooltip"; +import { useConfig } from "@/hooks/use-config"; +import MenuLabel from "../common/menu-label"; +import MenuItem from "../common/menu-item"; +import { CollapseMenuButton } from "../common/collapse-menu-button"; +import MenuWidget from "../common/menu-widget"; +import SearchBar from '@/components/partials/sidebar/common/search-bar' +import TeamSwitcher from '../common/team-switcher' +import { useTranslations } from 'next-intl'; +import { useParams } from 'next/navigation' +import { getLangDir } from 'rtl-detect'; +import Logo from '@/components/logo'; +import SidebarHoverToggle from '@/components/partials/sidebar/sidebar-hover-toggle'; +import { useMenuHoverConfig } from '@/hooks/use-menu-hover'; +import { useMediaQuery } from '@/hooks/use-media-query'; +import TeamWorkspaceSwitcher from '../common/team-workspace-switcher'; + + +export function MenuClassic({ }) { + // translate + const t = useTranslations("Menu") + const pathname = usePathname(); + const params = useParams<{ locale: string; }>(); + const direction = getLangDir(params?.locale ?? ''); + + const isDesktop = useMediaQuery('(min-width: 1280px)') + const userRoleId = getCookiesDecrypt("urie"); + + const menuList = getMenuList(pathname, t); + const [config, setConfig] = useConfig() + const collapsed = config.collapsed + const [hoverConfig] = useMenuHoverConfig(); + const { hovered } = hoverConfig; + + const scrollableNodeRef = React.useRef(null); + const [scroll, setScroll] = React.useState(false); + + React.useEffect(() => { + const handleScroll = () => { + if (scrollableNodeRef.current && scrollableNodeRef.current.scrollTop > 0) { + setScroll(true); + } else { + setScroll(false); + } + }; + scrollableNodeRef.current?.addEventListener("scroll", handleScroll); + }, [scrollableNodeRef]); + + return ( + <> + {isDesktop && ( +
+ + +
+ )} + + + + + + {isDesktop && Number(userRoleId) == 19 ? ( +
+ + {/* */} +
+ + ) : ""} + + + +
+ + ); +} diff --git a/components/partials/sidebar/menu/menu-dragable.tsx b/components/partials/sidebar/menu/menu-dragable.tsx new file mode 100644 index 0000000..5ad72c4 --- /dev/null +++ b/components/partials/sidebar/menu/menu-dragable.tsx @@ -0,0 +1,212 @@ +"use client"; + +import React from 'react' +import Logo from '@/components/logo'; +import SidebarHoverToggle from '@/components/partials/sidebar/sidebar-hover-toggle'; +import { Ellipsis, LogOut } from "lucide-react"; +import { usePathname } from "@/components/navigation"; + +import { cn } from "@/lib/utils"; +import { getMenuList } from "@/lib/menus"; + +import { ScrollArea } from "@/components/ui/scroll-area"; +import { + Tooltip, + TooltipTrigger, + TooltipContent, + TooltipProvider +} from "@/components/ui/tooltip"; +import { useConfig } from "@/hooks/use-config"; +import MenuLabel from "../common/menu-label"; + +import MenuItem from "../common/menu-item"; +import { CollapseMenuButton } from "../common/collapse-menu-button"; +import MenuWidget from "../common/menu-widget"; +import SearchBar from '@/components/partials/sidebar/common/search-bar' +import TeamSwitcher from '../common/team-switcher' + +// for dnd +import { + DndContext, + KeyboardSensor, + MouseSensor, + TouchSensor, + closestCenter, + useSensor, + type DragEndEvent, + type UniqueIdentifier, + useSensors, +} from "@dnd-kit/core"; +import { + restrictToVerticalAxis, + restrictToHorizontalAxis, +} from "@dnd-kit/modifiers"; +import { + useSortable, + arrayMove, + SortableContext, + verticalListSortingStrategy, + horizontalListSortingStrategy, +} from "@dnd-kit/sortable"; +import { useTranslations } from 'next-intl'; +import { useParams } from 'next/navigation' +import { getLangDir } from 'rtl-detect'; +import { CSS } from "@dnd-kit/utilities"; + +export function MenuDragAble() { + const t = useTranslations("Menu") + const pathname = usePathname(); + const menuList = getMenuList(pathname, t); + const [config, setConfig] = useConfig() + const collapsed = config.collapsed + + const params = useParams<{ locale: string; }>(); + const direction = getLangDir(params?.locale ?? ''); + // for dnd + // reorder rows after drag & drop + const [data, setData] = React.useState(menuList); + + const dataIds = React.useMemo( + () => data.flatMap(group => group.menus.map(menu => menu.id)), [data] + + ); + function handleDragEnd(event: DragEndEvent) { + const { active, over } = event; + + if (active && over && active.id !== over.id) { + setData((data) => { + + const dataIds = data.flatMap(group => group.menus.map(menu => menu.id)); + + const oldIndex = dataIds.indexOf(active.id as string); + const newIndex = dataIds.indexOf(over.id as string); + + if (oldIndex !== -1 && newIndex !== -1) { + // Flatten data + const flattenedMenus = data.flatMap(group => group.menus); + const updatedMenus = arrayMove(flattenedMenus, oldIndex, newIndex); + + // Reconstruct the data structure + let currentIndex = 0; + const updatedData = data.map(group => { + const groupMenusCount = group.menus.length; + const updatedGroupMenus = updatedMenus.slice(currentIndex, currentIndex + groupMenusCount); + currentIndex += groupMenusCount; + return { ...group, menus: updatedGroupMenus }; + }); + + return updatedData; + } + return data; + }); + } + } + + const sensors = useSensors( + useSensor(MouseSensor, {}), + useSensor(TouchSensor, {}), + useSensor(KeyboardSensor, {}) + ); + + return ( + + <> + + +
+ + +
+ + +
+ + + +
+ + + + + +
+ + ); +} \ No newline at end of file diff --git a/components/partials/sidebar/menu/menu-two-column.tsx b/components/partials/sidebar/menu/menu-two-column.tsx new file mode 100644 index 0000000..4739822 --- /dev/null +++ b/components/partials/sidebar/menu/menu-two-column.tsx @@ -0,0 +1,45 @@ +"use client"; + +import React from 'react' +import { Ellipsis, LogOut } from "lucide-react"; +import { usePathname } from "@/components/navigation"; + + +import { cn } from "@/lib/utils"; +import { getMenuList, type Group, type Menu, type Submenu } from "@/lib/menus"; + +import { ScrollArea } from "@/components/ui/scroll-area"; +import { + Tooltip, + TooltipTrigger, + TooltipContent, + TooltipProvider +} from "@/components/ui/tooltip"; +import { useConfig } from "@/hooks/use-config"; +import MenuLabel from "../common/menu-label"; + +import MenuItem from "../common/menu-item"; +import { CollapseMenuButton } from "../common/collapse-menu-button"; +import MenuWidget from "../common/menu-widget"; +import SearchBar from '@/components/partials/sidebar/common/search-bar' +import TeamSwitcher from '../common/team-switcher' +import IconNav from './icon-nav'; +import SidebarNav from './sidebar-nav'; +import { useTranslations } from 'next-intl'; + + +export function MenuTwoColumn() { + // translate + const t = useTranslations("Menu") + const pathname = usePathname(); + const menuList = getMenuList(pathname, t); + + return ( + <> + + + + + + ); +} diff --git a/components/partials/sidebar/menu/sheet-menu.tsx b/components/partials/sidebar/menu/sheet-menu.tsx new file mode 100644 index 0000000..fe89338 --- /dev/null +++ b/components/partials/sidebar/menu/sheet-menu.tsx @@ -0,0 +1,61 @@ +"use client"; +import { Link } from "@/i18n/routing"; +import { MenuIcon, PanelsTopLeft } from "lucide-react"; +import { Icon } from "@/components/ui/icon"; +import { Button } from "@/components/ui/button"; +import { Menu } from "@/components/partials/sidebar/menu"; +import { + Sheet, + SheetHeader, + SheetContent, + SheetTrigger, +} from "@/components/ui/sheet"; +import { MenuClassic } from "./menu-classic"; +import DashCodeLogo from "@/components/dascode-logo"; +import { useMobileMenuConfig } from "@/hooks/use-mobile-menu"; +import { useMediaQuery } from "@/hooks/use-media-query"; +import { useConfig } from "@/hooks/use-config"; + +export function SheetMenu() { + const [mobileMenuConfig, setMobileMenuConfig] = useMobileMenuConfig(); + const [config, setConfig] = useConfig(); + const { isOpen } = mobileMenuConfig; + + const isDesktop = useMediaQuery("(min-width: 1280px)"); + if (isDesktop) return null; + return ( + setMobileMenuConfig({ isOpen: !isOpen })} + > + + + + + + + logo + + + + + + ); +} diff --git a/components/partials/sidebar/menu/sidebar-nav.tsx b/components/partials/sidebar/menu/sidebar-nav.tsx new file mode 100644 index 0000000..270b2bf --- /dev/null +++ b/components/partials/sidebar/menu/sidebar-nav.tsx @@ -0,0 +1,94 @@ +'use client' +import React, { useEffect, useState } from 'react' +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Submenu, Group } from '@/lib/menus'; +import { useParams, usePathname } from 'next/navigation'; +import { Link } from "@/components/navigation" +import { useConfig } from '@/hooks/use-config'; +import { Button } from '@/components/ui/button'; +import MenuLabel from '../common/menu-label'; +import MenuItem from '../common/menu-item'; +import { Icon } from '@/components/ui/icon'; +import { CollapseMenuButton2 } from '../common/collapse-menu-button2'; +import TeamSwitcher from '../common/team-switcher'; +import SearchBar from '../common/search-bar'; +import { getLangDir } from 'rtl-detect'; +const SidebarNav = ({ menuList }: { menuList: Group[] }) => { + const [config, setConfig] = useConfig() + const pathname = usePathname(); + const params = useParams<{ locale: string; }>(); + const direction = getLangDir(params?.locale ?? ''); + const activeKey = pathname?.split('/')?.[2]; + const data = menuList.find(item => item.id === activeKey); + + // Render null if config.subMenu is true + if (config.subMenu || !config.hasSubMenu) { + return null; + } + + + + + return ( +
+ {config.sidebarBgImage !== undefined && ( +
+ )} + + + +
+ + + +
+
+ {data?.groupLabel && ( + + )} +
+ + +
+
+ ) +} + +export default SidebarNav \ No newline at end of file diff --git a/components/partials/sidebar/sidebar-content.tsx b/components/partials/sidebar/sidebar-content.tsx new file mode 100644 index 0000000..4249873 --- /dev/null +++ b/components/partials/sidebar/sidebar-content.tsx @@ -0,0 +1,61 @@ +'use client' +import React from 'react' +import { cn } from "@/lib/utils" +import { useConfig } from '@/hooks/use-config' +import { useMediaQuery } from "@/hooks/use-media-query"; +import { useMenuHoverConfig } from '@/hooks/use-menu-hover'; + +const SidebarContent = ({ children }: { children: React.ReactNode }) => { + const isDesktop = useMediaQuery("(min-width: 1280px)"); + const [config] = useConfig() + const [hoverConfig, setHoverConfig] = useMenuHoverConfig(); + const sidebarTheme = config.sidebarTheme !== 'light' ? `dark theme-${config.sidebarTheme}` : `theme-${config.sidebarTheme}` + + if (config.menuHidden || config.layout === "horizontal") return null + + if (config.sidebar === 'two-column') { + return ( + + + ) + } + + + return ( + + ) +} + +export default SidebarContent \ No newline at end of file diff --git a/components/partials/sidebar/sidebar-hover-toggle.tsx b/components/partials/sidebar/sidebar-hover-toggle.tsx new file mode 100644 index 0000000..c380dcb --- /dev/null +++ b/components/partials/sidebar/sidebar-hover-toggle.tsx @@ -0,0 +1,34 @@ +'use client' +import React from 'react' +import { useMenuHoverConfig } from '@/hooks/use-menu-hover' +import { cn } from "@/lib/utils" +import { useConfig } from '@/hooks/use-config' +import { useMediaQuery } from '@/hooks/use-media-query' + + +const SidebarHoverToggle = () => { + const [hoverConfig, setHoverConfig] = useMenuHoverConfig(); + const [config, setConfig] = useConfig(); + + const isDesktop = useMediaQuery("(min-width: 1280px)"); + if (config.sidebar !== 'draggable' || !isDesktop) { + return null + + } + + return ( + !config.collapsed || hoverConfig.hovered ?
setConfig({ ...config, collapsed: !config.collapsed })} + + className={cn("h-4 w-4 border-[1.5px] border-default-900 dark:border-default-700 rounded-full transition-all duration-150", { + "ring-2 ring-inset ring-offset-4 ring-black-900 dark:ring-default-400 bg-default-900 dark:bg-default-400 dark:ring-offset-default-700": !config.collapsed + })} + + >
+ : null + + + ) +} + +export default SidebarHoverToggle \ No newline at end of file diff --git a/components/partials/sidebar/sidebar-toggle.tsx b/components/partials/sidebar/sidebar-toggle.tsx new file mode 100644 index 0000000..33e1c30 --- /dev/null +++ b/components/partials/sidebar/sidebar-toggle.tsx @@ -0,0 +1,86 @@ +'use client' +import { ChevronLeft } from "lucide-react"; + +import { cn } from "@/lib/utils"; +import { Button } from "@/components/ui/button"; +import { useConfig } from "@/hooks/use-config"; +import { useMediaQuery } from "@/hooks/use-media-query"; +import { Icon } from "@/components/ui/icon"; +import { motion } from 'framer-motion'; + + +export function SidebarToggle() { + const [config, setConfig] = useConfig() + const collapsed = config.collapsed + const isDesktop = useMediaQuery('(min-width: 1280px)') + if (!isDesktop) return null + if (config.sidebar === 'two-column' && !config.hasSubMenu || config.menuHidden || config.layout === "horizontal" || config.sidebar === 'draggable' ) { + return null + } + if (config.sidebar === 'two-column') { + return ( + + + + ) + } + return ( + + + + ); +} \ No newline at end of file diff --git a/components/project/activity.tsx b/components/project/activity.tsx new file mode 100644 index 0000000..4ed9381 --- /dev/null +++ b/components/project/activity.tsx @@ -0,0 +1,28 @@ +import { Check } from "lucide-react"; + +interface ActivityItemProps { + activity: { + title: string; + date: string; + } +} +const ActivityItem = ({ activity }: ActivityItemProps) => { + const { title, date } = activity; + return ( +
  • +
    + +
    +
    +

    + {title} +

    +

    + {date} +

    +
    +
  • + ); +}; + +export default ActivityItem; diff --git a/components/project/deals-distribution-chart.tsx b/components/project/deals-distribution-chart.tsx new file mode 100644 index 0000000..2203543 --- /dev/null +++ b/components/project/deals-distribution-chart.tsx @@ -0,0 +1,78 @@ +"use client"; +import dynamic from "next/dynamic"; +const Chart = dynamic(() => import("react-apexcharts"), { ssr: false }); +import { useTheme } from "next-themes"; +import { + getGridConfig, + getXAxisConfig, + getYAxisConfig, +} from "@/lib/appex-chart-options"; +import { colors } from "@/lib/colors"; + +interface DealsDistributionChartProps { + height?: number; + seriesData?: number[]; +} +const DealsDistributionChart = ({ + height = 300 , + seriesData =[90, 70, 85, 60, 80, 70, 90, 75, 60, 80] + +}: DealsDistributionChartProps) => { + + const { theme: mode } = useTheme(); + const series = [ + { + data:seriesData + }, + ]; + const options: any = { + chart: { + toolbar: { + show: false, + }, + }, + dataLabels: { + enabled: false, + }, + stroke: { + curve: "smooth", + width: 4, + }, + colors: [colors.primary], + tooltip: { + theme: mode === "dark" ? "dark" : "light", + }, + grid: getGridConfig(), + fill: { + type: "gradient", + colors: [colors.primary], + gradient: { + shadeIntensity: 1, + opacityFrom: 0.4, + opacityTo: 0.5, + stops: [50, 100, 0], + }, + }, + yaxis: getYAxisConfig(mode === 'light' ? colors["default-600"] : colors["default-300"]), + xaxis: getXAxisConfig( + mode === 'light' ? colors["default-600"] : colors["default-300"] + ), + padding: { + top: 0, + right: 0, + bottom: 0, + left: 0, + }, + }; + return ( + + ); +}; + +export default DealsDistributionChart; diff --git a/components/project/message-list-item.tsx b/components/project/message-list-item.tsx new file mode 100644 index 0000000..4861d89 --- /dev/null +++ b/components/project/message-list-item.tsx @@ -0,0 +1,46 @@ +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { cn } from "@/lib/utils"; + +interface MessageListItemProps { + message: { + title: string; + desc: string; + active?: boolean; + hasnotifaction?: boolean; + notification_count?: number; + image: string; + } +} + +const MessageListItem = ({ message }: MessageListItemProps) => { + const { title, desc, active, hasnotifaction, notification_count, image } = message; + return ( +
  • +
    +
    + + + + + +
    +
    +
    {title}
    +
    {desc}
    +
    3 min ago
    +
    +
    + {hasnotifaction && ( +
    + + {notification_count} + +
    + )} +
  • + ); +}; + +export default MessageListItem; \ No newline at end of file diff --git a/components/project/notes-calendar.tsx b/components/project/notes-calendar.tsx new file mode 100644 index 0000000..d5620f4 --- /dev/null +++ b/components/project/notes-calendar.tsx @@ -0,0 +1,18 @@ +"use client" + +import { useState } from "react" +import { Calendar } from "@/components/ui/calendar" + +const NotesCalendar = () => { + const [date, setDate] = useState(new Date()) + + return ( + + ) +} + +export default NotesCalendar; \ No newline at end of file diff --git a/components/project/task-item.tsx b/components/project/task-item.tsx new file mode 100644 index 0000000..a0a172d --- /dev/null +++ b/components/project/task-item.tsx @@ -0,0 +1,31 @@ +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; +import { SquarePen, Trash2 } from "lucide-react"; + +interface TaskItemProps { + task: { image: string; title: string }; +} +const TaskItem = ({ task }: TaskItemProps) => { + const { image, title } = task + return ( +
  • +
    + +
    +
    + + + SA + +
    +
    +
    {title}
    +
    + + +
  • + ); +}; + +export default TaskItem; \ No newline at end of file diff --git a/components/project/team-table.tsx b/components/project/team-table.tsx new file mode 100644 index 0000000..0e982e6 --- /dev/null +++ b/components/project/team-table.tsx @@ -0,0 +1,298 @@ +"use client" + +import * as React from "react" +import Chart from "react-apexcharts"; +import { + ColumnDef, + ColumnFiltersState, + PaginationState, + SortingState, + VisibilityState, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, +} from "@tanstack/react-table" +import { Button } from "@/components/ui/button" + +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" +import { Eye, MoreVertical, SquarePen, Trash2 } from "lucide-react" + +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" +import { colors } from "@/lib/colors" + + +export type TeamTableProps = { + customer: { + name: string; + image: string; + deg: string; + }, + status: "progress" | "complete"; + time: string; + chart: null; + action:null; +} +const series = [ + { + data: [800, 600, 1000, 800, 600, 1000, 800, 900], + } +]; +const options: any = { + chart: { + toolbar: { + autoSelected: "pan", + show: false, + }, + offsetX: 0, + offsetY: 0, + zoom: { + enabled: false, + }, + sparkline: { + enabled: true, + }, + }, + dataLabels: { + enabled: false, + }, + stroke: { + curve: "smooth", + width: 2, + }, + colors: [colors.primary], + tooltip: { + theme: "dark", + }, + grid: { + show: false, + padding: { + left: 0, + right: 0, + }, + }, + yaxis: { + show: false, + }, + fill: { + type: "solid", + opacity: [0.1], + }, + legend: { + theme: "dark", + show: false, + }, + xaxis: { + low: 0, + offsetX: 0, + offsetY: 0, + show: false, + labels: { + low: 0, + offsetX: 0, + show: false, + }, + axisBorder: { + low: 0, + offsetX: 0, + show: false, + }, + }, +}; + + +export const columns: ColumnDef[] = [ + { + accessorKey: "customer", + header: "ASSIGNEE", + cell: ({ row }) => ( +
    +
    +
    + + + SC + +
    +
    +
    + {row.original.customer.name} +
    +
    + ), + }, + { + accessorKey: "status", + header: "status", + enableSorting: false, + cell: ({ row }) => { + return ( +
    +
    + {row.getValue("status") === "progress" && ( + + + In progress + + )} + {row.getValue("status") === "complete" && ( + + + Complete + + )} +
    +
    + ) + } + }, + { + accessorKey: "time", + header: "Time", + cell: ({ row }) => ( + {row.getValue("time")} + ) + }, + { + accessorKey: "chart", + header: "Chart", + cell: ({ row }) => ( +
    + +
    + ) + }, + + { + id: "actions", + accessorKey: "action", + header: "Actions", + enableHiding: false, + cell: ({ row }) => { + return ( +
    + + + + + + + + View + + + + Edit + + + Delete + + +
    + ) + } + } +] + +const TeamTable = ({ data }: any) => { + const [sorting, setSorting] = React.useState([]) + const [columnFilters, setColumnFilters] = React.useState([]) + const [columnVisibility, setColumnVisibility] = React.useState({}) + const [rowSelection, setRowSelection] = React.useState({}) + const [pagination, setPagination] = React.useState({ + pageIndex: 0, + pageSize: 6, + }) + + const table = useReactTable({ + data, + columns, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + onColumnVisibilityChange: setColumnVisibility, + onRowSelectionChange: setRowSelection, + onPaginationChange: setPagination, + state: { + sorting, + columnFilters, + columnVisibility, + rowSelection, + pagination + }, + }) + + return ( +
    + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder ? null : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ))} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + )) + ) : ( + + + No results. + + + )} + +
    + + +
    + ) +} + +export default TeamTable; + diff --git a/components/revenue-bar-chart.tsx b/components/revenue-bar-chart.tsx new file mode 100644 index 0000000..f1afa9e --- /dev/null +++ b/components/revenue-bar-chart.tsx @@ -0,0 +1,174 @@ +"use client" + +import { useConfig } from "@/hooks/use-config"; +import { useTranslations } from "next-intl"; +import { useTheme } from "next-themes"; +import dynamic from "next/dynamic"; +const Chart = dynamic(() => import("react-apexcharts"), { ssr: false }); + +interface RevenueBarChartProps { + height?: number; + chartType?: "bar" | "area"; + series?: any[]; + chartColors?: string[] +} +const defaultSeries = [{ + name: "Net Profit", + data: [44, 55, 57, 56, 61, 58, 63, 60, 66], +}, +{ + name: "Revenue", + data: [76, 85, 101, 98, 87, 105, 91, 114, 94], +}, +{ + name: "Free Cash Flow", + data: [35, 41, 36, 26, 45, 48, 52, 53, 41], +}] +const RevenueBarChart = ({ + height = 400, + chartType = "bar", + series = defaultSeries, + chartColors = ["#4669FA", "#0CE7FA", "#FA916B"] + +}: RevenueBarChartProps) => { + const [config] = useConfig(); + const { isRtl } = config; + const t = useTranslations("AnalyticsDashboard"); + const { theme: mode } = useTheme(); + const options: any = { + chart: { + toolbar: { + show: false, + }, + }, + plotOptions: { + bar: { + horizontal: false, + endingShape: "rounded", + columnWidth: "45%", + }, + }, + legend: { + show: true, + position: "top", + horizontalAlign: "right", + fontSize: "12px", + fontFamily: "Inter", + offsetY: -30, + markers: { + width: 8, + height: 8, + offsetY: -1, + offsetX: -5, + radius: 12, + }, + labels: { + colors: mode === "dark" ? "#CBD5E1" : "#475569", + }, + itemMargin: { + horizontal: 18, + vertical: 0, + }, + }, + title: { + text: `${t("revenue_report", { defaultValue: "Revenue Report" })}`, + align: "left", + offsetY: 13, + offsetX: isRtl ? "0%" : 0, + floating: false, + style: { + fontSize: "20px", + fontWeight: "500", + fontFamily: "Inter", + color: mode === "dark" ? "#fff" : "#0f172a", + }, + }, + dataLabels: { + enabled: false, + }, + stroke: { + show: true, + width: 2, + colors: ["transparent"], + }, + yaxis: { + labels: { + style: { + colors: mode === "dark" ? "#CBD5E1" : "#475569", + fontFamily: "Inter", + }, + }, + }, + xaxis: { + categories: [ + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + ], + labels: { + style: { + colors: mode === "dark" ? "#CBD5E1" : "#475569", + fontFamily: "Inter", + }, + }, + axisBorder: { + show: false, + }, + axisTicks: { + show: false, + }, + }, + + fill: { + opacity: 1, + }, + tooltip: { + y: { + formatter: function (val: number) { + return "$ " + val + " thousands"; + }, + }, + }, + colors: chartColors, + grid: { + show: false, + borderColor: mode === "dark" ? "#334155" : "#E2E8F0", + strokeDashArray: 10, + position: "back", + }, + responsive: [ + { + breakpoint: 600, + options: { + legend: { + position: "bottom", + offsetY: 8, + horizontalAlign: "center", + }, + plotOptions: { + bar: { + columnWidth: "80%", + }, + }, + }, + }, + ], + }; + return ( + + ); +}; + +export default RevenueBarChart; diff --git a/components/shimmer/page.tsx b/components/shimmer/page.tsx new file mode 100644 index 0000000..a4e0e0d --- /dev/null +++ b/components/shimmer/page.tsx @@ -0,0 +1,39 @@ +import Image from "next/image"; +import ViewSource from "../view-source/view-source"; + + +const shimmer = (w: number, h: number) => ` + + + + + + + + + + + +`; + +const toBase64 = (str: string) => (typeof window === "undefined" ? Buffer.from(str).toString("base64") : window.btoa(str)); + +const Shimmer = () => ( +
    + +

    Image Component With Shimmer Data URL

    + Mountains +
    +); + +export default Shimmer; diff --git a/components/ui/accordion.tsx b/components/ui/accordion.tsx new file mode 100644 index 0000000..ad5a178 --- /dev/null +++ b/components/ui/accordion.tsx @@ -0,0 +1,58 @@ +"use client" + +import * as React from "react" +import * as AccordionPrimitive from "@radix-ui/react-accordion" +import { ChevronDown } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Accordion = AccordionPrimitive.Root + +const AccordionItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AccordionItem.displayName = "AccordionItem" + +const AccordionTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + svg]:rotate-180 rounded-md data-[state=open]:rounded-b-none ", + className + )} + {...props} + > + {children} + + + +)); +AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName + +const AccordionContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + +
    {children}
    +
    +)) + +AccordionContent.displayName = AccordionPrimitive.Content.displayName + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/components/ui/alert-dialog.tsx b/components/ui/alert-dialog.tsx new file mode 100644 index 0000000..6d1c5d5 --- /dev/null +++ b/components/ui/alert-dialog.tsx @@ -0,0 +1,141 @@ +"use client" + +import * as React from "react" +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" + +import { cn } from "@/lib/utils" +import { buttonVariants } from "@/components/ui/button" + +const AlertDialog = AlertDialogPrimitive.Root + +const AlertDialogTrigger = AlertDialogPrimitive.Trigger + +const AlertDialogPortal = AlertDialogPrimitive.Portal + +const AlertDialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName + +const AlertDialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + +)) +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName + +const AlertDialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
    +) +AlertDialogHeader.displayName = "AlertDialogHeader" + +const AlertDialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
    +) +AlertDialogFooter.displayName = "AlertDialogFooter" + +const AlertDialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName + +const AlertDialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogDescription.displayName = + AlertDialogPrimitive.Description.displayName + +const AlertDialogAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName + +const AlertDialogCancel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +} diff --git a/components/ui/alert.tsx b/components/ui/alert.tsx new file mode 100644 index 0000000..8806eca --- /dev/null +++ b/components/ui/alert.tsx @@ -0,0 +1,158 @@ +"use client"; +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; +import { Icon } from "@iconify/react"; +import { cn } from "@/lib/utils"; +import { color, rounded, shadow } from "@/lib/type"; + +const alertVariants = cva( + "relative w-full py-[18px] px-6 font-normal text-sm rounded-md flex md:items-center items-start gap-4", + { + variants: { + color: { + default: "bg-default text-default-foreground", + primary: "bg-primary text-primary-foreground", + secondary: "bg-secondary text-secondary-foreground", + success: "bg-success text-success-foreground", + info: "bg-info text-info-foreground", + warning: "bg-warning text-warning-foreground", + destructive: "bg-destructive text-destructive-foreground", + }, + variant: { + outline: "border border-default text-default bg-transparent", + soft: "text-default bg-default/10", + }, + }, + compoundVariants: [ + { + variant: "outline", + color: "primary", + className: "text-primary border-primary", + }, + { + variant: "outline", + color: "secondary", + className: "text-default-400 border-secondary ", + }, + { + variant: "outline", + color: "success", + className: "text-success border-success", + }, + { + variant: "outline", + color: "info", + className: "text-info border-info", + }, + { + variant: "outline", + color: "warning", + className: "text-warning border-warning", + }, + { + variant: "outline", + color: "destructive", + className: "text-destructive border-destructive", + }, + { + variant: "soft", + color: "primary", + className: "text-primary bg-primary/10", + }, + { + variant: "soft", + color: "secondary", + className: "text-default-500 bg-secondary/80", + }, + { + variant: "soft", + color: "success", + className: "text-success bg-success/10", + }, + { + variant: "soft", + color: "info", + className: "text-info bg-info/10", + }, + { + variant: "soft", + color: "warning", + className: "text-warning bg-warning/10", + }, + { + variant: "soft", + color: "destructive", + className: "text-destructive bg-destructive/10", + } + ], + defaultVariants: { + color: "default", + } + } +); + +export interface AlertProps + extends React.HTMLAttributes, + VariantProps { + dismissible?: boolean; + onDismiss?: () => void; + color?: color; + shadow?: shadow; + rounded?: rounded; +} + +const Alert = React.forwardRef( + ({ className, color, variant, dismissible, onDismiss, children, ...props }, ref) => { + const [dismissed, setDismissed] = React.useState(false); + + const handleDismiss = () => { + setDismissed(true); + if (onDismiss) { + onDismiss(); + } + }; + + return !dismissed ? ( +
    + {children} + {dismissible && ( + + )} +
    + ) : null; + } +); +Alert.displayName = "Alert"; + +const AlertTitle = React.forwardRef>( + ({ className, ...props }, ref) => ( +
    + ) +); +AlertTitle.displayName = "AlertTitle"; + +const AlertDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
    +)); +AlertDescription.displayName = "AlertDescription"; + +export { Alert, AlertTitle, AlertDescription }; + diff --git a/components/ui/avatar.tsx b/components/ui/avatar.tsx new file mode 100644 index 0000000..52d8ffa --- /dev/null +++ b/components/ui/avatar.tsx @@ -0,0 +1,213 @@ +"use client"; + +import * as React from "react"; +import * as AvatarPrimitive from "@radix-ui/react-avatar"; + +import { cn } from "@/lib/utils"; +import { cva } from "class-variance-authority"; +import { color, rounded } from "@/lib/type"; + +export interface AvatarProps extends React.HTMLAttributes { + size?: "sm" | "base" | "md" | "lg" | "xl"; + shape?: "circle" | "square"; + color?: color; + variant?: "default" | "outline" | "soft" | "ghost"; + rounded?: rounded; +} + +const avatarVariant = cva( + "inline-flex items-center justify-center font-normal text-foreground select-none shrink-0 overflow-hidden", + { + variants: { + color: { + default: "bg-default text-default-foreground hover:bg-default/90 hover:ring-default", + primary: "bg-primary text-primary-foreground hover:bg-primary/90 hover:ring-primary", + secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 hover:ring-secondary", + destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90 hover:ring-destructive", + warning: "bg-warning text-warning-foreground hover:bg-warning/90 hover:ring-warning", + info: "bg-info text-info-foreground hover:bg-info/90 hover:ring-info", + success: "bg-success text-success-foreground hover:bg-success/90 hover:ring-success", + }, + variant: { + default: "", + outline: "border border-default text-default bg-transparent hover:bg-default hover:text-default-foreground hover:ring-0 hover:ring-transparent", + soft: "bg-opacity-10 text-default-foreground hover:bg-opacity-100 hover:text-primary-foreground", + ghost: "bg-transparent text-default hover:bg-transparent-10 hover:ring-0 hover:ring-transparent", + }, + size: { + sm: "h-10 w-10 text-xs", + base: "h-16 w-16 text-2xl", + md: "h-24 w-24 text-3xl", + lg: "h-28 w-28 text-4xl", + xl: "h-32 w-32 text-5xl", + }, + rounded: { + sm: "rounded-sm", + md: "rounded-md", + lg: "rounded-lg", + full: "rounded-full" + }, + shape: { + circle: "rounded-full", + square: "rounded-md", + }, + }, + compoundVariants: [ + { + variant: "outline", + color: "primary", + class: "text-primary border-primary hover:text-primary-foreground hover:border-primary hover:bg-primary", + }, + { + variant: "outline", + color: "secondary", + class: "text-secondary border-secondary hover:text-secondary-foreground hover:border-secondary hover:bg-secondary", + }, + { + variant: "outline", + color: "success", + class: "text-success border-success hover:text-success-foreground hover:border-success hover:bg-success", + }, + { + variant: "outline", + color: "info", + class: "text-info border-info hover:text-info-foreground hover:border-info hover:bg-info", + }, + { + variant: "outline", + color: "warning", + class: "text-warning border-warning hover:text-warning-foreground hover:border-warning hover:bg-warning", + }, + { + variant: "outline", + color: "destructive", + class: "text-destructive border-destructive hover:text-destructive-foreground hover:border-destructive hover:bg-destructive", + }, + { + variant: "outline", + color: "info", + class: "text-info hover:text-info-foreground hover:border-info hover:bg-info", + }, + { + variant: "soft", + color: "primary", + class: "text-primary hover:text-primary-foreground", + }, + { + variant: "soft", + color: "secondary", + class: "text-secondary hover:text-secondary-foreground", + }, + { + variant: "soft", + color: "success", + class: "text-success hover:text-success-foreground", + }, + { + variant: "soft", + color: "info", + class: "text-info hover:text-info-foreground", + }, + { + variant: "soft", + color: "warning", + class: "text-warning hover:text-warning-foreground", + }, + { + variant: "soft", + color: "destructive", + class: "bg-destructive/10 text-destructive hover:text-destructive-foreground", + }, + { + variant: "ghost", + color: "primary", + class: "text-primary bg-primary/10", + }, + { + variant: "ghost", + color: "secondary", + class: "text-secondary bg-secondary/10", + }, + { + variant: "ghost", + color: "success", + class: "text-success bg-success/10", + }, + { + variant: "ghost", + color: "info", + class: "text-info bg-info/10", + }, + { + variant: "ghost", + color: "warning", + class: "text-warning bg-warning/10", + }, + { + variant: "ghost", + color: "destructive", + class: "text-destructive bg-destructive/10", + }, + { + variant: "ghost", + color: "default", + class: "text-default bg-default/10", + }, + ], + defaultVariants: { + color: "default", + size: "sm", + rounded: "full", + variant: "default" + }, + } +); + +const Avatar = React.forwardRef( + ({ size, shape, color, variant, rounded, className, ...props }, ref) => { + return ( + + ); + } +); +Avatar.displayName = AvatarPrimitive.Root.displayName; + + +export interface AvatarImageProps extends React.ImgHTMLAttributes { } + +const AvatarImage = React.forwardRef( + ({ className, ...props }, ref) => { + return ( + + ); + } +); +AvatarImage.displayName = AvatarPrimitive.Image.displayName; + + +export interface AvatarFallbackProps extends React.HTMLAttributes { } + +const AvatarFallback = React.forwardRef( + ({ className, ...props }, ref) => { + return ( + + ); + } +); +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; + + +export { Avatar, AvatarImage, AvatarFallback }; + diff --git a/components/ui/badge.tsx b/components/ui/badge.tsx index 0205413..0ce261d 100644 --- a/components/ui/badge.tsx +++ b/components/ui/badge.tsx @@ -1,46 +1,52 @@ -import * as React from "react" -import { Slot } from "@radix-ui/react-slot" -import { cva, type VariantProps } from "class-variance-authority" +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; +import { color, rounded } from "@/lib/type"; const badgeVariants = cva( - "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", + "inline-flex items-center rounded-md border py-1 px-2 text-xs capitalize font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", { variants: { - variant: { - default: - "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", - secondary: - "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", - destructive: - "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", - outline: - "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + color: { + default: "border-transparent bg-default text-default-foreground", + primary: "border-transparent bg-primary text-primary-foreground", + secondary: "bg-secondary border-transparent text-secondary-foreground ", + destructive: "bg-destructive border-transparent text-destructive-foreground", + success: "bg-success border-transparent text-success-foreground ", + info: "bg-info border-transparent text-info-foreground ", + warning: "bg-warning border-transparent text-warning-foreground", }, + rounded: { + sm: "rounded", + md: "rounded-md", + lg: "rounded-lg", + full: "rounded-full", + } }, + defaultVariants: { - variant: "default", + color: "default", + rounded: "md", }, } -) +); +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps { -function Badge({ - className, - variant, - asChild = false, - ...props -}: React.ComponentProps<"span"> & - VariantProps & { asChild?: boolean }) { - const Comp = asChild ? Slot : "span" - - return ( - - ) + color?: color; + rounded?: rounded; } -export { Badge, badgeVariants } + +function Badge({ className, color, rounded, ...props }: BadgeProps) { + return ( +
    + ); +} + +export { Badge, badgeVariants }; diff --git a/components/ui/breadcrumb.tsx b/components/ui/breadcrumb.tsx index eb88f32..993a4ba 100644 --- a/components/ui/breadcrumb.tsx +++ b/components/ui/breadcrumb.tsx @@ -4,99 +4,105 @@ import { ChevronRight, MoreHorizontal } from "lucide-react" import { cn } from "@/lib/utils" -function Breadcrumb({ ...props }: React.ComponentProps<"nav">) { - return