This commit is contained in:
Sabda Yagra 2025-09-23 23:50:01 +07:00
commit ff6fbe6217
6917 changed files with 64225 additions and 737600 deletions

View File

@ -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>
);
}

View File

@ -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>;
}

View File

@ -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>
);
}

View File

@ -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;

View File

Before

Width:  |  Height:  |  Size: 228 KiB

After

Width:  |  Height:  |  Size: 228 KiB

295
app/[locale]/globals.css Normal file
View File

@ -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;
}
}

92
app/[locale]/layout.tsx Normal file
View File

@ -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>
);
}

21
app/[locale]/page.tsx Normal file
View File

@ -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>
);
}

63
app/[locale]/theme.css Normal file
View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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>
);
}

View File

@ -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 />;
}

View File

@ -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"
}
}

View File

@ -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;
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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 };

View File

@ -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 };

View File

@ -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 };

View File

@ -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 };

View File

@ -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;

View File

@ -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>
)
}

View File

@ -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

View File

@ -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;

View File

@ -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>
);
}

View File

@ -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;

101
components/editor/index.tsx Normal file
View File

@ -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}
/>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
};

View File

@ -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);
};

View File

@ -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;
};

34
components/loader.tsx Normal file
View File

@ -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;

42
components/logo.tsx Normal file
View File

@ -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;

View File

@ -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>

365
components/maps/Map copy.js Normal file
View File

@ -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;

52
components/maps/Map.tsx Normal file
View File

@ -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>
);
}

View File

@ -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;

159
components/maps/Maps.tsx Normal file
View File

@ -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"
/>
);
}

View File

@ -0,0 +1 @@
export const GoogleMapsAPI = "AIzaSyCuQHorDceMCzlSgrB9AEY5ns8KeriFsME";

61
components/nav.tsx Normal file
View File

@ -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

View File

@ -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