feat: update major change template

This commit is contained in:
hanif salafi 2025-09-23 20:07:34 +07:00
parent 4ea8888da9
commit 6b0c866e05
641 changed files with 64434 additions and 5759 deletions

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

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

@ -0,0 +1,91 @@
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: { locale },
}: Readonly<{
children: React.ReactNode;
params: { locale: string };
}>) {
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>
);
}

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 +0,0 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
// const geistSans = Geist({
// variable: "--font-geist-sans",
// subsets: ["latin"],
// });
// const geistMono = Geist_Mono({
// variable: "--font-geist-mono",
// subsets: ["latin"],
// });
export const metadata: Metadata = {
title: "NetidHub",
description: "NetidHub",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}

View File

@ -1,21 +1,17 @@
{ {
"$schema": "https://ui.shadcn.com/schema.json", "$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york", "style": "default",
"rsc": true, "rsc": true,
"tsx": true, "tsx": true,
"tailwind": { "tailwind": {
"config": "", "config": "tailwind.config.ts",
"css": "app/globals.css", "css": "app/globals.css",
"baseColor": "neutral", "baseColor": "slate",
"cssVariables": true, "cssVariables": true,
"prefix": "" "prefix": ""
}, },
"aliases": { "aliases": {
"components": "@/components", "components": "@/components",
"utils": "@/lib/utils", "utils": "@/lib/utils"
"ui": "@/components/ui", }
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
} }

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;

View File

@ -0,0 +1,45 @@
'use client';
import dynamic from 'next/dynamic';
import { CKEditor } from '@ckeditor/ckeditor5-react';
import { ClassicEditor } from 'ckeditor5';
// Dynamically import CKEditor to avoid SSR issues
const DynamicCKEditor = dynamic(() => Promise.resolve(CKEditor), {
ssr: false,
loading: () => <div>Loading editor...</div>
});
interface EditorProps {
data?: string;
onChange?: (data: string) => void;
config?: any;
disabled?: boolean;
}
export default function Editor({ data = '', onChange, config = {}, disabled = false }: EditorProps) {
return (
<div className="ckeditor-wrapper">
<DynamicCKEditor
editor={ClassicEditor}
data={data}
onChange={(event, editor) => {
const data = editor.getData();
onChange?.(data);
}}
config={{
toolbar: [
'heading', '|',
'bold', 'italic', 'link', '|',
'bulletedList', 'numberedList', '|',
'outdent', 'indent', '|',
'blockQuote', 'insertTable', '|',
'undo', 'redo'
],
...config
}}
disabled={disabled}
/>
</div>
);
}

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;

43
components/logo.tsx Normal file
View File

@ -0,0 +1,43 @@
"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-100"
src="/../images/all-img/mediahub-logo.png"
alt="logo"
width={150}
/>
</Link>
);
};
export default Logo;

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 { createSharedPathnamesNavigation } from 'next-intl/navigation';
// import {locales} from '@/config'; import { locales } from '@/config';
// export const {Link, redirect, usePathname, useRouter} = export const { Link, redirect, usePathname, useRouter } =
// createSharedPathnamesNavigation({locales,}); createSharedPathnamesNavigation({
locales: locales,
defaultLocale: "in",
});

269
components/page-title.tsx Normal file
View File

@ -0,0 +1,269 @@
"use client";
import React, { useEffect, useState } from "react";
import DateRangePicker from "@/components/date-range-picker";
import { usePathname } from "@/components/navigation";
import { cn, getCookiesDecrypt } from "@/lib/utils";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "./ui/dialog";
import { Button } from "./ui/button";
import { Label } from "./ui/label";
import { Input } from "./ui/input";
import pdfGenerator from "@/utils/pdf-generator";
import {
tableauSignin,
tableauViewImage,
} from "@/service/tableau/tableau-service";
const PageTitle = ({
title,
className,
}: {
title?: string;
className?: string;
}) => {
const [reportDate, setReportDate] = useState<any>();
const pathname = usePathname();
const name = pathname?.split("/").slice(1).join(" ");
const roleId = getCookiesDecrypt("urie");
useEffect(() => {
console.log("role", roleId);
}, [roleId]);
const downloadReport = async () => {
console.log(reportDate);
const formattedDate = `${reportDate.year}-${String(
reportDate.month
).padStart(2, "0")}-${String(reportDate.day).padStart(2, "0")}`;
const resLogin = await tableauSignin();
const token = resLogin?.data?.data?.credentials?.token;
const resFrontCover = await tableauViewImage(
token,
"5b35ec9c-add5-45e7-b71a-31a28db9954d",
reportDate
);
const resAssignmentDetail = await tableauViewImage(
token,
"b2e7e184-8311-49e1-8314-a6b2d8a8c6f8",
reportDate
);
// const resMonitoringContent = await tableauViewImage(
// token,
// "34683f52-1029-49e4-950d-2c94159ebd8f",
// reportDate
// );
// const resMonitoringArticle = await tableauViewImage(
// token,
// "34683f52-1029-49e4-950d-2c94159ebd8f",
// reportDate
// );
const resInteractionContent = await tableauViewImage(
token,
"8f038669-4135-4693-96e5-e6a957a1cc4f",
reportDate
);
const resTotalInteractionContent = await tableauViewImage(
token,
"e019e7f7-8d3b-4303-aa20-3fdbb318df1e",
reportDate
);
const resInteractionContentCategory1 = await tableauViewImage(
token,
"c1dcc6c8-17dc-4e30-85db-7938e30b4418",
reportDate
);
const resInteractionContentCategory2 = await tableauViewImage(
token,
"68b95c1d-aee1-4073-afb7-05188eccee67",
reportDate
);
const resInteractionContentCategory3 = await tableauViewImage(
token,
"7de1f07f-9a09-45a8-8ff6-96484106492d",
reportDate
);
const resInteractionDistribution = await tableauViewImage(
token,
"0781d7e6-e133-416c-bf41-e5b2fa5996dd",
reportDate
);
const resAssignments = await tableauViewImage(
token,
"b2e7e184-8311-49e1-8314-a6b2d8a8c6f8",
reportDate
);
const resUserCount = await tableauViewImage(
token,
"36ff675d-790c-4420-b0c7-0a6441f59cb1",
reportDate
);
const resUserCountDistribution = await tableauViewImage(
token,
"86370e20-b13d-4cb0-a54a-c25dc13bb427",
reportDate
);
const resBackCover = await tableauViewImage(
token,
"516c8790-ccd5-44dc-bb66-b0e146d7168b",
reportDate
);
const blobFrontCover = new Blob([resFrontCover.data], {
type: "image/png",
});
const blobAssignmentDetail = new Blob([resAssignmentDetail.data], {
type: "image/png",
});
// const blobMonitoringContent = new Blob([resMonitoringContent.data], { type: "image/png" });
// const blobMonitoringArticle = new Blob([resMonitoringArticle.data], { type: "image/png" });
const blobInteractionContent = new Blob([resInteractionContent.data], {
type: "image/png",
});
const blobTotalInteractionContent = new Blob(
[resTotalInteractionContent.data],
{ type: "image/png" }
);
const blobInteractionContentCategory1 = new Blob(
[resInteractionContentCategory1.data],
{ type: "image/png" }
);
const blobInteractionContentCategory2 = new Blob(
[resInteractionContentCategory2.data],
{ type: "image/png" }
);
const blobInteractionContentCategory3 = new Blob(
[resInteractionContentCategory3.data],
{ type: "image/png" }
);
const blobInteractionDistribution = new Blob(
[resInteractionDistribution.data],
{ type: "image/png" }
);
const blobAssignments = new Blob([resAssignments.data], {
type: "image/png",
});
const blobUserCount = new Blob([resUserCount.data], { type: "image/png" });
const blobUserCountDistribution = new Blob(
[resUserCountDistribution.data],
{ type: "image/png" }
);
const blobBackCover = new Blob([resBackCover.data], { type: "image/png" });
console.log(blobFrontCover);
await pdfGenerator([
blobFrontCover,
blobAssignmentDetail,
// blobMonitoringContent,
// blobMonitoringArticle,
blobInteractionContent,
blobTotalInteractionContent,
blobInteractionContentCategory1,
blobInteractionContentCategory2,
blobInteractionContentCategory3,
blobInteractionDistribution,
blobAssignments,
blobUserCount,
blobUserCountDistribution,
blobBackCover,
]);
};
async function mergeImagesToCanvas(blobs: Blob[]) {
try {
const images = await Promise.all(
blobs.map((blob) => {
return new Promise<HTMLImageElement>((resolve) => {
const url = URL.createObjectURL(blob);
const img = new Image();
img.onload = () => {
resolve(img);
URL.revokeObjectURL(url);
};
img.src = url;
});
})
);
// Hitung total tinggi dari semua gambar (stacked vertically)
const width = Math.max(...images.map((img) => img.width));
const height = images.reduce((sum, img) => sum + img.height, 0);
const canvas = document.getElementById("pdf-canvas") as HTMLCanvasElement;
const ctx = canvas.getContext("2d")!;
canvas.width = width;
canvas.height = height;
let y = 0;
for (const img of images) {
ctx.drawImage(img, 0, y, img.width, img.height);
y += img.height;
}
// Simpan hasil sebagai gambar (PNG) atau bisa print
const dataURL = canvas.toDataURL("image/png");
// Simpan atau cetak (print sebagai PDF)
const link = document.createElement("a");
link.href = dataURL;
link.download = "merged-image.png";
link.click();
// Atau tampilkan dan user bisa "Print to PDF"
window.open(dataURL, "_blank");
} catch (error) {
console.log("Error :::", error);
}
}
return Number(roleId) == 2 || Number(roleId) == 11 || Number(roleId) == 12 ? (
""
) : (
<div
className={cn(
"flex flex-wrap gap-4 items-center justify-between",
className
)}
>
<div className="text-2xl font-medium text-default-800 capitalize">
Dashboard
</div>
{/* <Dialog>
<DialogTrigger asChild>
<Button variant="outline">Download Report</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Download Report</DialogTitle>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="w-full">
<canvas id="pdf-canvas"></canvas>
<Label>Date</Label>
<Input
type="date"
value={reportDate}
onChange={(e) => setReportDate(e.target.value)}
className="w-full"
/>
</div>
</div>
<DialogFooter>
<Button type="submit" onClick={downloadReport}>
Download
</Button>
</DialogFooter>
</DialogContent>
</Dialog> */}
</div>
);
};
export default PageTitle;

View File

@ -0,0 +1,8 @@
const Copyright = () => {
const currentYear = new Date().getFullYear();
return <>Copyright {currentYear}, Dashcode All Rights Reserved.</>;
};
export default Copyright;

View File

@ -0,0 +1,74 @@
"use client";
import React, { useState } from "react";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
type Inputs = {
example: string;
exampleRequired: string;
};
import { useForm, SubmitHandler } from "react-hook-form";
import { error, loading } from "@/config/swal";
import withReactContent from "sweetalert2-react-content";
import Swal from "sweetalert2";
import { useRouter } from "@/i18n/routing";
import { forgotPassword } from "@/service/landing/landing";
const ForgotPass = () => {
const [username, setUsername] = useState<any>();
const MySwal = withReactContent(Swal);
const router = useRouter();
const {
register,
handleSubmit,
watch,
formState: { errors },
} = useForm<Inputs>();
const onSubmit: SubmitHandler<Inputs> = (data) => console.log(data);
async function handleCheckUsername() {
loading();
const response = await forgotPassword(username);
if (response.error) {
error(response.message);
return false;
}
successSubmit();
return false;
}
function successSubmit() {
MySwal.fire({
title: "Email berhasil dikirim. Silahkan cek email Anda.",
icon: "success",
confirmButtonColor: "#3085d6",
confirmButtonText: "OK",
}).then((result: any) => {
if (result.isConfirmed) {
router.push("/admin");
}
});
}
return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4 ">
<div className="space-y-2">
<Label htmlFor="user-name">Username</Label>
<Input id="user-name" defaultValue="akun1234" className="h-[48px] text-sm text-default-900 " onChange={(e) => setUsername(e.target.value)} />
</div>
<Button type="submit" fullWidth onClick={handleCheckUsername}>
Check Username
</Button>
<Button type="submit" fullWidth onClick={handleCheckUsername}>
Kirim Ulang?{" "}
</Button>
</form>
);
};
export default ForgotPass;

View File

@ -0,0 +1,40 @@
"use client";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useForm, SubmitHandler } from "react-hook-form";
type Inputs = {
example: string;
exampleRequired: string;
};
const LockScreen = () => {
const {
register,
handleSubmit,
watch,
formState: { errors },
} = useForm<Inputs>();
const onSubmit: SubmitHandler<Inputs> = (data) => console.log(data);
console.log(watch("example"));
return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4 ">
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
defaultValue="dashcode@gmail.com"
{...register("example")}
className="h-[48px] text-sm text-default-900 "
/>
</div>
<Button type="submit" fullWidth>
Unlock
</Button>
</form>
);
};
export default LockScreen;

View File

@ -0,0 +1,652 @@
"use client";
import React, { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import Cookies from "js-cookie";
import { Icon } from "@/components/ui/icon";
import { useForm, SubmitHandler } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { cn, getCookiesDecrypt, setCookiesEncrypt } from "@/lib/utils";
import { Eye, EyeOff, Loader2 } from "lucide-react";
import {
getProfile,
login,
postEmailValidation,
postSetupEmail,
requestOTP,
verifyOTPByUsername,
} from "@/service/auth";
import { toast } from "sonner";
import { useRouter } from "@/components/navigation";
import { warning } from "@/lib/swal";
import { Link } from "@/i18n/routing";
import { useTranslations } from "next-intl";
import {
InputOTP,
InputOTPGroup,
InputOTPSeparator,
InputOTPSlot,
} from "@/components/ui/input-otp";
import { error, loading } from "@/config/swal";
import { data } from "jquery";
import {
Dialog,
DialogContent,
DialogFooter,
DialogTrigger,
} from "@/components/ui/dialog";
import { getUserNotifications, listRole } from "@/service/landing/landing";
// Schema validasi menggunakan zod
const schema = z.object({
username: z.string().min(1, { message: "Judul diperlukan" }),
password: z
.string()
.min(4, { message: "Password must be at least 4 characters." }),
});
// Tipe untuk form values
type LoginFormValues = {
username: string;
password: string;
};
const LoginForm = () => {
const [isPending, startTransition] = React.useTransition();
const router = useRouter();
const [passwordType, setPasswordType] = React.useState("password");
const t = useTranslations("LandingPage");
const [step, setStep] = useState<number>(1);
const [otpValue, setOtpValue] = useState("");
const [userIdentity] = useState();
const [email, setEmail] = useState();
const [category, setCategory] = useState("5");
const roleId = getCookiesDecrypt("urie");
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [oldEmail, setOldEmail] = useState("");
const [oldEmailValidate, setOldEmailValidate] = useState("");
const [role, setRole] = useState<any>();
const [menuActive, setMenuActive] = useState<string>();
const [notifications, setNotifications] = useState([]);
const [notificationsUpdate, setNotificationsUpdate] = useState([]);
const [newEmail, setNewEmail] = useState("");
const [newEmailValidate, setNewEmailValidate] = useState("");
const [otpValidate, setOtpValidate] = useState("");
const [showPassword, setShowPassword] = useState(false);
const togglePasswordType = () => {
setPasswordType((prevType) =>
prevType === "password" ? "text" : "password"
);
};
const {
register,
handleSubmit,
getValues,
formState: { errors },
} = useForm<LoginFormValues>({
resolver: zodResolver(schema),
mode: "all",
});
const handleTypeOTP = (event: React.KeyboardEvent<HTMLInputElement>) => {
const { key } = event;
const target = event.currentTarget;
if (key === "Enter") {
event.preventDefault();
const inputs = Array.from(target.form?.querySelectorAll("input") || []);
const currentIndex = inputs.indexOf(target);
const nextInput = inputs[currentIndex + 1] as HTMLElement | undefined;
if (nextInput) {
nextInput.focus();
}
}
};
const onSubmit: SubmitHandler<LoginFormValues> = async (data) => {
try {
const response = await login({
...data,
grantType: "password",
clientId: "mediahub-app",
});
console.log("LOGIN: ", response);
if (response?.error) {
toast.error("Username / Password Tidak Sesuai");
} else {
const { access_token } = response?.data;
const { refresh_token } = response?.data;
const dateTime = new Date();
const newTime = dateTime.getTime() + 10 * 60 * 1000;
Cookies.set("access_token", access_token, {
expires: 1,
});
Cookies.set("refresh_token", refresh_token, {
expires: 1,
});
Cookies.set("time_refresh", new Date(newTime).toISOString(), {
expires: 1,
});
Cookies.set("is_first_login", String(true), {
secure: true,
sameSite: "strict",
});
const profile = await getProfile(access_token);
console.log("PROFILE : ", profile?.data?.data);
Cookies.set("home_path", profile?.data?.data?.homePath, {
expires: 1,
});
Cookies.set(
"profile_picture",
profile?.data?.data?.profilePictureUrl,
{
expires: 1,
}
);
Cookies.set("state", profile?.data?.data?.userLevel?.name, {
expires: 1,
});
Cookies.set(
"state-prov",
profile.data?.data?.userLevel?.province?.provName,
{
expires: 1,
}
);
setCookiesEncrypt("uie", profile?.data?.data?.id, {
expires: 1,
});
setCookiesEncrypt("urie", profile?.data?.data?.roleId, {
expires: 1,
});
setCookiesEncrypt("urne", profile?.data?.data?.role?.name, {
expires: 1,
});
setCookiesEncrypt("ulie", profile?.data?.data?.userLevel?.id, {
expires: 1,
});
setCookiesEncrypt(
"uplie",
profile?.data?.data?.userLevel?.parentLevelId,
{
expires: 1,
}
);
setCookiesEncrypt(
"ulne",
profile?.data?.data?.userLevel?.levelNumber,
{
expires: 1,
}
);
setCookiesEncrypt("ufne", profile?.data?.data?.fullname, {
expires: 1,
});
setCookiesEncrypt("ulnae", profile?.data?.data?.userLevel?.name, {
expires: 1,
});
setCookiesEncrypt("uinse", profile?.data?.data?.instituteId, {
expires: 1,
});
console.log("ssaddd", profile?.data?.data?.roleId);
if (
Number(profile?.data?.data?.roleId) == 2 ||
Number(profile?.data?.data?.roleId) == 3 ||
Number(profile?.data?.data?.roleId) == 4 ||
Number(profile?.data?.data?.roleId) == 9 ||
Number(profile?.data?.data?.roleId) == 10 ||
Number(profile?.data?.data?.roleId) == 11 ||
Number(profile?.data?.data?.roleId) == 12 ||
Number(profile?.data?.data?.roleId) == 18 ||
Number(profile?.data?.data?.roleId) == 19
) {
if (profile?.data?.data?.roleId === 18) {
window.location.href = "/in/dashboard/executive-data";
// router.push('/admin/dashboard');
Cookies.set("status", "login", {
expires: 1,
});
} else if (profile?.data?.data?.roleId === 2) {
window.location.href = "/in/dashboard/executive";
Cookies.set("status", "login", {
expires: 1,
});
} else if (
profile?.data?.data?.userLevel?.id == 794 ||
profile?.data?.data?.userLevel?.parentLevelId == 761
) {
window.location.href = "/in/dashboard";
Cookies.set("status", "login", {
expires: 1,
});
} else {
window.location.href = "/in/dashboard";
// router.push('/admin/dashboard');
Cookies.set("status", "login", {
expires: 1,
});
}
} else {
window.location.href = "/";
Cookies.set("status", "login", {
expires: 1,
});
}
}
} catch (err: any) {
toast.error(err.message || "An unexpected error occurred.");
}
// startTransition( () => {
// });
};
const handleEmailValidation = async () => {
const data = getValues();
// loading();
const response = await postEmailValidation(data);
if (response?.error) {
error(response?.message);
return false;
}
const msg = response?.data?.message;
if (msg == "Continue to setup email") {
setStep(2);
} else if (msg == "Email is valid and OTP has been sent") {
setStep(3);
} else if (msg == "Username & password valid") {
onSubmit(data);
} else {
setStep(1);
}
};
const handleSetupEmail = async () => {
const values = getValues();
const data = {
username: values.username,
password: values.password,
oldEmail: oldEmail,
newEmail: newEmail,
};
// loading();
const response = await postSetupEmail(data);
// close();
if (response?.error) {
error(response.message);
return false;
}
const msg = response?.data?.message;
if (msg == "Email is valid and OTP has been sent") {
setStep(3);
} else if (msg == "The old email is not same") {
error("Email is invalid");
}
};
const checkEmail = (state: any, e: any) => {
const regEmail =
/^(([^\s"(),.:;<>@[\\\]]+(\.[^\s"(),.:;<>@[\\\]]+)*)|(".+"))@((\[(?:\d{1,3}\.){3}\d{1,3}])|(([\dA-Za-z\-]+\.)+[A-Za-z]{2,}))$/;
if (regEmail.test(e)) {
if (state == "old") {
setOldEmailValidate("");
setOldEmail(e);
} else {
setNewEmailValidate("");
setNewEmail(e);
}
} else {
if (state == "old") {
setOldEmailValidate("Email tidak valid");
setOldEmail("");
} else {
setNewEmailValidate("Email tidak valid");
setNewEmail("");
}
}
};
const handleLoginOTP = async () => {
// const otp = `${otp1}${otp2}${otp3}${otp4}${otp5}${otp6}`;
const values = getValues();
if (otpValue.length === 6) {
loading();
const response = await verifyOTPByUsername(values.username, otpValue);
if (response?.error) {
error(response.message);
return false;
}
close();
if (response?.message === "success") {
onSubmit(values);
} else {
setOtpValidate("Kode OTP Tidak Valid");
}
}
};
let menu = "";
useEffect(() => {
async function initState() {
setMenuActive(menu);
const res = await listRole();
setRole(res?.data?.data);
}
async function getNotif() {
if (roleId != undefined) {
const response = await getUserNotifications(0, 2);
setNotifications(response?.data?.data?.content);
console.log("respon:", response);
}
}
async function getNotifUpdate() {
if (roleId != undefined) {
const response = await getUserNotifications(0, 3);
setNotificationsUpdate(response?.data?.data?.content);
console.log("Notiffff:", response);
}
}
initState();
getNotif();
getNotifUpdate();
}, []);
return (
<>
{step !== 3 && (
<form
onSubmit={handleSubmit(onSubmit)}
className="mt-5 2xl:mt-7 space-y-4"
>
{step === 1 && (
<>
<div className="text-left 2xl:mb-10 mb-4 mt-10">
<h4 className="font-semibold text-3xl text-left">
{t("logInPlease", { defaultValue: "Log In Please" })}
</h4>
<div className="text-default-500 text-base">
{t("acc", { defaultValue: "Acc" })}
<Dialog>
<DialogTrigger asChild>
<span className="w-full lg:w-fit px-2 h-8 text-red-500 hover:cursor-pointer">
{t("register", { defaultValue: "Register" })}
</span>
</DialogTrigger>
<DialogContent size="sm" className="sm:max-w-[425px]">
<div className="flex flex-col w-full gap-1">
<p className="text-lg font-semibold text-center">
{t("categoryReg", { defaultValue: "Category Reg" })}
</p>
<p className="text-base text-center">
{t("selectOne", { defaultValue: "Select One" })}
</p>
</div>
<div>
{role?.map((row: any) => (
<div key={row.id}>
<input
type="radio"
id={`category${row.id}`}
name="category"
className=""
value={row.id}
checked={category == `${row.id}`}
onChange={(event) =>
setCategory(event.target.value)
}
/>
<label
className="ml-2"
htmlFor={`category${row.id}`}
>
{row.name}
</label>
</div>
))}
</div>
<div className="border-b-2 border-black"></div>
<DialogFooter>
<Link
href={`/auth/registration?category=${category}`}
className="flex justify-center bg-red-500 px-4 py-1 rounded-md border border-black text-white"
type="submit"
>
{t("next", { defaultValue: "Next" })}{" "}
</Link>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</div>
<div className="space-y-2">
<Label
htmlFor="username"
className="font-medium text-default-600"
>
Username
</Label>
<Input
size="lg"
disabled={isPending}
{...register("username")}
id="username"
type="text"
className={cn("", {
"border-destructive": errors.username,
})}
/>
{errors.username?.message && (
<div className="text-destructive mt-2 text-sm">
{errors.username.message}
</div>
)}
</div>
<div className="mt-3.5 space-y-2">
<Label
htmlFor="password"
className="font-medium text-default-600"
>
{t("password", { defaultValue: "Password" })}
</Label>
<div className="relative">
<Input
size="lg"
disabled={isPending}
{...register("password")}
id="password"
type={showPassword ? "text" : "password"}
className="peer pr-10"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-default-500 hover:text-default-700"
tabIndex={-1}
>
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
</button>
</div>
{errors.password?.message && (
<div className="text-destructive mt-2 text-sm">
{errors.password.message}
</div>
)}
</div>
<div className="flex justify-between">
<div className="flex gap-2 items-center">
<Checkbox id="checkbox" defaultChecked />
<Label htmlFor="checkbox">{t("rememberMe", { defaultValue: "Remember Me" })}</Label>
</div>
<Link
href="/auth/forgot-password"
className="text-sm text-default-800 dark:text-default-400 leading-6 font-medium"
>
{t("forgotPass", { defaultValue: "Forgot Pass" })}
</Link>
</div>
<Button
type="button"
fullWidth
onClick={handleEmailValidation}
disabled={isPending}
>
Selanjutnya
</Button>
</>
)}
{step === 2 && (
<>
<div className="text-left 2xl:mb-10 mb-4">
<h4 className="font-semibold text-3xl text-left">
Anda perlu memasukkan email baru untuk bisa Login.
</h4>
</div>
<div className="flex flex-col justify-center mb-6">
<div className="space-y-2">
<Label
htmlFor="username"
className="font-medium text-default-600"
>
Email Lama <span className="text-red-500">*</span>
</Label>
<Input
size="lg"
disabled={isPending}
onChange={(e) => checkEmail("old", e.target.value)}
id="oldEmail"
type="email"
className={cn("", {
"border-destructive": errors.username,
})}
/>
<p className="invalid-feedback-custom">{oldEmailValidate}</p>
{errors.username?.message && (
<div className="text-destructive mt-2 text-sm">
{errors.username.message}
</div>
)}
</div>
<div className="space-y-2">
<Label
htmlFor="username"
className="font-medium text-default-600"
>
Email Baru <span className="text-red-500">*</span>
</Label>
<Input
size="lg"
disabled={isPending}
onChange={(e) => checkEmail("new", e.target.value)}
id="newEmail"
type="email"
className={cn("", {
"border-destructive": errors.username,
})}
/>
<p className="invalid-feedback-custom">{newEmailValidate}</p>
{errors.username?.message && (
<div className="text-destructive mt-2 text-sm">
{errors.username.message}
</div>
)}
</div>
</div>
<Button
fullWidth
className="bg-red-500"
onClick={handleSetupEmail}
type="button"
>
Simpan
</Button>
</>
)}
</form>
)}
{step === 3 && (
<div className="mt-10 space-y-6">
<div className="text-left 2xl:mb-10 mb-4 mt-10">
<h4 className="font-semibold text-3xl text-left">
{t("pleaseEnterOtp", { defaultValue: "Please Enter Otp" })}
</h4>
</div>
<div className="flex justify-center mb-6">
<InputOTP
maxLength={6}
onChange={(val: string) => setOtpValue(val)}
>
<InputOTPGroup>
<InputOTPSlot index={0} onKeyDown={handleTypeOTP} />
<InputOTPSlot index={1} onKeyDown={handleTypeOTP} />
</InputOTPGroup>
<InputOTPSeparator />
<InputOTPGroup>
<InputOTPSlot index={2} onKeyDown={handleTypeOTP} />
<InputOTPSlot index={3} onKeyDown={handleTypeOTP} />
</InputOTPGroup>
<InputOTPSeparator />
<InputOTPGroup>
<InputOTPSlot index={4} onKeyDown={handleTypeOTP} />
<InputOTPSlot index={5} onKeyDown={handleTypeOTP} />
</InputOTPGroup>
</InputOTP>
</div>
{otpValidate && (
<p className="invalid-feedback-custom text-center">
<b>{otpValidate}</b>
</p>
)}
<Button
fullWidth
className="bg-red-500"
type="button"
onClick={handleLoginOTP}
disabled={otpValue.length !== 6}
>
{t("signIn", { defaultValue: "Sign In" })}
</Button>
</div>
)}
</>
);
};
export default LoginForm;

View File

@ -0,0 +1,24 @@
'use client'
import Image from 'next/image';
import { useTheme } from "next-themes";
const Logo = () => {
const { theme: mode } = useTheme();
return (
<div>
<Image
src={
mode === "light"
? "/images/logo/logo.svg"
: "/images/logo/logo-white.svg"
}
alt=""
width={300}
height={300}
className=" w-36 "
/>
</div>
);
}
export default Logo;

View File

@ -0,0 +1,72 @@
"use client";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useForm, SubmitHandler } from "react-hook-form";
type Inputs = {
example: string;
exampleRequired: string;
};
const RegForm = () => {
const {
register,
handleSubmit,
watch,
formState: { errors },
} = useForm<Inputs>();
const onSubmit: SubmitHandler<Inputs> = (data) => console.log(data);
console.log(watch("example"));
return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4 ">
<div className="space-y-2">
<Label htmlFor="name">Name</Label>
<Input
id="name"
placeholder="John Doe"
{...register("example")}
size="lg"
/>
</div>
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
placeholder="dashcode@gmail.com"
{...register("example")}
size="lg"
/>
</div>
<div className="space-y-2">
<Label htmlFor="password">Password</Label>
<Input
id="password"
type="password"
placeholder="dashcode"
{...register("exampleRequired", { required: true })}
size="lg"
/>
</div>
<div className="flex justify-between">
<div className="flex gap-2 items-center">
<Checkbox
id="checkbox"
defaultChecked
/>
<Label htmlFor="checkbox">
You Accept Our Terms And Conditions And Privacy Policy
</Label>
</div>
</div>
<Button type="submit" fullWidth>
Create An Account
</Button>
</form>
);
};
export default RegForm;

View File

@ -0,0 +1,75 @@
import Image from "next/image";
const Social = ({ locale }: { locale: string }) => {
return (
<>
<ul className="flex">
<li className="flex-1">
<a
href="#"
className="inline-flex h-10 w-10 p-2 bg-[#1C9CEB] text-white text-2xl flex-col items-center justify-center rounded-full"
>
<Image
width={300}
height={300}
className="w-full h-full"
src="/images/icon/tw.svg"
alt=""
/>
</a>
</li>
<li className="flex-1">
<a
href="#"
className="inline-flex h-10 w-10 p-2 bg-[#395599] text-white text-2xl flex-col items-center justify-center rounded-full"
>
<Image
width={300}
height={300}
className="w-full h-full"
src="/images/icon/fb.svg"
alt=""
/>
</a>
</li>
<li className="flex-1">
<a
href="#"
className="inline-flex h-10 w-10 p-2 bg-[#0A63BC] text-white text-2xl flex-col items-center justify-center rounded-full"
>
<Image
width={300}
height={300}
className="w-full h-full"
src="/images/icon/in.svg"
alt=""
/>
</a>
</li>
<li className="flex-1">
<form
// action={async () => {
// "use server";
// await signIn("google", { redirectTo: `/${locale}/dashboard/analytics` });
// }}
>
<button
type="submit"
className="inline-flex h-10 w-10 p-2 bg-[#EA4335] text-white text-2xl flex-col items-center justify-center rounded-full"
>
<Image
width={300}
height={300}
className="w-full h-full"
src="/images/icon/gp.svg"
alt=""
/>
</button>
</form>
</li>
</ul>
</>
);
};
export default Social;

View File

@ -0,0 +1,28 @@
'use client'
import { useTheme } from 'next-themes';
import React from 'react'
import { Button } from '@/components/ui/button';
import Link from 'next/link';
import { useMediaQuery } from '@/hooks/use-media-query';
const BuyButton = () => {
const { theme: mode } = useTheme();
const isMobile = useMediaQuery("(min-width: 768px)");
return (
<>
<Button asChild size={!isMobile ? "sm" : "md"} fullWidth color={mode !== 'light' ? 'secondary' : 'default'}>
<Link href="https://1.envato.market/vNaJR3">Buy Now</Link>
</Button>
<Button asChild size={!isMobile ? "sm" : "md"} fullWidth color={mode !== 'light' ? 'secondary' : 'default'}>
<Link
href="https://themeforest.net/user/codeshaperbd/portfolio"
target="__blank"
>
Our Portfolio
</Link>
</Button>
</>
)
}
export default BuyButton

Some files were not shown because too many files have changed in this diff Show More