Merge branch 'main' of https://gitlab.com/hanifsalafi/new-netidhub-public into dev-1
This commit is contained in:
commit
ff6fbe6217
|
|
@ -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 (
|
||||
<div className="h-full overflow-auto bg-gradient-to-br from-slate-50/50 via-white to-slate-50/50 flex items-center justify-center">
|
||||
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-blue-500"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className="h-full overflow-auto bg-gradient-to-br from-slate-50/50 via-white to-slate-50/50"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<div className="p-6">
|
||||
<DashboardContainer />
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import { AdminLayout } from "@/components/layout/admin-layout";
|
||||
|
||||
export default function AdminPageLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return <AdminLayout>{children}</AdminLayout>;
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<div className="h-full overflow-auto bg-gray-50 flex items-center justify-center">
|
||||
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-blue-500"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className="h-full overflow-auto bg-gray-50"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<DashboardContainer />
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<MountedProvider isProtected={true}>
|
||||
<LayoutProvider>
|
||||
<ThemeCustomize />
|
||||
<DashCodeHeader />
|
||||
<DashCodeSidebar />
|
||||
<LayoutContentProvider>{children}</LayoutContentProvider>
|
||||
<DashCodeFooter />
|
||||
</LayoutProvider>
|
||||
</MountedProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default layout;
|
||||
|
Before Width: | Height: | Size: 228 KiB After Width: | Height: | Size: 228 KiB |
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<html lang={locale} dir={direction}>
|
||||
<head>
|
||||
{/* DNS Prefetch for external domains */}
|
||||
<link rel="dns-prefetch" href="//fonts.googleapis.com" />
|
||||
<link rel="dns-prefetch" href="//fonts.gstatic.com" />
|
||||
|
||||
{/* Preconnect to external domains */}
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link
|
||||
rel="preconnect"
|
||||
href="https://fonts.gstatic.com"
|
||||
crossOrigin="anonymous"
|
||||
/>
|
||||
|
||||
{/* Preload critical fonts */}
|
||||
<link
|
||||
rel="preload"
|
||||
href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&display=swap"
|
||||
as="style"
|
||||
/>
|
||||
<noscript>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</noscript>
|
||||
</head>
|
||||
<body className={`${inter.className} dashcode-app`}>
|
||||
<NextIntlClientProvider messages={messages} locale={locale}>
|
||||
<AuthProvider>
|
||||
<ThemeProvider attribute="class" defaultTheme="light">
|
||||
<DirectionProvider direction={direction}>
|
||||
{children}
|
||||
</DirectionProvider>
|
||||
<Toaster />
|
||||
<SonnerToaster />
|
||||
</ThemeProvider>
|
||||
</AuthProvider>
|
||||
</NextIntlClientProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<div className="relative min-h-screen font-[family-name:var(--font-geist-sans)]">
|
||||
<div className="relative z-10 bg-white w-full mx-auto">
|
||||
<Navbar />
|
||||
<div className="flex-1">
|
||||
<Header />
|
||||
</div>
|
||||
<MediaUpdate />
|
||||
<Category />
|
||||
<Footer />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
122
app/globals.css
122
app/globals.css
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<html lang="en">
|
||||
<body>{children}</body>
|
||||
<html lang="in">
|
||||
<body className={`${inter.className} dashcode-app`}>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
22
app/page.tsx
22
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 (
|
||||
<div className="relative min-h-screen font-[family-name:var(--font-geist-sans)]">
|
||||
<div className="relative z-10 bg-white w-full mx-auto">
|
||||
<Navbar />
|
||||
<div className="flex-1">
|
||||
<Header />
|
||||
</div>
|
||||
<MediaUpdate />
|
||||
<Category />
|
||||
<Footer />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
export default function RootPage() {
|
||||
return <AutoRedirect />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<Card className={cn("", className)}>
|
||||
<CardContent className="py-3 px-4">
|
||||
<div className="flex flex-col gap-2 md:flex-row items-center">
|
||||
<div className="flex-1">
|
||||
<div className="text-sm text-default-600 mb-1.5">{title}</div>
|
||||
<div className="text-lg text-default-900 font-medium mb-1.5">
|
||||
{total}
|
||||
</div>
|
||||
<div className="font-normal text-xs text-default-600 whitespace-nowrap">
|
||||
<span className="text-primary me-1">{percentage}</span>
|
||||
{t("statistics_graph_desc", { defaultValue: "Statistics Graph Desc" })}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-none">
|
||||
<Chart
|
||||
options={options}
|
||||
series={series}
|
||||
type={chartType}
|
||||
height={height}
|
||||
width={"100%"}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default EarningBlock;
|
||||
|
|
@ -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 = <span className="text-warning">-60% </span>
|
||||
}: 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 (
|
||||
<Card className={cn("p-4", className)}>
|
||||
<CardContent className="p-0 ">
|
||||
{
|
||||
title && (
|
||||
<div className="text-sm text-default-600 mb-1.5">
|
||||
{title}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{total && (
|
||||
<div className="text-lg text-default-900 font-medium mb-1.5">
|
||||
{total}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<div className="font-normal text-xs text-default-600">
|
||||
{percentageContent}
|
||||
<span className="ms-1">From last Week</span>
|
||||
</div>
|
||||
<div className="mt-5">
|
||||
<Chart
|
||||
options={options}
|
||||
series={chartSeries}
|
||||
type={chartType}
|
||||
height={height}
|
||||
width={"100%"}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card >
|
||||
);
|
||||
};
|
||||
|
||||
export default OrdersBlock;
|
||||
|
|
@ -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 (
|
||||
<Card className={cn("", className)}>
|
||||
<CardContent className="py-[18px] px-4">
|
||||
{
|
||||
title &&
|
||||
<div className="text-default-500 dark:text-default-900 text-sm font-medium mb-3">
|
||||
{title}
|
||||
</div>
|
||||
}
|
||||
<Chart
|
||||
options={options}
|
||||
series={series}
|
||||
type={chartType}
|
||||
height={height}
|
||||
width={"100%"}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProgressBlock;
|
||||
|
||||
|
|
@ -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 (
|
||||
<Card className={cn(" ", className)}>
|
||||
<CardContent className=" py-[18px] px-4 ">
|
||||
|
||||
<div className="flex gap-6">
|
||||
<div className="flex-none">
|
||||
<Chart
|
||||
options={options}
|
||||
series={chartSeries}
|
||||
type={chartType}
|
||||
height={48}
|
||||
width={48}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="text-default-800 text-sm mb-1 font-medium">
|
||||
{title}
|
||||
</div>
|
||||
<div className="text-default-900 text-lg font-medium">
|
||||
{total}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export { StatisticsBlock };
|
||||
|
|
@ -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 (
|
||||
<Card className={cn('', className,)}>
|
||||
<CardContent className="p-4">
|
||||
<div className={cn('flex gap-3', {
|
||||
'flex-row-reverse': reverse
|
||||
})}>
|
||||
{
|
||||
icon &&
|
||||
<div className="flex-none">
|
||||
<div
|
||||
className={cn("h-12 w-12 rounded-full flex flex-col items-center justify-center text-2xl bg-default/10", iconWrapperClass)}
|
||||
>
|
||||
{icon}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
{(title || total) && (
|
||||
<div className="flex-1">
|
||||
{title && (
|
||||
<div className="text-default-600 text-sm font-medium">
|
||||
{title}
|
||||
</div>
|
||||
)}
|
||||
{total && (
|
||||
<div className="text-default-900 text-lg font-medium">
|
||||
{total}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
<div className="ms-auto max-w-[124px]">
|
||||
<Chart
|
||||
options={options}
|
||||
series={chartSeries}
|
||||
type={chartType}
|
||||
height={41}
|
||||
width={124}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card >
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
export { StatusBlock };
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
interface UpgradeProps {
|
||||
image?: any;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
const UpgradeBlock = ({ children, className }: UpgradeProps) => {
|
||||
return (
|
||||
<div
|
||||
className={cn("p-6 relative bg-default-900 rounded-2xl", className)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export { UpgradeBlock };
|
||||
|
|
@ -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 (
|
||||
<div
|
||||
className={cn('bg-no-repeat bg-cover bg-center p-4 rounded-md relative z-10', className)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const BlockBadge = ({ children, className }: { children: React.ReactNode, className?: string }) => {
|
||||
return (
|
||||
<div className={cn('absolute z-10 top-1/2 -translate-y-1/2 end-6 mt-2 h-12 w-12 bg-primary-foreground dark:bg-default-900 dark:text-default-100 rounded-full text-xs font-medium flex flex-col items-center justify-center', className)} >
|
||||
{children}
|
||||
</div >
|
||||
)
|
||||
}
|
||||
|
||||
export { WelcomeBlock, BlockBadge };
|
||||
|
|
@ -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 (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center">
|
||||
{title && (
|
||||
<CardTitle className="flex-1 leading-normal"> {title}</CardTitle>
|
||||
)}
|
||||
{code && (
|
||||
<div className="flex-none ">
|
||||
<Switch id="airplane-mode" onClick={toggle} />
|
||||
|
||||
</div>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{children}
|
||||
<Collapsible open={show}>
|
||||
<CollapsibleContent className="CollapsibleContent relative" >
|
||||
<div className="absolute end-2 top-2">
|
||||
<CopyButton
|
||||
event="copy_chart_code"
|
||||
name={title}
|
||||
code={code}
|
||||
className="[&_svg]-h-3 h-6 w-6 rounded-[6px] bg-background hover:bg-background hover:text-foreground text-foreground shadow-none [&_svg]:w-3"
|
||||
|
||||
/></div>
|
||||
<SyntaxHighlighter
|
||||
language="javascript"
|
||||
className=" rounded-md text-sm mt-6 "
|
||||
style={atomOneDark}
|
||||
customStyle={{
|
||||
padding: "24px",
|
||||
}}
|
||||
>
|
||||
{`${code}`}
|
||||
</SyntaxHighlighter>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default CardSnippet;
|
||||
|
|
@ -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 (
|
||||
<TooltipProvider>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
className={cn(
|
||||
"[&_svg]-h-3.5 h-7 w-7 rounded-[6px] [&_svg]:w-3.5 border-default-500 text-default-500 ",
|
||||
className
|
||||
)}
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(code)
|
||||
trackEvent({
|
||||
name: event,
|
||||
properties: {
|
||||
name,
|
||||
},
|
||||
})
|
||||
setHasCopied(true)
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<span className="sr-only">Copy</span>
|
||||
{hasCopied ? <CheckIcon /> : <ClipboardIcon />}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="bg-default text-default-foreground text-sm font-normal">{tooltip}</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
</TooltipProvider>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import React from 'react'
|
||||
type IconProps = React.HTMLAttributes<SVGElement>
|
||||
const DashCodeLogo = (props: IconProps) => {
|
||||
return (
|
||||
<>
|
||||
<svg
|
||||
{...props}
|
||||
width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 6C0 2.68629 2.68629 0 6 0H26C29.3137 0 32 2.68629 32 6V32H6C2.68629 32 0 29.3137 0 26V6Z" fill="currentColor" />
|
||||
<path d="M20 23.2C19.0133 23.2 18.0933 23.02 17.24 22.66C16.4 22.3 15.6667 21.7933 15.04 21.14C14.4133 20.4867 13.9267 19.7333 13.58 18.88C13.2467 18.0133 13.08 17.0867 13.08 16.1C13.08 15.1133 13.2467 14.1933 13.58 13.34C13.9267 12.4733 14.4067 11.72 15.02 11.08C15.6467 10.44 16.38 9.94 17.22 9.58C18.06 9.22 18.9667 9.04 19.94 9.04C20.9933 9.04 21.9333 9.22667 22.76 9.6C23.6 9.97333 24.3333 10.4867 24.96 11.14L23.96 12.14C23.48 11.6067 22.9 11.1933 22.22 10.9C21.54 10.5933 20.78 10.44 19.94 10.44C19.1667 10.44 18.4533 10.58 17.8 10.86C17.16 11.14 16.5933 11.54 16.1 12.06C15.62 12.5667 15.2467 13.1667 14.98 13.86C14.7267 14.54 14.6 15.2867 14.6 16.1C14.6 16.9133 14.7333 17.6667 15 18.36C15.2667 19.0533 15.64 19.66 16.12 20.18C16.6 20.6867 17.1667 21.08 17.82 21.36C18.4733 21.64 19.1867 21.78 19.96 21.78C20.84 21.78 21.62 21.6267 22.3 21.32C22.9933 21.0133 23.58 20.5933 24.06 20.06L25.06 21.08C24.4467 21.7333 23.7133 22.2533 22.86 22.64C22.0067 23.0133 21.0533 23.2 20 23.2Z" fill='currentColor' />
|
||||
<path d="M9.3 23V20.58H12.88C13.7867 20.58 14.58 20.3933 15.26 20.02C15.94 19.6333 16.4667 19.0933 16.84 18.4C17.2267 17.7067 17.42 16.8867 17.42 15.94C17.42 15.02 17.2267 14.22 16.84 13.54C16.4533 12.8467 15.92 12.3133 15.24 11.94C14.56 11.5533 13.7733 11.36 12.88 11.36H9.24V8.94H12.92C13.96 8.94 14.92 9.11333 15.8 9.46C16.6933 9.80667 17.4667 10.3 18.12 10.94C18.7867 11.5667 19.3 12.3067 19.66 13.16C20.0333 14.0133 20.22 14.9467 20.22 15.96C20.22 16.9733 20.0333 17.9133 19.66 18.78C19.3 19.6333 18.7933 20.38 18.14 21.02C17.4867 21.6467 16.7133 22.1333 15.82 22.48C14.94 22.8267 13.9867 23 12.96 23H9.3ZM7.44 23V8.94H10.16V23H7.44Z" fill='currentColor' />
|
||||
</svg>
|
||||
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default DashCodeLogo
|
||||
|
|
@ -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 (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
className="w-6 h-6 bg-transparent border border-default-300 hover:bg-transparent ring-offset-transparent hover:ring-0 hover:ring-transparent "
|
||||
>
|
||||
<MoreHorizontal className="w-4 h-4 text-default-600" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-[140px] p-0">
|
||||
<DropdownMenuItem className="py-2 rounded-none border-b border-default-200 text-default-900 focus:bg-default-400 focus:text-default-100 dark:focus:text-default-900">
|
||||
{t("last_28_days", { defaultValue: "Last 28 Days" })}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="py-2 rounded-none border-b border-default-200 text-default-900 focus:bg-default-400 focus:text-default-100 dark:focus:text-default-900">
|
||||
{t("last_months", { defaultValue: "Last Months" })}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="py-2 rounded-none text-default-900 focus:bg-default-400 focus:text-default-100 dark:focus:text-default-900">
|
||||
{t("last_year", { defaultValue: "Last Year" })}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardDropdown;
|
||||
|
|
@ -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<any | null>(null);
|
||||
const { theme: mode } = useTheme();
|
||||
|
||||
return (
|
||||
<div className={cn("grid gap-2", className)}>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
|
||||
className={cn(" font-normal", {
|
||||
" bg-background hover:bg-background hover:ring-background text-default-600": mode !== "dark",
|
||||
})}
|
||||
>
|
||||
<CalendarIcon className="ltr:mr-2 rtl:ml-2 h-4 w-4" />
|
||||
{date?.from ? (
|
||||
date.to ? (
|
||||
<>
|
||||
{format(date.from, "LLL dd, y")} -{" "}
|
||||
{format(date.to, "LLL dd, y")}
|
||||
</>
|
||||
) : (
|
||||
format(date.from, "LLL dd, y")
|
||||
)
|
||||
) : (
|
||||
<span>Pick a date</span>
|
||||
)}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="end">
|
||||
<Calendar
|
||||
initialFocus
|
||||
mode="range"
|
||||
defaultMonth={date?.from}
|
||||
selected={date}
|
||||
onSelect={setDate}
|
||||
numberOfMonths={2}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -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<void>;
|
||||
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 (
|
||||
<AlertDialog open={open}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This action cannot be undone. This will permanently delete your
|
||||
account and remove your data from our servers.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel onClick={onClose}>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
className={isPending ? "pointer-events-none" : ""}
|
||||
onClick={() => startTransition(handleConfirm)}
|
||||
>
|
||||
{isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
{isPending ? "Deleting.." : "Continue"}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeleteConfirmationDialog;
|
||||
|
|
@ -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: () => (
|
||||
<div className="flex items-center justify-center p-8 border border-gray-200 rounded-lg">
|
||||
<div className="text-gray-500">Loading editor...</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
|
||||
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 (
|
||||
<div className={`flex items-center justify-center p-8 border border-gray-200 rounded-lg ${className}`}>
|
||||
<div className="text-gray-500">Loading editor...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (useSimpleEditor) {
|
||||
return (
|
||||
<SimpleEditor
|
||||
data={data}
|
||||
onChange={onChange}
|
||||
className={className}
|
||||
disabled={disabled}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<CKEditorWrapper
|
||||
data={data}
|
||||
onChange={onChange}
|
||||
onReady={onReady}
|
||||
onBlur={onBlur}
|
||||
onFocus={onFocus}
|
||||
config={config}
|
||||
disabled={disabled}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -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<HTMLDivElement>(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 (
|
||||
<div className={`flex items-center justify-center p-8 border border-gray-200 rounded-lg ${className}`}>
|
||||
<div className="text-gray-500">Loading editor...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`border border-gray-200 rounded-lg overflow-hidden ${className}`}>
|
||||
{/* Toolbar */}
|
||||
<div className="flex flex-wrap items-center gap-1 p-2 bg-gray-50 border-b border-gray-200">
|
||||
{toolbarButtons.map((button) => {
|
||||
const IconComponent = button.icon;
|
||||
return (
|
||||
<Button
|
||||
key={button.command}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
if (button.command === 'createLink') {
|
||||
insertLink();
|
||||
} else {
|
||||
execCommand(button.command, button.value);
|
||||
}
|
||||
}}
|
||||
className="h-8 w-8 p-0"
|
||||
disabled={disabled}
|
||||
title={button.label}
|
||||
>
|
||||
<IconComponent className="h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={insertLink}
|
||||
className="h-8 w-8 p-0"
|
||||
disabled={disabled}
|
||||
title="Insert Link"
|
||||
>
|
||||
<Link className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Editor Content */}
|
||||
<div
|
||||
ref={editorRef}
|
||||
contentEditable={!disabled}
|
||||
onInput={handleInput}
|
||||
className="min-h-[200px] p-4 focus:outline-none"
|
||||
style={{ minHeight: '200px' }}
|
||||
dangerouslySetInnerHTML={{ __html: content }}
|
||||
data-placeholder={placeholder}
|
||||
suppressContentEditableWarning={true}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-slate-50 flex items-center justify-center">
|
||||
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-blue-500"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ThemeProvider>
|
||||
<SidebarProvider>
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-slate-50 dark:from-slate-900 dark:via-slate-800 dark:to-slate-900">
|
||||
<div className="flex h-screen overflow-hidden">
|
||||
<RetractingSidebar
|
||||
sidebarData={isOpen}
|
||||
updateSidebarData={updateSidebarData}
|
||||
/>
|
||||
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key="main-content"
|
||||
className="flex-1 flex flex-col overflow-hidden"
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
{/* Header */}
|
||||
<motion.header
|
||||
className="bg-white/80 dark:bg-slate-800/80 backdrop-blur-sm border-b border-slate-200/60 dark:border-slate-700/60 shadow-sm"
|
||||
initial={{ y: -20, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
transition={{ delay: 0.2, duration: 0.3 }}
|
||||
>
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<div className="flex items-center space-x-4">
|
||||
<button
|
||||
className="md:hidden p-2 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors duration-200"
|
||||
onClick={() => updateSidebarData(true)}
|
||||
>
|
||||
<BurgerButtonIcon />
|
||||
</button>
|
||||
<Breadcrumbs />
|
||||
</div>
|
||||
</div>
|
||||
</motion.header>
|
||||
|
||||
{/* Main Content */}
|
||||
<motion.main
|
||||
className="flex-1 overflow-auto"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.3, duration: 0.3 }}
|
||||
>
|
||||
<div className="h-full">{children}</div>
|
||||
</motion.main>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
</SidebarProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
'use client'
|
||||
import React, { createContext, useContext, useEffect, useState } from 'react';
|
||||
|
||||
interface SidebarContextType {
|
||||
isOpen: boolean;
|
||||
toggleSidebar: () => void;
|
||||
}
|
||||
|
||||
const SidebarContext = createContext<SidebarContextType | undefined>(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 (
|
||||
<SidebarContext.Provider value={{ isOpen, toggleSidebar }}>
|
||||
{children}
|
||||
</SidebarContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
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);
|
||||
};
|
||||
|
|
@ -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<ThemeContextType | undefined>(undefined);
|
||||
|
||||
export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const [theme, setThemeState] = useState<Theme>('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 (
|
||||
<ThemeContext.Provider value={{ theme, toggleTheme, setTheme }}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useTheme = () => {
|
||||
const context = useContext(ThemeContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useTheme must be used within a ThemeProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
|
@ -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 : <div className=" h-screen flex items-center justify-center flex-col space-y-2">
|
||||
<div className="flex gap-2 items-center ">
|
||||
{/* <DashCodeLogo className=" text-default-900 h-8 w-8 [&>path:nth-child(3)]:text-background [&>path:nth-child(2)]:text-background" /> */}
|
||||
<Image
|
||||
src="/assets/mediahub-logo-min.png"
|
||||
alt=""
|
||||
width={80}
|
||||
height={80}
|
||||
className="mb-4 w-full h-full"
|
||||
/>
|
||||
{/* <h1 className="text-xl font-semibold text-default-900 ">
|
||||
DashCode
|
||||
</h1> */}
|
||||
</div>
|
||||
<span className=" inline-flex gap-1 items-center">
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Loading...
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Loader;
|
||||
|
||||
|
|
@ -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 (
|
||||
<Link
|
||||
href="/dashboard/analytics"
|
||||
className="flex gap-2 items-center justify-center "
|
||||
>
|
||||
<DashCodeLogo className=" text-default-900 h-8 w-8 [&>path:nth-child(3)]:text-background [&>path:nth-child(2)]:text-background" />
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
if (config.sidebar === "two-column" || !isDesktop) return null;
|
||||
|
||||
return (
|
||||
<Link href="/" className="flex items-center">
|
||||
{/* <DashCodeLogo className=" text-default-900 h-8 w-8 [&>path:nth-child(3)]:text-background [&>path:nth-child(2)]:text-background" />
|
||||
{(!config?.collapsed || hovered) && (
|
||||
<h1 className="text-xl font-semibold text-default-900 ">D</h1>
|
||||
)} */}
|
||||
<img
|
||||
className="w-[100px]"
|
||||
src="/logo-netidhub.png"
|
||||
alt="logo"
|
||||
/>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
export default Logo;
|
||||
|
|
@ -125,18 +125,19 @@ export default function DashboardContainer() {
|
|||
return (
|
||||
<div className="space-y-8">
|
||||
{/* Stats Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-6 gap-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-6 gap-4">
|
||||
{/* User Profile Card */}
|
||||
<motion.div
|
||||
className="col-span-1 md:col-span-2 bg-white rounded-2xl shadow-lg border border-slate-200/60 p-6 hover:shadow-xl transition-all duration-300"
|
||||
className="col-span-1 md:col-span-2 bg-white rounded-2xl shadow-md border border-gray-200 p-4 hover:shadow-lg transition-all duration-300"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.1 }}
|
||||
>
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="space-y-2">
|
||||
<p className="text-slate-600">{username}</p>
|
||||
<div className="flex space-x-6 pt-2"></div>
|
||||
<p className="text-gray-600 font-medium">Welcome back,</p>
|
||||
<p className="text-xl font-semibold text-gray-900">{username}</p>
|
||||
<p className="text-sm text-gray-500">Admin Dashboard</p>
|
||||
</div>
|
||||
<div className="p-3 bg-gradient-to-br from-blue-50 to-purple-50 rounded-xl">
|
||||
<DashboardUserIcon size={60} />
|
||||
|
|
@ -146,7 +147,7 @@ export default function DashboardContainer() {
|
|||
|
||||
{/* Total Posts */}
|
||||
<motion.div
|
||||
className="bg-white rounded-2xl shadow-lg border border-slate-200/60 p-6 hover:shadow-xl transition-all duration-300"
|
||||
className="bg-white rounded-2xl shadow-md border border-gray-200 p-4 hover:shadow-lg transition-all duration-300"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
|
|
@ -156,17 +157,17 @@ export default function DashboardContainer() {
|
|||
<DashboardSpeecIcon />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-3xl font-bold text-slate-800">
|
||||
{summary?.totalAll}
|
||||
<p className="text-3xl font-bold text-gray-900">
|
||||
{summary?.totalAll || 0}
|
||||
</p>
|
||||
<p className="text-sm text-slate-500">Total Posts</p>
|
||||
<p className="text-sm text-gray-500">Total Posts</p>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Total Views */}
|
||||
<motion.div
|
||||
className="bg-white rounded-2xl shadow-lg border border-slate-200/60 p-6 hover:shadow-xl transition-all duration-300"
|
||||
className="bg-white rounded-2xl shadow-md border border-gray-200 p-4 hover:shadow-lg transition-all duration-300"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.3 }}
|
||||
|
|
@ -176,17 +177,17 @@ export default function DashboardContainer() {
|
|||
<DashboardConnectIcon />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-3xl font-bold text-slate-800">
|
||||
{summary?.totalViews}
|
||||
<p className="text-3xl font-bold text-gray-900">
|
||||
{summary?.totalViews || 0}
|
||||
</p>
|
||||
<p className="text-sm text-slate-500">Total Views</p>
|
||||
<p className="text-sm text-gray-500">Total Views</p>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Total Shares */}
|
||||
<motion.div
|
||||
className="bg-white rounded-2xl shadow-lg border border-slate-200/60 p-6 hover:shadow-xl transition-all duration-300"
|
||||
className="bg-white rounded-2xl shadow-md border border-gray-200 p-4 hover:shadow-lg transition-all duration-300"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.4 }}
|
||||
|
|
@ -196,17 +197,17 @@ export default function DashboardContainer() {
|
|||
<DashboardShareIcon />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-3xl font-bold text-slate-800">
|
||||
{summary?.totalShares}
|
||||
<p className="text-3xl font-bold text-gray-900">
|
||||
{summary?.totalShares || 0}
|
||||
</p>
|
||||
<p className="text-sm text-slate-500">Total Shares</p>
|
||||
<p className="text-sm text-gray-500">Total Shares</p>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Total Comments */}
|
||||
<motion.div
|
||||
className="bg-white rounded-2xl shadow-lg border border-slate-200/60 p-6 hover:shadow-xl transition-all duration-300"
|
||||
className="bg-white rounded-2xl shadow-md border border-gray-200 p-4 hover:shadow-lg transition-all duration-300"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.5 }}
|
||||
|
|
@ -216,26 +217,26 @@ export default function DashboardContainer() {
|
|||
<DashboardCommentIcon size={40} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-3xl font-bold text-slate-800">
|
||||
{summary?.totalComments}
|
||||
<p className="text-3xl font-bold text-gray-900">
|
||||
{summary?.totalComments || 0}
|
||||
</p>
|
||||
<p className="text-sm text-slate-500">Total Comments</p>
|
||||
<p className="text-sm text-gray-500">Total Comments</p>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Content Section */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
{/* Analytics Chart */}
|
||||
<motion.div
|
||||
className="bg-white rounded-2xl shadow-lg border border-slate-200/60 p-6"
|
||||
className="bg-white rounded-2xl shadow-md border border-gray-200 p-4"
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: 0.6 }}
|
||||
>
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h3 className="text-lg font-semibold text-slate-800">
|
||||
<h3 className="text-lg font-semibold text-gray-900">
|
||||
Analytics Overview
|
||||
</h3>
|
||||
<div className="flex space-x-4">
|
||||
|
|
@ -250,25 +251,28 @@ export default function DashboardContainer() {
|
|||
handleChange(option.value, checked as boolean)
|
||||
}
|
||||
/>
|
||||
<span className="text-sm text-slate-600">{option.label}</span>
|
||||
<span className="text-sm text-gray-600">{option.label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-64 flex items-center justify-center bg-gray-50 rounded-lg">
|
||||
<p className="text-gray-500">Chart will be displayed here</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Recent Articles */}
|
||||
<motion.div
|
||||
className="bg-white rounded-2xl shadow-lg border border-slate-200/60 p-6"
|
||||
className="bg-white rounded-2xl shadow-md border border-gray-200 p-4"
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: 0.7 }}
|
||||
>
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h3 className="text-lg font-semibold text-slate-800">
|
||||
<h3 className="text-lg font-semibold text-gray-900">
|
||||
Recent Content
|
||||
</h3>
|
||||
<Link href="/admin/article/create">
|
||||
<Link href="/admin/content/image/create">
|
||||
<Button className="bg-gradient-to-r from-blue-500 to-purple-500 hover:from-blue-600 hover:to-purple-600 text-white shadow-lg">
|
||||
Create Content
|
||||
</Button>
|
||||
|
|
@ -279,7 +283,7 @@ export default function DashboardContainer() {
|
|||
{article?.map((list: any) => (
|
||||
<motion.div
|
||||
key={list?.id}
|
||||
className="flex space-x-4 p-4 rounded-xl hover:bg-slate-50 transition-colors duration-200"
|
||||
className="flex space-x-4 p-4 rounded-xl hover:bg-gray-50 transition-colors duration-200"
|
||||
whileHover={{ scale: 1.02 }}
|
||||
transition={{ type: "spring", stiffness: 300 }}
|
||||
>
|
||||
|
|
@ -291,10 +295,10 @@ export default function DashboardContainer() {
|
|||
className="h-20 w-20 object-cover rounded-lg shadow-sm flex-shrink-0"
|
||||
/>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h4 className="font-medium text-slate-800 line-clamp-2 mb-1">
|
||||
<h4 className="font-medium text-gray-900 line-clamp-2 mb-1">
|
||||
{list?.title}
|
||||
</h4>
|
||||
<p className="text-sm text-slate-500">
|
||||
<p className="text-sm text-gray-500">
|
||||
{convertDateFormat(list?.createdAt)}
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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(() => (
|
||||
<GoogleMap
|
||||
google={this.props.google}
|
||||
defaultZoom={this.props.zoom}
|
||||
defaultCenter={{
|
||||
lat: this.state.mapPosition.lat,
|
||||
lng: this.state.mapPosition.lng,
|
||||
}}
|
||||
>
|
||||
<Autocomplete
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "40px",
|
||||
paddingLeft: "16px",
|
||||
marginTop: "2px",
|
||||
marginBottom: "500px",
|
||||
}}
|
||||
onPlaceSelected={this.onPlaceSelected}
|
||||
options={{
|
||||
types: ["geocode"],
|
||||
}}
|
||||
/>
|
||||
{/* InfoWindow on top of marker */}
|
||||
<InfoWindow
|
||||
onClose={this.onInfoWindowClose}
|
||||
position={{
|
||||
lat: this.state.markerPosition.lat + 0.0018,
|
||||
lng: this.state.markerPosition.lng,
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<span
|
||||
style={{
|
||||
padding: 0,
|
||||
margin: 0,
|
||||
}}
|
||||
>
|
||||
{this.state.address}
|
||||
</span>
|
||||
</div>
|
||||
</InfoWindow>
|
||||
{/* Marker */}
|
||||
<Marker
|
||||
google={this.props.google}
|
||||
name="Dolores park"
|
||||
draggable={this.props.draggable}
|
||||
onDragEnd={this.onMarkerDragEnd}
|
||||
position={{
|
||||
lat: this.state.markerPosition.lat,
|
||||
lng: this.state.markerPosition.lng,
|
||||
}}
|
||||
/>
|
||||
<Marker />
|
||||
{/* For Auto complete Search Box */}
|
||||
</GoogleMap>
|
||||
)),
|
||||
);
|
||||
|
||||
let map;
|
||||
|
||||
if (this.props.center.lat == undefined) {
|
||||
map = (
|
||||
<div
|
||||
style={{
|
||||
height: this.props.height,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
map = (
|
||||
<div>
|
||||
{/* <div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="">Address</label>
|
||||
<input type="text" name="address" className="form-control" onChange={ this.onChange } readOnly="readOnly" value={ this.state.address }/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="">All Data</label>
|
||||
<input type="text" name="address" className="form-control" onChange={ this.onChange } readOnly="readOnly" value={ this.state.markerPosition.lat + ";" + this.state.markerPosition.lng }/>
|
||||
</div>
|
||||
</div> */}
|
||||
<AsyncMap
|
||||
googleMapURL={`https://maps.googleapis.com/maps/api/js?key=${GoogleMapsAPI}&libraries=places`}
|
||||
loadingElement={
|
||||
<div
|
||||
style={{
|
||||
height: "100%",
|
||||
}}
|
||||
/>
|
||||
}
|
||||
containerElement={
|
||||
<div
|
||||
style={{
|
||||
height: this.props.height,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
mapElement={
|
||||
<div
|
||||
style={{
|
||||
height: "100%",
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
||||
export default Map;
|
||||
|
|
@ -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 (
|
||||
<GoogleMap
|
||||
zoom={10}
|
||||
center={selected || { lat, lng }}
|
||||
mapContainerStyle={containerStyle}
|
||||
>
|
||||
<Marker
|
||||
draggable={draggable}
|
||||
position={selected || { lat, lng }}
|
||||
onDragEnd={onMarkerDragEnd}
|
||||
/>
|
||||
</GoogleMap>
|
||||
);
|
||||
}
|
||||
|
|
@ -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<MapHomeProps> {
|
||||
render() {
|
||||
const { newLat, newLng, draggable, setLocation } = this.props;
|
||||
|
||||
const lat = newLat || -6.2393033;
|
||||
const lng = newLng || 106.8013579;
|
||||
|
||||
return (
|
||||
<div style={{ marginBottom: "10px", marginTop: "8px" }}>
|
||||
<Places
|
||||
center={{ lat: Number(lat), lng: Number(lng) }}
|
||||
draggable={draggable}
|
||||
onLocationChange={setLocation}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default MapHome;
|
||||
|
|
@ -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 <div>Loading...</div>;
|
||||
|
||||
return (
|
||||
<Map
|
||||
lat={center.lat}
|
||||
lng={center.lng}
|
||||
draggable={draggable}
|
||||
onLocationChange={onLocationChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<>
|
||||
<div>
|
||||
<PlacesAutocomplete setSelected={setSelected} />
|
||||
</div>
|
||||
<GoogleMap
|
||||
zoom={selected == null ? 10 : 15}
|
||||
center={selected == null ? center : selected}
|
||||
mapContainerStyle={containerStyle}
|
||||
>
|
||||
{selected && (
|
||||
<Marker
|
||||
draggable={draggable}
|
||||
position={selected}
|
||||
onDragEnd={onMarkerDragEnd}
|
||||
/>
|
||||
)}
|
||||
</GoogleMap>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<PlacesCombobox
|
||||
value={value}
|
||||
onValueChange={(newValue) => setValue(newValue)}
|
||||
onSelect={handleSelect}
|
||||
suggestions={data}
|
||||
status={status}
|
||||
disabled={!ready}
|
||||
placeholder="Cari Alamat"
|
||||
className="border"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export const GoogleMapsAPI = "AIzaSyCuQHorDceMCzlSgrB9AEY5ns8KeriFsME";
|
||||
|
|
@ -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 (
|
||||
<nav className=' space-y-1 px-5'>
|
||||
{links.map(({ icon, title, active }, index) =>
|
||||
<Button
|
||||
key={`link-${index}`}
|
||||
fullWidth className='capitalize justify-start hover:bg-transparent hover:text-default-600'
|
||||
variant="ghost">
|
||||
<span
|
||||
className={cn(
|
||||
"h-2 w-2 me-2 rounded-full transition-all duration-150 ring-0 bg-default ring-default ring-opacity-30",
|
||||
{
|
||||
"ring-4": active,
|
||||
"bg-destructive ring-destructive": title === 'team' || title==="promotions",
|
||||
"bg-success ring-success": title === 'low' || title==="social",
|
||||
"bg-warning ring-warning": title === 'medium',
|
||||
"bg-primary ring-primary": title === 'high' || title==="buisness",
|
||||
"bg-info ring-info/30": title === 'update',
|
||||
}
|
||||
)}
|
||||
></span>
|
||||
{title}
|
||||
</Button>
|
||||
)}
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<nav className=' space-y-1 px-5'>
|
||||
|
||||
{links.map(({ icon, title, active }, index) =>
|
||||
<Button key={`link-${index}`} color='secondary' fullWidth className='hover:ring-0 hover:ring-transparent justify-start' variant={active ? 'default' : 'ghost'}>
|
||||
<Icon
|
||||
icon={icon}
|
||||
className='me-2 h-5 w-5'
|
||||
/>
|
||||
|
||||
{title}
|
||||
</Button>
|
||||
)}
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
export default Nav
|
||||
|
|
@ -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",
|
||||
});
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue