commit 1cdb0cd0865b70b24955d5b45e56790586dff3a4 Author: Anang Yusman Date: Tue Feb 17 17:05:22 2026 +0800 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5ef6a52 --- /dev/null +++ b/.gitignore @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/README.md b/README.md new file mode 100644 index 0000000..e215bc4 --- /dev/null +++ b/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/app/(admin)/admin/dashboard/page.tsx b/app/(admin)/admin/dashboard/page.tsx new file mode 100644 index 0000000..92cce1c --- /dev/null +++ b/app/(admin)/admin/dashboard/page.tsx @@ -0,0 +1,34 @@ +"use client"; + +import DashboardContainer from "@/components/main/dashboard/dashboard-container"; +import { motion } from "framer-motion"; +import { useEffect, useState } from "react"; + +export default function AdminPage() { + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + }, []); + + if (!mounted) { + return ( +
+
+
+ ); + } + + return ( + +
+ +
+
+ ); +} diff --git a/app/(admin)/admin/layout.tsx b/app/(admin)/admin/layout.tsx new file mode 100644 index 0000000..4c1eb03 --- /dev/null +++ b/app/(admin)/admin/layout.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { AdminLayout } from "@/components/layout/admin-layout"; + +export default function AdminPageLayout({ children }: { children: React.ReactNode }) { + return {children}; +} diff --git a/app/audio/filter/page.tsx b/app/audio/filter/page.tsx new file mode 100644 index 0000000..1be7bc6 --- /dev/null +++ b/app/audio/filter/page.tsx @@ -0,0 +1,85 @@ +"use client"; + +import AudioCard from "@/components/audio/audio-card"; +import FilterAudioSidebar from "@/components/audio/filter-sidebar"; +import FloatingMenuNews from "@/components/landing-page/floating-news"; +import Footer from "@/components/landing-page/footer"; +import FilterSidebar from "@/components/video/filter-sidebar"; +import VideoCard from "@/components/video/video-card"; +import { Menu } from "lucide-react"; +import { useState } from "react"; + +export default function AudioFilterPage() { + const [openFilter, setOpenFilter] = useState(false); + + return ( +
+
+
+ {/* ===== TOP BAR ===== */} + + {/* ===== CONTENT ===== */} +
+ {/* Sidebar */} +
+ +
+ + {/* Mobile Sidebar */} + {openFilter && ( +
+
+ + +
+
+ )} + + {/* Cards */} +
+
+
+ Audio   >   + Lihat Semua + {"|"} + + Terdapat 1636 berita + +
+ +
+ Urutkan: + + +
+
+
+ {Array.from({ length: 6 }).map((_, i) => ( + + ))} +
+
+
+ + {/* ===== PAGINATION ===== */} +
+ + + ... + + +
+
+
+
+
+ ); +} diff --git a/app/auth/layout.tsx b/app/auth/layout.tsx new file mode 100644 index 0000000..c9e99f9 --- /dev/null +++ b/app/auth/layout.tsx @@ -0,0 +1,7 @@ +export default function AuthLayout({ + children, +}: { + children: React.ReactNode; +}) { + return <> {children}; +} diff --git a/app/auth/page.tsx b/app/auth/page.tsx new file mode 100644 index 0000000..64d9e6e --- /dev/null +++ b/app/auth/page.tsx @@ -0,0 +1,9 @@ +import Login from "@/components/form/login"; + +export default function AuthPage() { + return ( + <> + + + ); +} diff --git a/app/details/[slug]/page.tsx b/app/details/[slug]/page.tsx new file mode 100644 index 0000000..b198ce7 --- /dev/null +++ b/app/details/[slug]/page.tsx @@ -0,0 +1,46 @@ +"use client"; + +import { useSearchParams } from "next/navigation"; + +import VideoPlayerSection from "@/components/details/video-sections"; +import VideoSidebar from "@/components/details/video-sidebar-details"; + +import ImageSidebar from "@/components/details/image-sidebar-details"; +import ImageDetailSection from "@/components/details/image-selections"; +import DocumentDetailSection from "@/components/details/document-selections"; +import AudioPlayerSection from "@/components/details/audio-selections"; +import DocumentSidebar from "@/components/details/document-sidebar-details"; +import AudioSidebar from "@/components/details/audio-sidebar-details"; +import FloatingMenuNews from "@/components/landing-page/floating-news"; +import Footer from "@/components/landing-page/footer"; + +export default function DetailsPage() { + const params = useSearchParams(); + const type = params.get("type"); + + return ( +
+
+
+ {/* LEFT */} +
+ {type === "video" && } + {type === "image" && } + {type === "text" && } + {type === "audio" && } +
+ + {/* RIGHT */} +
+ {type === "video" && } + {type === "image" && } + {type === "text" && } + {type === "audio" && } +
+ +
+
+
+
+ ); +} diff --git a/app/document/filter/page.tsx b/app/document/filter/page.tsx new file mode 100644 index 0000000..724a7d4 --- /dev/null +++ b/app/document/filter/page.tsx @@ -0,0 +1,83 @@ +"use client"; + +import DocumentCard from "@/components/document/document-card"; +import FilterDocumentSidebar from "@/components/document/filter-sidebar"; +import FloatingMenuNews from "@/components/landing-page/floating-news"; +import Footer from "@/components/landing-page/footer"; + +import { useState } from "react"; + +export default function DocumentFilterPage() { + const [openFilter, setOpenFilter] = useState(false); + + return ( +
+
+
+ {/* ===== TOP BAR ===== */} + + {/* ===== CONTENT ===== */} +
+ {/* Sidebar */} +
+ +
+ + {/* Mobile Sidebar */} + {openFilter && ( +
+
+ + +
+
+ )} + + {/* Cards */} +
+
+
+ Document   >   + Lihat Semua + {"|"} + + Terdapat 1636 berita + +
+ +
+ Urutkan: + + +
+
+
+ {Array.from({ length: 6 }).map((_, i) => ( + + ))} +
+
+
+ + {/* ===== PAGINATION ===== */} +
+ + + ... + + +
+
+
+
+
+ ); +} diff --git a/app/favicon.ico b/app/favicon.ico new file mode 100644 index 0000000..718d6fe Binary files /dev/null and b/app/favicon.ico differ diff --git a/app/globals.css b/app/globals.css new file mode 100644 index 0000000..b30e457 --- /dev/null +++ b/app/globals.css @@ -0,0 +1,140 @@ +@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); + --radius-2xl: calc(var(--radius) + 8px); + --radius-3xl: calc(var(--radius) + 12px); + --radius-4xl: calc(var(--radius) + 16px); +} + +: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 utilities { + @keyframes tech-scroll { + 0% { + transform: translateX(0); + } + 100% { + transform: translateX(-50%); + } + } + + .animate-tech-scroll { + animation: tech-scroll 40s linear infinite; + } +} diff --git a/app/image/filter/page.tsx b/app/image/filter/page.tsx new file mode 100644 index 0000000..92385b7 --- /dev/null +++ b/app/image/filter/page.tsx @@ -0,0 +1,87 @@ +"use client"; + +import AudioCard from "@/components/audio/audio-card"; +import FilterAudioSidebar from "@/components/audio/filter-sidebar"; +import FilterImageSidebar from "@/components/image/filter-sidebar"; +import ImageCard from "@/components/image/image-card"; +import FloatingMenuNews from "@/components/landing-page/floating-news"; +import Footer from "@/components/landing-page/footer"; +import FilterSidebar from "@/components/video/filter-sidebar"; +import VideoCard from "@/components/video/video-card"; +import { Menu } from "lucide-react"; +import { useState } from "react"; + +export default function ImageFilterPage() { + const [openFilter, setOpenFilter] = useState(false); + + return ( +
+
+
+ {/* ===== TOP BAR ===== */} + + {/* ===== CONTENT ===== */} +
+ {/* Sidebar */} +
+ +
+ + {/* Mobile Sidebar */} + {openFilter && ( +
+
+ + +
+
+ )} + + {/* Cards */} +
+
+
+ Foto   >   + Lihat Semua + {"|"} + + Terdapat 1636 berita + +
+ +
+ Urutkan: + + +
+
+
+ {Array.from({ length: 6 }).map((_, i) => ( + + ))} +
+
+
+ + {/* ===== PAGINATION ===== */} +
+ + + ... + + +
+
+
+
+
+ ); +} diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 0000000..f7fa87e --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,34 @@ +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: "Create Next App", + description: "Generated by create next app", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + {children} + + + ); +} diff --git a/app/news-services/page.tsx b/app/news-services/page.tsx new file mode 100644 index 0000000..a33fd49 --- /dev/null +++ b/app/news-services/page.tsx @@ -0,0 +1,20 @@ +import Footer from "@/components/landing-page/footer"; +import FloatingMenu from "@/components/landing-page/floating"; +import NewsAndServicesHeader from "@/components/landing-page/headers-news-services"; +import ContentLatest from "@/components/landing-page/content-latest"; +import ContentPopular from "@/components/landing-page/content-popular"; +import ContentCategory from "@/components/landing-page/category-content"; +import FloatingMenuNews from "@/components/landing-page/floating-news"; + +export default function NewsAndServicesPage() { + return ( +
+ + + + + +
+
+ ); +} diff --git a/app/page.tsx b/app/page.tsx new file mode 100644 index 0000000..06f1c2e --- /dev/null +++ b/app/page.tsx @@ -0,0 +1,24 @@ +import Header from "@/components/landing-page/headers"; +import AboutSection from "@/components/landing-page/about"; +import ProductSection from "@/components/landing-page/product"; +import ServiceSection from "@/components/landing-page/service"; +import Technology from "@/components/landing-page/technology"; +import Footer from "@/components/landing-page/footer"; +import FloatingMenu from "@/components/landing-page/floating"; + +export default function Home() { + return ( +
+ {/* FIXED MENU */} + + + {/* PAGE CONTENT */} +
+ + + + +
+
+ ); +} diff --git a/app/video/filter/page.tsx b/app/video/filter/page.tsx new file mode 100644 index 0000000..2dbf37a --- /dev/null +++ b/app/video/filter/page.tsx @@ -0,0 +1,83 @@ +"use client"; + +import FloatingMenuNews from "@/components/landing-page/floating-news"; +import Footer from "@/components/landing-page/footer"; +import FilterSidebar from "@/components/video/filter-sidebar"; +import VideoCard from "@/components/video/video-card"; +import { Menu } from "lucide-react"; +import { useState } from "react"; + +export default function VideoFilterPage() { + const [openFilter, setOpenFilter] = useState(false); + + return ( +
+
+
+ {/* ===== TOP BAR ===== */} + + {/* ===== CONTENT ===== */} +
+ {/* Sidebar */} +
+ +
+ + {/* Mobile Sidebar */} + {openFilter && ( +
+
+ + +
+
+ )} + + {/* Cards */} +
+
+
+ Audio Visual   >   + Lihat Semua + {"|"} + + Terdapat 1636 berita + +
+ +
+ Urutkan: + + +
+
+
+ {Array.from({ length: 6 }).map((_, i) => ( + + ))} +
+
+
+ + {/* ===== PAGINATION ===== */} +
+ + + ... + + +
+
+
+
+
+ ); +} diff --git a/components.json b/components.json new file mode 100644 index 0000000..b7b9791 --- /dev/null +++ b/components.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "registries": {} +} diff --git a/components/audio/audio-card.tsx b/components/audio/audio-card.tsx new file mode 100644 index 0000000..bee72c9 --- /dev/null +++ b/components/audio/audio-card.tsx @@ -0,0 +1,51 @@ +"use client"; + +import Image from "next/image"; +import Link from "next/link"; + +export default function DocumentCard() { + const slug = "bharatu-mardi-hadji-gugur-saat-bertugas"; + return ( + +
+ {/* IMAGE */} +
+ news +
+ + {/* CONTENT */} +
+ {/* BADGE + TAG */} +
+ + POLRI + + + SEPUTAR PRESTASI + +
+ + {/* DATE */} +

02 Februari 2024

+ + {/* TITLE */} +

+ Bharatu Mardi Hadji Gugur Saat Bertugas, Diganjar Kenaikan Pangkat + Luar Biasa +

+ + {/* EXCERPT */} +

+ Jakarta - Kapolri Jenderal Polisi Drs. Listyo Sigit Prabowo + memberikan kenaikan pangkat luar biasa anumerta kepada... +

+
+
+ + ); +} diff --git a/components/audio/filter-sidebar.tsx b/components/audio/filter-sidebar.tsx new file mode 100644 index 0000000..202f459 --- /dev/null +++ b/components/audio/filter-sidebar.tsx @@ -0,0 +1,99 @@ +"use client"; + +import { ChevronLeft } from "lucide-react"; + +export default function FilterAudioSidebar() { + return ( +
+ {/* HEADER */} +
+

+ Filter +

+ +
+ + {/* CONTENT */} +
+ {/* KATEGORI */} + + + + + + + + + + + {/* JENIS FILE */} + + + + + + + + + + + {/* FORMAT */} + + + + + {/* RESET */} +
+ +
+
+
+ ); +} + +/* ===== COMPONENTS ===== */ + +function FilterSection({ + title, + children, +}: { + title: string; + children: React.ReactNode; +}) { + return ( +
+

{title}

+
{children}
+
+ ); +} + +function Checkbox({ + label, + count, + defaultChecked, +}: { + label: string; + count: number; + defaultChecked?: boolean; +}) { + return ( + + ); +} + +function Divider() { + return
; +} diff --git a/components/details/audio-selections.tsx b/components/details/audio-selections.tsx new file mode 100644 index 0000000..8005d2c --- /dev/null +++ b/components/details/audio-selections.tsx @@ -0,0 +1,105 @@ +"use client"; + +import { useState } from "react"; +import { Play, Pause, Volume2 } from "lucide-react"; + +export default function AudioPlayerSection() { + const [playing, setPlaying] = useState(false); + const [progress, setProgress] = useState(30); + + return ( +
+ {/* ===== AUDIO PLAYER CARD ===== */} +
+
+ {/* PLAY BUTTON */} + + + {/* WAVEFORM + DURATION */} +
+ {/* FAKE WAVEFORM */} +
+ {Array.from({ length: 70 }).map((_, i) => ( +
+ ))} +
+ + {/* TIME */} +
+ 2:14 + 5:00 +
+ + {/* PROGRESS */} +
+ + + setProgress(Number(e.target.value))} + className="w-full accent-blue-600" + /> +
+
+
+
+ + {/* ===== META INFO ===== */} +
+ + POLRI + + + 02 Februari 2024 + 61 + Kreator: BPKH Jurnalis +
+ + {/* ===== TITLE ===== */} +

+ Bharatu Mardi Hadji Gugur Saat Bertugas, Diganjar Kenaikan Pangkat Luar + Biasa +

+ + {/* ===== ARTICLE ===== */} +
+

+ Jakarta - Kapolri Jenderal Polisi Drs. Listyo Sigit Prabowo memberikan + kenaikan pangkat luar biasa anumerta kepada almarhum Bharatu Mardi + Hadji... +

+ +

+ Dengan penghargaan ini, almarhum resmi dinaikkan pangkatnya satu + tingkat lebih tinggi menjadi Bharaka Anumerta... +

+ +

+ Karo Penmas Divisi Humas Polri, Brigjen Pol. Trunoyudo Wisnu Andiko, + menyatakan bahwa kenaikan pangkat anumerta ini merupakan bentuk + penghormatan... +

+
+
+ ); +} diff --git a/components/details/audio-sidebar-details.tsx b/components/details/audio-sidebar-details.tsx new file mode 100644 index 0000000..3a4a60e --- /dev/null +++ b/components/details/audio-sidebar-details.tsx @@ -0,0 +1,177 @@ +"use client"; + +import { useState } from "react"; +import { Download, Facebook, Twitter } from "lucide-react"; + +export default function AudioSidebar() { + const [selected, setSelected] = useState("4K"); + + const options = [ + { + title: "4K", + size: "3840 x 2160 px", + file: "138 Mb", + format: "mov", + }, + { + title: "HD", + size: "1920 x 1080 px", + file: "100 Mb", + format: "mov", + }, + ]; + + return ( +
+ {/* TAG */} +
+ + POLRI + + +
+ + + +
+
+ +
+ + {/* OPTIONS */} +
+

Opsi Ukuran Audio

+ +
+ {options.map((item) => ( +
setSelected(item.title)} + className="cursor-pointer" + > +
+ {/* LEFT */} +
+ {/* CUSTOM RADIO */} +
+ {selected === item.title && ( +
+ )} +
+ +
+

{item.title}

+
+
+ + {/* RIGHT */} +
+

{item.size}

+

+ {item.file}   |   {item.format} +

+
+
+ +
+
+ ))} +
+
+ + {/* DOWNLOAD */} +
+ + + +
+ + {/* SHARE */} +
+

Bagikan:

+
+ +
+ + + +
+
+ +
+ + + +
+
+ +
+ + + +
+
+
+
+
+ ); +} + +/* COMPONENTS */ + +function Tag({ label }: { label: string }) { + return ( + + {label} + + ); +} + +function ShareIcon({ children }: { children: React.ReactNode }) { + return ( +
+ {children} +
+ ); +} diff --git a/components/details/document-selections.tsx b/components/details/document-selections.tsx new file mode 100644 index 0000000..1cb386d --- /dev/null +++ b/components/details/document-selections.tsx @@ -0,0 +1,13 @@ +export default function DocumentDetailSection() { + return ( +
+ + +

+ Novita Hardini: Jangan Sampai Pariwisata Meminggirkan Warga Lokal +

+ +

PARLEMENTARIA, Mandalika...

+
+ ); +} diff --git a/components/details/document-sidebar-details.tsx b/components/details/document-sidebar-details.tsx new file mode 100644 index 0000000..faf6119 --- /dev/null +++ b/components/details/document-sidebar-details.tsx @@ -0,0 +1,180 @@ +"use client"; + +import { useState } from "react"; +import { Download, Facebook, Twitter } from "lucide-react"; + +export default function DocumentSidebar() { + const [selected, setSelected] = useState("4K"); + + const options = [ + { + title: "DOC", + size: "296KB", + file: "138 Mb", + format: "mov", + }, + { + title: "PPT", + size: "296KB", + file: "100 Mb", + format: "mov", + }, + { + title: "PDF", + size: "296KB", + file: "80 Mb", + format: "mp4", + }, + ]; + + return ( +
+ {/* TAG */} +
+ + POLRI + + +
+ + + +
+
+ +
+ + {/* OPTIONS */} +
+

Opsi Ukuran Document

+ +
+ {options.map((item) => ( +
setSelected(item.title)} + className="cursor-pointer" + > +
+ {/* LEFT */} +
+ {/* CUSTOM RADIO */} +
+ {selected === item.title && ( +
+ )} +
+ +
+

{item.title}

+
+
+ + {/* RIGHT */} +
+

{item.size}

+
+
+ +
+
+ ))} +
+
+ + {/* DOWNLOAD */} +
+ + + +
+ + {/* SHARE */} +
+

Bagikan:

+
+ +
+ + + +
+
+ +
+ + + +
+
+ +
+ + + +
+
+
+
+
+ ); +} + +/* COMPONENTS */ + +function Tag({ label }: { label: string }) { + return ( + + {label} + + ); +} + +function ShareIcon({ children }: { children: React.ReactNode }) { + return ( +
+ {children} +
+ ); +} diff --git a/components/details/image-selections.tsx b/components/details/image-selections.tsx new file mode 100644 index 0000000..67644aa --- /dev/null +++ b/components/details/image-selections.tsx @@ -0,0 +1,13 @@ +export default function ImageDetailSection() { + return ( +
+ + +

+ Novita Hardini: Jangan Sampai Pariwisata Meminggirkan Warga Lokal +

+ +

PARLEMENTARIA, Mandalika...

+
+ ); +} diff --git a/components/details/image-sidebar-details.tsx b/components/details/image-sidebar-details.tsx new file mode 100644 index 0000000..396dfef --- /dev/null +++ b/components/details/image-sidebar-details.tsx @@ -0,0 +1,186 @@ +"use client"; + +import { useState } from "react"; +import { Download, Facebook, Twitter } from "lucide-react"; + +export default function ImageSidebar() { + const [selected, setSelected] = useState("4K"); + + const options = [ + { + title: "XL", + size: "3840 x 2160 px", + file: "138 Mb", + format: "mov", + }, + { + title: "L", + size: "1920 x 1080 px", + file: "100 Mb", + format: "mov", + }, + { + title: "M", + size: "1280 x 720 px", + file: "80 Mb", + format: "mp4", + }, + { + title: "S", + size: "640 x 360 px", + file: "40 Mb", + format: "mp4", + }, + ]; + + return ( +
+ {/* TAG */} +
+ + POLRI + + +
+ + + +
+
+ +
+ + {/* OPTIONS */} +
+

Opsi Ukuran Foto

+ +
+ {options.map((item) => ( +
setSelected(item.title)} + className="cursor-pointer" + > +
+ {/* LEFT */} +
+ {/* CUSTOM RADIO */} +
+ {selected === item.title && ( +
+ )} +
+ +
+

{item.title}

+
+
+ + {/* RIGHT */} +
+

{item.size}

+
+
+ +
+
+ ))} +
+
+ + {/* DOWNLOAD */} +
+ + + +
+ + {/* SHARE */} +
+

Bagikan:

+
+ +
+ + + +
+
+ +
+ + + +
+
+ +
+ + + +
+
+
+
+
+ ); +} + +/* COMPONENTS */ + +function Tag({ label }: { label: string }) { + return ( + + {label} + + ); +} + +function ShareIcon({ children }: { children: React.ReactNode }) { + return ( +
+ {children} +
+ ); +} diff --git a/components/details/video-meta.tsx b/components/details/video-meta.tsx new file mode 100644 index 0000000..b889fa1 --- /dev/null +++ b/components/details/video-meta.tsx @@ -0,0 +1,80 @@ +"use client"; + +import { Eye, Calendar } from "lucide-react"; + +export default function VideoMeta() { + return ( +
+ {/* INFO */} +
+ + POLRI + + +
+ + 02 Februari 2024 +
+ +
+ + 61 +
+ + Kreator: POLRI Jurnalis +
+ + {/* TITLE */} +

+ Bharatu Mardi Hadji Gugur Saat Bertugas, Diganjar Kenaikan Pangkat Luar + Biasa +

+ + {/* ARTICLE */} +
+

+ Jakarta - Kapolri Jenderal Polisi Drs. Listyo Sigit Prabowo memberikan + kenaikan pangkat luar biasa anumerta kepada almarhum Bharatu Mardi + Hadji, yang gugur dalam misi kemanusiaan saat melakukan pencarian dua + nelayan yang mengalami mati mesin di perairan Desa Gita, Kecamatan + Oba, Kota Tidore Kepulauan, Minggu (2/2/2025). Dengan penghargaan ini, + almarhum resmi dinaikkan pangkatnya satu tingkat lebih tinggi menjadi + Bharaka Anumerta, terhitung mulai 3 Februari 2025, berdasarkan + Keputusan Kapolri Nomor: Kep/208/II/2025. Karo Penmas Divisi Humas + Polri, Brigjen Pol. Trunoyudo Wisnu Andiko, menyatakan bahwa kenaikan + pangkat anumerta ini merupakan bentuk penghormatan dan apresiasi atas + dedikasi serta pengorbanan almarhum dalam menjalankan tugasnya. + "Penghargaan kenaikan pangkat luar biasa anumerta ini diberikan + sebagai bentuk apresiasi Polri atas dedikasi dan pengorbanan almarhum + dalam menjalankan tugas kemanusiaan. Ini juga sebagai wujud + penghormatan atas pengabdiannya dalam melayani masyarakat," ujar + Brigjen Pol. Trunoyudo Wisnu Andiko, Selasa (4/2). Almarhum Bharaka + Anumerta Mardi Hadji merupakan anggota Direktorat Polairud Polda + Maluku Utara, yang gugur dalam insiden meledaknya speedboat milik + Basarnas Ternate saat menjalankan misi pencarian nelayan hilang. Dalam + insiden tersebut, tiga korban dinyatakan meninggal dunia, yakni + Bharatu Mardi Hadji, serta dua anggota Basarnas, Fadli Malagapi dan + Riski Esa, sementara satu wartawan Kontributor Metro TV dinyatakan + hilang dan masih dalam proses pencarian. Jenazah Bharaka Anumerta + Mardi Hadji dimakamkan dengan upacara penghormatan militer yang + dipimpin langsung oleh Direktur Polairud Polda Malut, Kombes Pol. + Azhari Juanda, di Kelurahan Moya, Kota Ternate, pada pukul 15.00 WIT. + Selain itu, santunan dari Kapolda Maluku Utara juga diserahkan kepada + keluarga almarhum sebagai bentuk duka cita dan penghargaan atas jasa + pengabdiannya. Wakapolda Maluku Utara, Brigjen Pol. Stephen M. Napiun, + sebelumnya telah mengusulkan kenaikan pangkat luar biasa bagi almarhum + kepada Mabes Polri sebagai bentuk penghormatan atas pengorbanannya. + "Polri menghormati jasa almarhum yang telah mengutamakan keselamatan + orang lain. Namun, kondisi cuaca dan ombak yang tidak menentu + menyebabkan insiden ini terjadi. Kami turut berbelasungkawa dan + berharap keluarga yang ditinggalkan diberi kekuatan," ujar Brigjen + Pol. Stephen M. Napiun saat berkunjung ke rumah duka di Ternate. + Dengan penghargaan ini, Polri menegaskan komitmennya untuk selalu + menghargai dedikasi dan pengabdian personel yang gugur dalam tugas + serta memastikan hak-hak keluarga almarhum terpenuhi sesuai aturan + yang berlaku. +

+
+
+ ); +} diff --git a/components/details/video-sections.tsx b/components/details/video-sections.tsx new file mode 100644 index 0000000..6a8d0ff --- /dev/null +++ b/components/details/video-sections.tsx @@ -0,0 +1,36 @@ +"use client"; + +import Image from "next/image"; +import { Play } from "lucide-react"; +import VideoMeta from "./video-meta"; + +export default function VideoPlayerSection() { + return ( +
+ {/* VIDEO THUMB */} +
+ video + + {/* Play Button */} +
+
+ +
+
+ + {/* Duration */} +
+ 1:58 / 3:00 +
+
+ + {/* META & CONTENT */} + +
+ ); +} diff --git a/components/details/video-sidebar-details.tsx b/components/details/video-sidebar-details.tsx new file mode 100644 index 0000000..cf5ba36 --- /dev/null +++ b/components/details/video-sidebar-details.tsx @@ -0,0 +1,191 @@ +"use client"; + +import { useState } from "react"; +import { Download, Facebook, Twitter } from "lucide-react"; + +export default function VideoSidebar() { + const [selected, setSelected] = useState("4K"); + + const options = [ + { + title: "4K", + size: "3840 x 2160 px", + file: "138 Mb", + format: "mov", + }, + { + title: "HD", + size: "1920 x 1080 px", + file: "100 Mb", + format: "mov", + }, + { + title: "HD Web", + size: "1280 x 720 px", + file: "80 Mb", + format: "mp4", + }, + { + title: "Web", + size: "640 x 360 px", + file: "40 Mb", + format: "mp4", + }, + ]; + + return ( +
+ {/* TAG */} +
+ + POLRI + + +
+ + + +
+
+ +
+ + {/* OPTIONS */} +
+

+ Opsi Ukuran Audio Visual +

+ +
+ {options.map((item) => ( +
setSelected(item.title)} + className="cursor-pointer" + > +
+ {/* LEFT */} +
+ {/* CUSTOM RADIO */} +
+ {selected === item.title && ( +
+ )} +
+ +
+

{item.title}

+
+
+ + {/* RIGHT */} +
+

{item.size}

+

+ {item.file}   |   {item.format} +

+
+
+ +
+
+ ))} +
+
+ + {/* DOWNLOAD */} +
+ + + +
+ + {/* SHARE */} +
+

Bagikan:

+
+ +
+ + + +
+
+ +
+ + + +
+
+ +
+ + + +
+
+
+
+
+ ); +} + +/* COMPONENTS */ + +function Tag({ label }: { label: string }) { + return ( + + {label} + + ); +} + +function ShareIcon({ children }: { children: React.ReactNode }) { + return ( +
+ {children} +
+ ); +} diff --git a/components/document/document-card.tsx b/components/document/document-card.tsx new file mode 100644 index 0000000..f94ee86 --- /dev/null +++ b/components/document/document-card.tsx @@ -0,0 +1,51 @@ +"use client"; + +import Image from "next/image"; +import Link from "next/link"; + +export default function DocumentCard() { + const slug = "bharatu-mardi-hadji-gugur-saat-bertugas"; + return ( + +
+ {/* IMAGE */} +
+ news +
+ + {/* CONTENT */} +
+ {/* BADGE + TAG */} +
+ + POLRI + + + SEPUTAR PRESTASI + +
+ + {/* DATE */} +

02 Februari 2024

+ + {/* TITLE */} +

+ Bharatu Mardi Hadji Gugur Saat Bertugas, Diganjar Kenaikan Pangkat + Luar Biasa +

+ + {/* EXCERPT */} +

+ Jakarta - Kapolri Jenderal Polisi Drs. Listyo Sigit Prabowo + memberikan kenaikan pangkat luar biasa anumerta kepada... +

+
+
+ + ); +} diff --git a/components/document/filter-sidebar.tsx b/components/document/filter-sidebar.tsx new file mode 100644 index 0000000..858e17d --- /dev/null +++ b/components/document/filter-sidebar.tsx @@ -0,0 +1,99 @@ +"use client"; + +import { ChevronLeft } from "lucide-react"; + +export default function FilterDocumentSidebar() { + return ( +
+ {/* HEADER */} +
+

+ Filter +

+ +
+ + {/* CONTENT */} +
+ {/* KATEGORI */} + + + + + + + + + + + {/* JENIS FILE */} + + + + + + + + + + + {/* FORMAT */} + + + + + {/* RESET */} +
+ +
+
+
+ ); +} + +/* ===== COMPONENTS ===== */ + +function FilterSection({ + title, + children, +}: { + title: string; + children: React.ReactNode; +}) { + return ( +
+

{title}

+
{children}
+
+ ); +} + +function Checkbox({ + label, + count, + defaultChecked, +}: { + label: string; + count: number; + defaultChecked?: boolean; +}) { + return ( + + ); +} + +function Divider() { + return
; +} diff --git a/components/form/login.tsx b/components/form/login.tsx new file mode 100644 index 0000000..3673b74 --- /dev/null +++ b/components/form/login.tsx @@ -0,0 +1,206 @@ +"use client"; +import React, { useState } from "react"; +import Link from "next/link"; +import Cookies from "js-cookie"; +import { close, error, loading } from "@/config/swal"; +import { useRouter } from "next/navigation"; +import Swal from "sweetalert2"; +import withReactContent from "sweetalert2-react-content"; +import { Input } from "../ui/input"; +import { Button } from "../ui/button"; +import { Label } from "../ui/label"; +import { EyeSlashFilledIcon, EyeFilledIcon } from "../icons"; +import Image from "next/image"; +import { EyeOff, Eye } from "lucide-react"; + +export default function Login() { + const router = useRouter(); + const [isVisible, setIsVisible] = useState(false); + const [isVisibleSetup, setIsVisibleSetup] = useState([false, false]); + const [oldEmail, setOldEmail] = useState(""); + const [newEmail, setNewEmail] = useState(""); + + const toggleVisibility = () => setIsVisible(!isVisible); + const [needOtp, setNeedOtp] = useState(false); + const [isFirstLogin, setFirstLogin] = useState(false); + const [otpValue, setOtpValue] = useState(""); + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [showPassword, setShowPassword] = useState(false); + + const setValUsername = (e: any) => { + const uname = e.replaceAll(/[^\w.-]/g, ""); + setUsername(uname.toLowerCase()); + }; + + const onSubmit = async () => { + if (!username || !password) { + error("Username & Password Wajib Diisi !"); + return; + } + + loading(); + + setTimeout(() => { + const users = [ + { + username: "admin", + password: "admin123", + role: "Admin", + redirect: "/admin/dashboard", + }, + { + username: "approver", + password: "approver123", + role: "Approver", + redirect: "/admin/dashboard", + }, + { + username: "kontributor", + password: "kontributor123", + role: "Kontributor", + redirect: "/admin/dashboard", + }, + ]; + + const foundUser = users.find( + (u) => u.username === username && u.password === password, + ); + + if (!foundUser) { + close(); + error("Username / Password Tidak Sesuai"); + return; + } + + // Dummy Token + const fakeToken = `dummy-token-${foundUser.role}`; + const fakeRefresh = `dummy-refresh-${foundUser.role}`; + + const newTime = (new Date().getTime() + 10 * 60 * 1000).toString(); + + Cookies.set("time_refresh", newTime, { expires: 1 }); + + Cookies.set("access_token", fakeToken, { expires: 1 }); + Cookies.set("refresh_token", fakeRefresh, { expires: 1 }); + Cookies.set("time_refresh", newTime, { expires: 1 }); + + Cookies.set("username", foundUser.username); + Cookies.set("fullname", foundUser.role); + Cookies.set("roleName", foundUser.role); + Cookies.set("status", "login"); + + close(); + + router.push(foundUser.redirect); + }, 1000); + }; + + return ( +
+ {/* LEFT IMAGE SECTION */} +
+ Login Illustration +
+ + {/* RIGHT FORM SECTION */} +
+
+ {/* LOGO */} +
+ Qudoco Logo +
+ + {/* TITLE */} +

+ Welcome Back +

+ + {/* FORM */} +
+ {/* Username */} +
+ + setValUsername(e.target.value)} + className="w-full border-b border-gray-300 focus:border-[#9c6b16] outline-none py-2 bg-transparent transition" + /> +
+ + {/* Password */} +
+ +
+ setPassword(e.target.value)} + className="w-full border-b border-gray-300 focus:border-[#9c6b16] outline-none py-2 pr-10 bg-transparent transition" + /> + + +
+
+ + {/* BUTTON */} + +
+ + {/* REGISTER */} +

+ Don’t have an account?{" "} + + Register + +

+ + {/* FOOTER */} +
+ Terms + + Privacy Policy + + Security + +
+ © 2024 Copyrights by company. All Rights Reserved. +
+ Designed by Qudoco Team +
+
+
+
+
+ ); +} diff --git a/components/icons.tsx b/components/icons.tsx new file mode 100644 index 0000000..7f3a70a --- /dev/null +++ b/components/icons.tsx @@ -0,0 +1,2734 @@ +import * as React from "react"; +import { IconSvgProps } from "@/types"; + +export const Logo: React.FC = ({ + size = 36, + width, + height, + ...props +}) => ( + + + +); + +export const DiscordIcon: React.FC = ({ + size = 24, + width, + height, + ...props +}) => { + return ( + + + + ); +}; + +export const TwitterIcon: React.FC = ({ + size = 30, + width, + height, + color = "white", + ...props +}) => { + return ( + + + + ); +}; + +export const IconX: React.FC = ({ + size = 30, + width, + height, + color = "white", + ...props +}) => { + return ( + + + + ); +}; + +export const SendIcon: React.FC = ({ + size, + width, + height, + color = "currentColor", + ...props +}) => { + return ( + + + + + + + + + + + + + + + + ); +}; + +export const GithubIcon: React.FC = ({ + size = 24, + width, + height, + ...props +}) => { + return ( + + + + ); +}; + +export const MoonFilledIcon = ({ + size = 24, + width, + height, + ...props +}: IconSvgProps) => ( + +); + +export const SunFilledIcon = ({ + size = 24, + width, + height, + ...props +}: IconSvgProps) => ( + +); + +export const HeartFilledIcon = ({ + size = 24, + width, + height, + ...props +}: IconSvgProps) => ( + +); + +export const SearchIcon = (props: IconSvgProps) => ( + +); + +export const NextUILogo: React.FC = (props) => { + const { width, height = 40 } = props; + + return ( + + + + + + ); +}; + +export const FbIcon: React.FC = (props) => { + return ( + + + + + + + + + + + + ); +}; + +export const ChevronUpIcon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + +); + +export const ChevronDownIcon = ({ + size, + height = 24, + width = 14, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + +); + +export const ChevronRightIcon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + +); + +export const ChevronLeftWhite = ({ + size, + height = 24, + width = 24, + color = "white", + ...props +}: IconSvgProps & { color?: string }) => ( + + + +); + +export const ChevronRightWhite = ({ + size, + height = 24, + width = 24, + color = "white", + ...props +}: IconSvgProps) => ( + + + +); + +export const IgIcon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + + +); + +export const FbIconNav = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + + +); + +export const YtIcon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + + + + + + + + + +); + +export const IdnIcon = ({ + size, + height = 24, + width = 14, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + + + + + + + + + + + +); + +export const UKIcon = ({ + size, + height = 24, + width = 14, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + + + + + + + + + + + + + + + + + +); + +export const TwIcon = ({ + size, + height = 24, + width = 14, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + + +); + +export const TtIcon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + + + + + + + + + +); + +export const EyeIcon = ({ + size, + height = 24, + width = 14, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + +); + +export const DotsIcon = ({ + size, + height = 24, + width = 24, + fill = "none", + ...props +}: IconSvgProps) => ( + + + +); + +export const MailIcon = (props: any) => ( + +); + +export const SearchIcons = (props: any) => ( + +); +export const UnderLine = (props: any) => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +); + +export const EyeSlashFilledIcon = (props: any) => ( + +); + +export const EyeFilledIcon = (props: any) => ( + +); + +export const ArrowIcons: React.FC = ({ + size = 30, + width, + height, + color = "white", + ...props +}) => { + return ( + + + + + + + + + + + ); +}; + +export const Hotline = ({ + size = 24, + width, + height, + ...props +}: IconSvgProps) => ( + + + + +); + +export const CustomerService = ({ + size = 24, + width, + height, + ...props +}: IconSvgProps) => ( + + + + +); + +export const Mail = ({ size = 24, width, height, ...props }: IconSvgProps) => ( + + + +); + +export const Location = ({ + size = 24, + width, + height, + ...props +}: IconSvgProps) => ( + + + +); + +export const Calender = ({ + size = 24, + width, + height, + ...props +}: IconSvgProps) => ( + + + + + + + + + + +); + +export const WorldIcon = ({ + size = 24, + width, + height, + ...props +}: IconSvgProps) => ( + + + + + + + + + + +); + +export const Checklist = ({ + size = 24, + width, + height, + ...props +}: IconSvgProps) => ( + + + + + + + + + + +); + +export const ChevronLeftIcon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + +); + +export const DotsYIcon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + +); + +export const DotsXIcon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + +); + +export const EyeIconMdi = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + +); + +export const OnlineIcon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + +); + +export const OfflineIcon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + + +); + +export const CreateIconIon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + + +); + +export const DeleteIcon = ({ + size, + height = 12, + width = 10, + fill = "none", + ...props +}: IconSvgProps) => ( + + + +); +export const BannerIcon = ({ + size, + height = 12, + width = 10, + fill = "none", + ...props +}: IconSvgProps) => ( + + + +); + +export const AccIcon = ({ + size, + height = 12, + width = 10, + fill = "none", + ...props +}: IconSvgProps) => ( + + + +); + +export const CloseIcon = ({ + size, + height = 12, + width = 10, + fill = "none", + ...props +}: IconSvgProps) => ( + + + +); + +export const RefundIcon = ({ + size, + height = 12, + width = 10, + fill = "none", + ...props +}: IconSvgProps) => ( + + + +); + +export const AddIcon = ({ + size, + height = 12, + width = 12, + fill = "none", + ...props +}: IconSvgProps) => ( + + + +); + +export const CompanyIcon = ({ + size, + height = 12, + width = 12, + fill = "none", + ...props +}: IconSvgProps) => ( + + + +); + +export const EmailIcon = ({ + size, + height = 12, + width = 12, + fill = "none", + ...props +}: IconSvgProps) => ( + + + +); + +export const PhoneIcon = ({ + size, + height = 12, + width = 12, + fill = "none", + ...props +}: IconSvgProps) => ( + + + +); + +export const MessageIcon = ({ + size, + height = 12, + width = 12, + fill = "none", + ...props +}: IconSvgProps) => ( + + + +); + +export const UserIcon = ({ + size, + height = 12, + width = 12, + fill = "none", + ...props +}: IconSvgProps) => ( + + + +); + +export const EyeOffIconMdi = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + +); + +export const DateIcon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + + +); + +export const WarningIcon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + +); + +export const PasswordIcon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + +); + +export const TimeIcon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + + +); + +export const VolumeLowIcon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + +); + +export const VolumeHighIcon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + + + +); + +export const FormVerticalIcon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + +); + +export const FormHorizontalIcon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + +); + +export const FormCustomIcon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + +); + +export const FormLayoutIcon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + +); + +export const FormValidationIcon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + + +); + +export const FormWizardIcon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + + +); + +export const FacebookIcon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + + +); + +export const GoogleIcon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + + + + + +); + +export const TimesIcon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + +); +export const CalendarIcon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + + + + +); +export const ClockIcon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + +); +export const SquareFacebookIcon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + +); + +export const SquareXIcon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + +); +export const SquareLinkedInIcon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + +); +export const SquareWhatsappIcon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + +); +export const CloudUploadIcon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + + + +); +export const BurgerButtonIcon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + +); + +export const XLandingIcon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + +); +export const InstagramLandingIcon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + +); +export const FacebookLandingIcon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + +); +export const TiktokLandingIcon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + +); +export const YoutubeLandingIcon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + +); +export const LandingEmailIcon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + +); +export const LandingCallIcon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + +); +export const LandingLocationIcon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + +); +export const LandingAppleIcon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + +); +export const LandingPlayStoreIcon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + +); +export const LandingAnalyticIcon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + +); +export const CopyIcon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + + + +); +export const PlayIcon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + + + + + + + + + + +); +export const ExportIcon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + + + +); +export const VideoIcon = ({ + size, + height = 37, + width = 32, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + + + + +); diff --git a/components/icons/dashboard-icon.tsx b/components/icons/dashboard-icon.tsx new file mode 100644 index 0000000..329ea16 --- /dev/null +++ b/components/icons/dashboard-icon.tsx @@ -0,0 +1,214 @@ +import * as React from "react"; +import { IconSvgProps } from "@/types/globals"; + +export const DashboardUserIcon = ({ + size, + height = 48, + width = 48, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + +); + +export const DashboardBriefcaseIcon = ({ + size, + height = 48, + width = 48, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + + + + + + + +); +export const DashboardMailboxIcon = ({ + size, + height = 48, + width = 48, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + + + + + +); +export const DashboardShareIcon = ({ + size, + height = 48, + width = 48, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + +); +export const DashboardSpeecIcon = ({ + size, + height = 48, + width = 48, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + + +); + +export const DashboardConnectIcon = ({ + size, + height = 48, + width = 48, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + +); + +export const DashboardTopLeftPointIcon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + +); + +export const DashboardRightDownPointIcon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + +); +export const DashboardCommentIcon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + + + + + + + + + +); diff --git a/components/icons/globals.tsx b/components/icons/globals.tsx new file mode 100644 index 0000000..0d11c16 --- /dev/null +++ b/components/icons/globals.tsx @@ -0,0 +1,196 @@ +import * as React from "react"; +import { IconSvgProps } from "@/types/globals"; + +export const PdfIcon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + + +); +export const CsvIcon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + +); +export const ExcelIcon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + + +); +export const WordIcon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + + +); + +export const PptIcon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + + +); +export const FileIcon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + +); +export const UserProfileIcon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + + +); +export const SettingsIcon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + +); diff --git a/components/icons/sidebar-icon.tsx b/components/icons/sidebar-icon.tsx new file mode 100644 index 0000000..2a8eee9 --- /dev/null +++ b/components/icons/sidebar-icon.tsx @@ -0,0 +1,487 @@ +import * as React from "react"; +import { IconSvgProps } from "@/types/globals"; + +export const MenuBurgerIcon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + +); + +export const DashboardIcon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + +); + +export const HomeIcon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + + + + +); + +export const Submenu1Icon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + + + + + + + + + + +); + +export const Submenu2Icon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + +); + +export const InfoCircleIcon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + + +); + +export const MinusCircleIcon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + + + + +); + +export const TableIcon = ({ + size, + height = 24, + width = 22, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + +); +export const ArticleIcon = ({ + size, + height = 20, + width = 20, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + +); +export const MagazineIcon = ({ + size, + height = 20, + width = 20, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +); + +export const StaticPageIcon = ({ + size, + height = 20, + width = 20, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + +); +export const MasterUsersIcon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + +); +export const MasterRoleIcon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + + +); +export const MasterUserLevelIcon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + +); +export const MasterCategoryIcon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + +); +export const AddvertiseIcon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + +); +export const SuggestionsIcon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + +); +export const CommentIcon = ({ + size, + height = 24, + width = 24, + fill = "currentColor", + ...props +}: IconSvgProps) => ( + + + +); diff --git a/components/image/filter-sidebar.tsx b/components/image/filter-sidebar.tsx new file mode 100644 index 0000000..b5310e9 --- /dev/null +++ b/components/image/filter-sidebar.tsx @@ -0,0 +1,99 @@ +"use client"; + +import { ChevronLeft } from "lucide-react"; + +export default function FilterImageSidebar() { + return ( +
+ {/* HEADER */} +
+

+ Filter +

+ +
+ + {/* CONTENT */} +
+ {/* KATEGORI */} + + + + + + + + + + + {/* JENIS FILE */} + + + + + + + + + + + {/* FORMAT */} + + + + + {/* RESET */} +
+ +
+
+
+ ); +} + +/* ===== COMPONENTS ===== */ + +function FilterSection({ + title, + children, +}: { + title: string; + children: React.ReactNode; +}) { + return ( +
+

{title}

+
{children}
+
+ ); +} + +function Checkbox({ + label, + count, + defaultChecked, +}: { + label: string; + count: number; + defaultChecked?: boolean; +}) { + return ( + + ); +} + +function Divider() { + return
; +} diff --git a/components/image/image-card.tsx b/components/image/image-card.tsx new file mode 100644 index 0000000..74b334d --- /dev/null +++ b/components/image/image-card.tsx @@ -0,0 +1,51 @@ +"use client"; + +import Image from "next/image"; +import Link from "next/link"; + +export default function ImageCard() { + const slug = "bharatu-mardi-hadji-gugur-saat-bertugas"; + return ( + +
+ {/* IMAGE */} +
+ news +
+ + {/* CONTENT */} +
+ {/* BADGE + TAG */} +
+ + POLRI + + + SEPUTAR PRESTASI + +
+ + {/* DATE */} +

02 Februari 2024

+ + {/* TITLE */} +

+ Bharatu Mardi Hadji Gugur Saat Bertugas, Diganjar Kenaikan Pangkat + Luar Biasa +

+ + {/* EXCERPT */} +

+ Jakarta - Kapolri Jenderal Polisi Drs. Listyo Sigit Prabowo + memberikan kenaikan pangkat luar biasa anumerta kepada... +

+
+
+ + ); +} diff --git a/components/landing-page/about.tsx b/components/landing-page/about.tsx new file mode 100644 index 0000000..29008d6 --- /dev/null +++ b/components/landing-page/about.tsx @@ -0,0 +1,74 @@ +import Image from "next/image"; + +export default function AboutSection() { + const socials = [ + { name: "Facebook", icon: "/image/fb.png" }, + { name: "Instagram", icon: "/image/ig.png" }, + { name: "X", icon: "/image/x.png" }, + { name: "Youtube", icon: "/image/yt.png" }, + { name: "Tiktok", icon: "/image/tt.png" }, + ]; + + return ( +
+ {/* TOP CENTER CONTENT */} +
+

+ Manage All your channels from Multipool +

+ + {/* SOCIAL ICONS */} +
+ {socials.map((item) => ( +
+ {item.name} +
+ ))} +
+
+ +
+ {/* PHONE IMAGE */} +
+ App Preview +
+ + {/* TEXT CONTENT */} +
+

+ About Us +

+ +

+ Helping you find the right{" "} + + + Solution + +

+ +

+ PT Qudo Buana Nawakara adalah perusahaan nasional Indonesia yang + berfokus pada pengembangan aplikasi untuk mendukung kegiatan + reputasi manajemen institusi, organisasi dan publik figur. Dengan + dukungan teknologi otomatisasi dan kecerdasan buatan (AI) untuk + mengoptimalkan proses. Perusahaan didukung oleh team SDM nasional + yang sudah berpengalaman serta memiliki sertifikasi internasional, + untuk memastikan produk yang dihasilkan handal dan berkualitas + tinggi. PT Qudo Buana Nawakara berkantor pusat di Jakarta dengan + support office di Bandung, Indonesia – India – USA – Oman. +

+
+
+
+ ); +} diff --git a/components/landing-page/category-content.tsx b/components/landing-page/category-content.tsx new file mode 100644 index 0000000..ed994d3 --- /dev/null +++ b/components/landing-page/category-content.tsx @@ -0,0 +1,53 @@ +"use client"; + +import { Card, CardContent } from "@/components/ui/card"; + +const categories = [ + { name: "Investment", total: 45 }, + { name: "Technology", total: 32 }, + { name: "Partnership", total: 28 }, + { name: "Report", total: 23 }, + { name: "Event", total: 19 }, + { name: "CSR", total: 15 }, +]; + +export default function ContentCategory() { + return ( +
+
+ {/* ===== Title ===== */} +

+ Kategori Konten +

+ + {/* ===== Card ===== */} + + + {categories.map((item, index) => ( +
+ {/* Left */} +
+ {/* Bullet */} +
+ + + {item.name} + +
+ + {/* Right total */} + {item.total} +
+ ))} + + +
+
+ ); +} diff --git a/components/landing-page/content-latest.tsx b/components/landing-page/content-latest.tsx new file mode 100644 index 0000000..f641797 --- /dev/null +++ b/components/landing-page/content-latest.tsx @@ -0,0 +1,158 @@ +"use client"; + +import Image from "next/image"; +import { Badge } from "@/components/ui/badge"; +import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; + +const data = [ + { + id: 1, + image: "/image/bharatu.jpg", + category: "POLRI", + categoryColor: "bg-red-600", + tag: "SEPUTAR PRESTASI", + date: "02 Februari 2024", + title: + "Bharatu Mardi Hadji Gugur Saat Bertugas, Diganjar Kenaikan Pangkat Luar Biasa", + }, + { + id: 2, + image: "/image/novita2.png", + category: "DPR", + categoryColor: "bg-yellow-500", + tag: "BERITA KOMISI 7", + date: "02 Februari 2024", + title: "Novita Hardini: Jangan Sampai Pariwisata Meminggirkan Warga Lokal", + }, + { + id: 3, + image: "/dummy/news-3.jpg", + category: "MPR", + categoryColor: "bg-yellow-600", + tag: "KEGIATAN EDUKASI", + date: "02 Februari 2024", + title: + "Lestari Moerdijat: Butuh Afirmasi dan Edukasi untuk Dorong Perempuan Aktif di Dunia Politik", + }, + { + id: 4, + image: "/dummy/news-2.jpg", + category: "MAHKAMAH AGUNG", + categoryColor: "bg-yellow-700", + tag: "HOT NEWS", + date: "02 Februari 2024", + title: "SEKRETARIS MAHKAMAH AGUNG LANTIK HAKIM TINGGI PENGAWAS", + }, +]; + +export default function ContentLatest() { + return ( +
+
+ {/* ===== HEADER ===== */} +
+

Konten Terbaru

+ + + {/* Tabs + Explore */} +
+ {/* Tabs Center */} +
+ + + Audio Visual + + + Audio + + + Foto + + + Teks + + +
+ + {/* Explore Right */} +
+ Explore more Trending +
+
+ + {/* ===== CONTENT ===== */} + + + + + + + + + + + + + + + +
+
+
+
+ ); +} + +/* ================= CARD GRID ================= */ + +function CardGrid() { + return ( +
+ {data.map((item) => ( +
+
+ {item.title} +
+ +
+
+ + {item.category} + + + {item.tag} +
+ +

{item.date}

+ +

+ {item.title} +

+
+
+ ))} +
+ ); +} diff --git a/components/landing-page/content-popular.tsx b/components/landing-page/content-popular.tsx new file mode 100644 index 0000000..0d0165a --- /dev/null +++ b/components/landing-page/content-popular.tsx @@ -0,0 +1,158 @@ +"use client"; + +import Image from "next/image"; +import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; +import { Badge } from "@/components/ui/badge"; + +const data = [ + { + id: 1, + image: "/image/bharatu.jpg", + category: "POLRI", + categoryColor: "bg-red-600", + tag: "SEPUTAR PRESTASI", + date: "02 Februari 2024", + title: + "Bharatu Mardi Hadji Gugur Saat Bertugas, Diganjar Kenaikan Pangkat Luar Biasa", + }, + { + id: 2, + image: "/image/novita2.png", + category: "DPR", + categoryColor: "bg-yellow-500", + tag: "BERITA KOMISI 7", + date: "02 Februari 2024", + title: "Novita Hardini: Jangan Sampai Pariwisata Meminggirkan Warga Lokal", + }, + { + id: 3, + image: "/dummy/news-3.jpg", + category: "MPR", + categoryColor: "bg-yellow-600", + tag: "KEGIATAN EDUKASI", + date: "02 Februari 2024", + title: + "Lestari Moerdijat: Butuh Afirmasi dan Edukasi untuk Dorong Perempuan Aktif di Dunia Politik", + }, + { + id: 4, + image: "/dummy/news-2.jpg", + category: "MAHKAMAH AGUNG", + categoryColor: "bg-yellow-700", + tag: "HOT NEWS", + date: "02 Februari 2024", + title: "SEKRETARIS MAHKAMAH AGUNG LANTIK HAKIM TINGGI PENGAWAS", + }, +]; + +export default function ContentLatest() { + return ( +
+
+ {/* ===== HEADER ===== */} +
+

Konten Terpopuler

+ + + {/* Tabs + Explore */} +
+ {/* Tabs Center */} +
+ + + Audio Visual + + + Audio + + + Foto + + + Teks + + +
+ + {/* Explore Right */} +
+ Explore more Trending +
+
+ + {/* ===== CONTENT ===== */} + + + + + + + + + + + + + + + +
+
+
+
+ ); +} + +/* ================= CARD GRID ================= */ + +function CardGrid() { + return ( +
+ {data.map((item) => ( +
+
+ {item.title} +
+ +
+
+ + {item.category} + + + {item.tag} +
+ +

{item.date}

+ +

+ {item.title} +

+
+
+ ))} +
+ ); +} diff --git a/components/landing-page/floating-news.tsx b/components/landing-page/floating-news.tsx new file mode 100644 index 0000000..ea3fecc --- /dev/null +++ b/components/landing-page/floating-news.tsx @@ -0,0 +1,122 @@ +"use client"; + +import { useState } from "react"; +import { + Menu, + X, + Home, + ChevronDown, + Video, + Music, + Image as ImageIcon, + FileText, +} from "lucide-react"; +import Link from "next/link"; + +export default function FloatingMenuNews() { + const [open, setOpen] = useState(false); + const [openKonten, setOpenKonten] = useState(false); + + return ( + <> + {/* FLOATING BUTTON */} + + + {/* OVERLAY */} + {open && ( +
setOpen(false)} + className="fixed inset-0 z-[90] bg-black/40" + /> + )} + + {/* SIDEBAR */} + + + ); +} + +/* ================= SUBMENU ================= */ + +function SubMenuItem({ + icon, + label, +}: { + icon: React.ReactNode; + label: string; +}) { + return ( +
+ {label} + {icon} +
+ ); +} diff --git a/components/landing-page/floating.tsx b/components/landing-page/floating.tsx new file mode 100644 index 0000000..6e6ae4c --- /dev/null +++ b/components/landing-page/floating.tsx @@ -0,0 +1,73 @@ +"use client"; + +import { useState } from "react"; +import { Menu, X, Home, Box, Briefcase, Newspaper, LogIn } from "lucide-react"; +import Link from "next/link"; + +export default function FloatingMenu() { + const [open, setOpen] = useState(false); + + return ( + <> + {/* FLOATING BUTTON */} + + + {/* OVERLAY */} + {open && ( +
setOpen(false)} + className="fixed inset-0 z-[90] bg-black/40" + /> + )} + + {/* SIDEBAR */} + + + ); +} + +function MenuItem({ icon, label }: { icon: React.ReactNode; label: string }) { + return ( +
+ {label} + {icon} +
+ ); +} diff --git a/components/landing-page/footer.tsx b/components/landing-page/footer.tsx new file mode 100644 index 0000000..6cc8ec2 --- /dev/null +++ b/components/landing-page/footer.tsx @@ -0,0 +1,93 @@ +import Image from "next/image"; +import { Mail, Facebook, Twitter, Youtube, Instagram } from "lucide-react"; + +export default function Footer() { + return ( +
+
+
+ {/* Logo */} +
+
+ Qudoco +
+
+ + {/* Information */} +
+

Information

+
    +
  • Home
  • +
  • Blog
  • +
  • Document
  • +
+
+ + {/* Product */} +
+

Product

+
    +
  • MediaHUB Content Aggregator
  • +
  • Multipool Reputation Management
  • +
  • PR Room Opinion Management
  • +
+
+ + {/* Service */} +
+

Service

+
    +
  • Artifintel
  • +
  • Produksi Video Animasi
  • +
  • Reelithic
  • +
  • Qudoin
  • +
  • Talkshow AI
  • +
+
+ + {/* Get in Touch */} +
+

Get in Touch.

+

+ Indonesia – India – USA – Oman +

+ + {/* Email */} +
+ + sales@qudoco.com + +
+ + {/* Social */} +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+ + {/* Divider */} +
+ © 2024 Copyrights by company. All Rights Reserved. Designed by{" "} + Qudoco Team +
+
+
+ ); +} diff --git a/components/landing-page/headers-news-services.tsx b/components/landing-page/headers-news-services.tsx new file mode 100644 index 0000000..e718c80 --- /dev/null +++ b/components/landing-page/headers-news-services.tsx @@ -0,0 +1,270 @@ +"use client"; + +import Image from "next/image"; +import { motion, AnimatePresence } from "framer-motion"; +import { X, ChevronLeft, ChevronRight } from "lucide-react"; +import { useEffect, useState } from "react"; +import { useRouter, useSearchParams } from "next/navigation"; + +const data = [ + { + id: 1, + title: + "Bharatu Mardi Hadji Gugur Saat Bertugas, Diganjar Kenaikan Pangkat Luar Biasa", + image: "/image/bharatu.jpg", + }, + { + id: 2, + title: "Pelayanan Publik Terus Ditingkatkan Demi Kenyamanan Masyarakat", + image: "/dummy/news-2.jpg", + }, + { + id: 3, + title: "Inovasi Teknologi Jadi Fokus Pengembangan Layanan", + image: "/dummy/news-3.jpg", + }, +]; + +const data1 = [ + { + id: 1, + title: "Novita Hardini: Jangan Sampai Pariwisata Meminggirkan Warga Lokal", + image: "/image/novita2.png", + excerpt: + "PARLEMENTARIA, Mandalika – Anggota Komisi VII DPR RI, Novita Hardini, menyoroti dampak sosial ekonomi dari pembangunan kawasan pariwisata...", + date: "7 November 2024", + category: "BERITA KOMISI 7", + tag: "DPR", + }, + { + id: 2, + title: "Pelayanan Publik Terus Ditingkatkan Demi Kenyamanan Masyarakat", + image: "/dummy/news-2.jpg", + excerpt: + "Pelayanan publik terus ditingkatkan untuk menjawab kebutuhan masyarakat...", + date: "6 November 2024", + category: "BERITA", + tag: "NASIONAL", + }, + { + id: 3, + title: "Inovasi Teknologi Jadi Fokus Pengembangan Layanan", + image: "/dummy/news-3.jpg", + excerpt: + "Transformasi digital menjadi fokus utama pengembangan layanan publik...", + date: "5 November 2024", + category: "TEKNOLOGI", + tag: "INOVASI", + }, +]; + +export default function NewsAndServicesHeader() { + // 🔹 STATE DIPISAH + const [activeHeader, setActiveHeader] = useState(0); + const [activeModal, setActiveModal] = useState(0); + + const [open, setOpen] = useState(false); + const [mounted, setMounted] = useState(false); + + const searchParams = useSearchParams(); + const router = useRouter(); + + useEffect(() => { + setMounted(true); + }, []); + + // 🔹 AUTO OPEN MODAL + useEffect(() => { + if (!mounted) return; + + const highlight = searchParams.get("highlight"); + if (highlight === "1") { + setActiveModal(activeHeader); // clone posisi header + setOpen(true); + } + }, [mounted, searchParams, activeHeader]); + + const closeModal = () => { + setOpen(false); + router.replace("/news-services"); + }; + + // ===== HEADER NAV ===== + const headerPrev = () => + setActiveHeader((p) => (p === 0 ? data.length - 1 : p - 1)); + const headerNext = () => + setActiveHeader((p) => (p === data.length - 1 ? 0 : p + 1)); + + // ===== MODAL NAV ===== + const modalPrev = () => + setActiveModal((p) => (p === 0 ? data.length - 1 : p - 1)); + const modalNext = () => + setActiveModal((p) => (p === data.length - 1 ? 0 : p + 1)); + + if (!mounted) return null; + + return ( + <> + {/* ================= HEADER ================= */} + {/* ================= HEADER ================= */} +
+
+ {/* ===== OUTER NAVIGATION ===== */} + + + + +
+ {/* IMAGE */} +
+
+ {data1[activeHeader].title} +
+ + {/* DOTS */} +
+ {data1.map((_, i) => ( + + ))} +
+
+ + {/* CONTENT */} +
+

+ {data1[activeHeader].title} +

+ +
+ {data1[activeHeader].date} + + {data1[activeHeader].category} + + + {data1[activeHeader].tag} + +
+ +

+ {data1[activeHeader].excerpt} +

+ + +
+
+ + {/* ===== SEARCH SECTION ===== */} +
+
+
🔍
+ + +
+
+
+
+ + {/* ================= MODAL ================= */} + + {open && ( + + + + +
+ {data[activeModal].title} + +
+

+ {data[activeModal].title} +

+
+ + + + +
+ +
+ {data.map((_, i) => ( +
+
+
+ )} +
+ + ); +} diff --git a/components/landing-page/headers.tsx b/components/landing-page/headers.tsx new file mode 100644 index 0000000..4ce9f61 --- /dev/null +++ b/components/landing-page/headers.tsx @@ -0,0 +1,77 @@ +"use client"; + +import Image from "next/image"; +import { Button } from "@/components/ui/button"; +import { useState } from "react"; +import { Menu, X, Home, Box, Briefcase, Newspaper } from "lucide-react"; + +export default function Header() { + const [open, setOpen] = useState(false); + + return ( +
+ + +
+
+

+ + + Beyond Expectations + +
+ Build Reputation. +

+ + +
+ +
+ Illustration +
+
+
+ ); +} + +function MenuItem({ icon, label }: { icon: React.ReactNode; label: string }) { + return ( +
+ {label} + {icon} +
+ ); +} diff --git a/components/landing-page/option.tsx b/components/landing-page/option.tsx new file mode 100644 index 0000000..512232d --- /dev/null +++ b/components/landing-page/option.tsx @@ -0,0 +1,120 @@ +import { motion } from "framer-motion"; +import { useState, Dispatch, SetStateAction } from "react"; + +export type OptionProps = { + Icon: any; + title: string; + selected?: string; + setSelected?: Dispatch>; + open: boolean; + notifs?: number; + active?: boolean; +}; + +const Option = ({ Icon, title, selected, setSelected, open, notifs, active }: OptionProps) => { + const [hovered, setHovered] = useState(false); + const isActive = active ?? selected === title; + + return ( + setSelected?.(title)} + onMouseEnter={() => setHovered(true)} + onMouseLeave={() => setHovered(false)} + className={`relative flex h-12 w-full px-3 items-center rounded-xl transition-all duration-200 cursor-pointer group ${ + isActive + ? "bg-gradient-to-r from-emerald-500 to-green-500 text-white shadow-lg shadow-emerald-500/25" + : "text-slate-600 hover:bg-gradient-to-r hover:from-slate-100 hover:to-slate-200/50 hover:text-slate-800" + }`} + whileHover={{ scale: 1.02 }} + whileTap={{ scale: 0.98 }} + > + {/* Active indicator */} + {isActive && ( + + )} + + +
+ +
+
+ + {open && ( + + {title} + + )} + + {/* Tooltip for collapsed state */} + {!open && hovered && ( + +
+ {title} + {/* Tooltip arrow */} +
+
+
+ )} + + {/* Notification badge */} + {notifs && open && ( + + {notifs} + + )} + + {/* Hover effect overlay */} + {hovered && !isActive && ( + + )} +
+ ); +}; + +export default Option; diff --git a/components/landing-page/product.tsx b/components/landing-page/product.tsx new file mode 100644 index 0000000..d841849 --- /dev/null +++ b/components/landing-page/product.tsx @@ -0,0 +1,189 @@ +import Image from "next/image"; +import { Check } from "lucide-react"; + +export default function ProductSection() { + const features = [ + "Content Creation: Producing creative and engaging content such as posts, images, videos, and stories that align with the brand and attract audience attention.", + "Social Media Account Management: Managing business social media accounts, including scheduling posts, monitoring interactions, and engaging with followers.", + "Paid Advertising Campaigns: Designing, executing, and managing paid advertising campaigns on various social media platforms to reach a more specific target audience and improve ROI (Return on Investment).", + ]; + return ( +
+
+ {/* TITLE */} +
+

+ Our Product +

+ +

+ The product we offer is{" "} + + + designed + {" "} + to meet your business needs. +

+
+ + {/* CONTENT */} +
+ {/* LEFT IMAGE */} +
+ Product Illustration +
+ + {/* RIGHT CONTENT */} +
+ {/* ICON */} +
+ Product Icon +
+ +

+ MediaHUB Content Aggregator +

+ +

+ Social media marketing services are provided by companies or + individuals who specialize in marketing strategies through social + media platforms. +

+ + {/* FEATURES */} +
    + {features.map((item) => ( +
  • + + + + {item} +
  • + ))} +
+ + {/* CTA */} + +
+
+
+ {/* LEFT IMAGE */} + + {/* RIGHT CONTENT */} +
+ {/* ICON */} +
+ Product Icon +
+ +

+ Multipool Reputation Management +

+ +

+ Social media marketing services are provided by companies or + individuals who specialize in marketing strategies through social + media platforms. +

+ + {/* FEATURES */} +
    + {features.map((item) => ( +
  • + + + + {item} +
  • + ))} +
+ + {/* CTA */} + +
+
+ Product Illustration +
+
+
+ {/* LEFT IMAGE */} +
+ Product Illustration +
+ + {/* RIGHT CONTENT */} +
+ {/* ICON */} +
+ Product Icon +
+ +

+ PR Room Opinion Management +

+ +

+ Social media marketing services are provided by companies or + individuals who specialize in marketing strategies through social + media platforms. +

+ + {/* FEATURES */} +
    + {features.map((item) => ( +
  • + + + + {item} +
  • + ))} +
+ + {/* CTA */} + +
+
+
+
+ ); +} diff --git a/components/landing-page/retracting-sidedar.tsx b/components/landing-page/retracting-sidedar.tsx new file mode 100644 index 0000000..33fdf2b --- /dev/null +++ b/components/landing-page/retracting-sidedar.tsx @@ -0,0 +1,427 @@ +"use client"; + +import React, { Dispatch, SetStateAction, useState, useEffect } from "react"; +import Image from "next/image"; +import { Icon } from "@iconify/react"; +import Link from "next/link"; +import DashboardContainer from "../main/dashboard/dashboard-container"; +import { usePathname } from "next/navigation"; +import { useTheme } from "../layout/theme-context"; +import { AnimatePresence, motion } from "framer-motion"; +import Option from "./option"; + +interface RetractingSidebarProps { + sidebarData: boolean; + updateSidebarData: (newData: boolean) => void; +} + +const getSidebarByRole = (role: string) => { + if (role === "Admin") { + return [ + { + title: "Dashboard", + items: [ + { + title: "Dashboard", + icon: () => ( + + ), + link: "/admin/dashboard", + }, + ], + }, + ]; + } + + if (role === "Approver" || role === "Kontributor") { + return [ + { + title: "Dashboard", + items: [ + { + title: "Dashboard", + icon: () => ( + + ), + link: "/admin/dashboard", + }, + ], + }, + { + title: "Content Management", + items: [ + { + title: "Articles", + icon: () => , + link: "/admin/article", + }, + ], + }, + ]; + } + + // fallback kalau role tidak dikenal + return []; +}; + +export const RetractingSidebar = ({ + sidebarData, + updateSidebarData, +}: RetractingSidebarProps) => { + const pathname = usePathname(); + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + }, []); + + if (!mounted) { + return null; + } + + return ( + <> + {/* DESKTOP SIDEBAR */} + + + + + + + {/* Desktop Toggle Button - appears when sidebar is collapsed */} + + {!sidebarData && ( + updateSidebarData(true)} + > + + + )} + + + {/* Mobile Toggle Button */} + + {!sidebarData && ( + updateSidebarData(true)} + > + + + )} + + + {/* MOBILE SIDEBAR */} + + {sidebarData && ( + + {/* */} + + + )} + + + ); +}; + +const SidebarContent = ({ + open, + pathname, + updateSidebarData, +}: { + open: boolean; + pathname: string; + updateSidebarData: (newData: boolean) => void; +}) => { + const { theme, toggleTheme } = useTheme(); + + const [username, setUsername] = useState("Guest"); + const [roleName, setRoleName] = useState(""); + + useEffect(() => { + const getCookie = (name: string) => { + const match = document.cookie.match( + new RegExp("(^| )" + name + "=([^;]+)"), + ); + return match ? decodeURIComponent(match[2]) : null; + }; + + const cookieUsername = getCookie("username"); + const cookieRole = getCookie("roleName"); // pastikan nama cookie sesuai + + if (cookieUsername) { + setUsername(cookieUsername); + } + + if (cookieRole) { + setRoleName(cookieRole); + } + }, []); + + const sidebarSections = getSidebarByRole(roleName); + + return ( +
+ {/* SCROLLABLE TOP SECTION */} +
+ {/* HEADER SECTION */} +
+ {/* Logo and Toggle */} +
+ +
+ +
+
+ {open && ( + + + Arah Negeri + + Admin Panel + + )} + + + {open && ( + updateSidebarData(false)} + > + + + )} +
+ + {/* Navigation Sections */} +
+ {sidebarSections.map((section, sectionIndex) => ( + + {open && ( + + {section.title} + + )} +
+ {section.items.map((item, itemIndex) => ( + +
+
+ ))} +
+
+
+ + {/* FIXED BOTTOM SECTION */} +
+ {/* Divider */} + {/*
+
+
*/} + + {/* Theme Toggle */} +
+ + +
+ {theme === "dark" ? ( + + ) : ( + + )} +
+
+ + {open && ( + + {theme === "dark" ? "Light Mode" : "Dark Mode"} + + )} +
+
+ + {/* Settings */} +
+ +
+ + {/* User Profile */} + +
+
+
+ A +
+
+
+ {open && ( + +

+ {username} +

+ +

+ Sign out +

+ +
+ )} +
+
+ + {/* Expand Button for Collapsed State */} + {/* {!open && ( + + + + )} */} +
+
+ ); +}; diff --git a/components/landing-page/service.tsx b/components/landing-page/service.tsx new file mode 100644 index 0000000..8000805 --- /dev/null +++ b/components/landing-page/service.tsx @@ -0,0 +1,189 @@ +import Image from "next/image"; + +export default function ServiceSection() { + return ( +
+
+ {/* Heading */} +
+

+ Our Services +

+

+ Innovative solutions for your{" "} + + business growth + + + . +

+
+ + {/* Service 1 */} +
+ {/* Image */} +
+ Artifintel Soundworks +
+ + {/* Content */} +
+

+ Artifintel +

+

+ Artifintel Soundworks adalah pionir musik AI yang menghadirkan + karya dan kolaborasi dari musisi AI penuh kreativitas dan emosi. + Sejak 2024, Artifintel telah merilis belasan hingga puluhan lagu + dengan melodi memukau dan ritme inovatif. +

+ +
    +
  • ✔ AI Music Composition & Songwriting
  • +
  • ✔ Vocal Synthesis & AI Musicians
  • +
  • ✔ Genre Exploration & Sound Innovation
  • +
  • ✔ AI Collaboration & Creative Experimentation
  • +
  • ✔ Music Release & Digital Distribution
  • +
+
+
+ + {/* Service 2 */} +
+ {/* Content */} +
+

+ Produksi Video Animasi +

+

+ Professional animation production services that bring your ideas + to life. From explainer videos to brand storytelling, we create + engaging animated content that resonates with your audience. +

+ +
    +
  • ✔ 2D & 3D Animation Production
  • +
  • ✔ Motion Graphics & Visual Effects
  • +
  • ✔ Character Design & Storyboarding
  • +
+
+ + {/* Image */} +
+ Animasee +
+
+
+ {/* Image */} +
+ Artifintel Soundworks +
+ + {/* Content */} +
+

+ Reelithic +

+

+ Artifintel Soundworks adalah pionir musik AI yang menghadirkan + karya dan kolaborasi dari musisi AI penuh kreativitas dan emosi. + Sejak 2024, Artifintel telah merilis belasan hingga puluhan lagu + dengan melodi memukau dan ritme inovatif. +

+ +
    +
  • ✔ AI Music Composition & Songwriting
  • +
  • ✔ Vocal Synthesis & AI Musicians
  • +
  • ✔ Genre Exploration & Sound Innovation
  • +
  • ✔ AI Collaboration & Creative Experimentation
  • +
  • ✔ Music Release & Digital Distribution
  • +
+
+
+ + {/* Service 3 */} +
+ {/* Content */} +
+

+ Qudoin +

+

+ Professional animation production services that bring your ideas + to life. From explainer videos to brand storytelling, we create + engaging animated content that resonates with your audience. +

+ +
    +
  • ✔ 2D & 3D Animation Production
  • +
  • ✔ Motion Graphics & Visual Effects
  • +
  • ✔ Character Design & Storyboarding
  • +
+
+ + {/* Image */} +
+ Animasee +
+
+
+ {/* Image */} +
+ Artifintel Soundworks +
+ + {/* Content */} +
+

+ Talkshow AI +

+

+ Artifintel Soundworks adalah pionir musik AI yang menghadirkan + karya dan kolaborasi dari musisi AI penuh kreativitas dan emosi. + Sejak 2024, Artifintel telah merilis belasan hingga puluhan lagu + dengan melodi memukau dan ritme inovatif. +

+ +
    +
  • ✔ AI Music Composition & Songwriting
  • +
  • ✔ Vocal Synthesis & AI Musicians
  • +
  • ✔ Genre Exploration & Sound Innovation
  • +
  • ✔ AI Collaboration & Creative Experimentation
  • +
  • ✔ Music Release & Digital Distribution
  • +
+
+
+
+
+ ); +} diff --git a/components/landing-page/technology.tsx b/components/landing-page/technology.tsx new file mode 100644 index 0000000..637b7db --- /dev/null +++ b/components/landing-page/technology.tsx @@ -0,0 +1,44 @@ +import Image from "next/image"; + +const technologies = [ + { name: "Tableau", src: "/image/tableu.png" }, + { name: "TVU Networks", src: "/image/tvu.png" }, + { name: "AWS", src: "/image/aws.png" }, + { name: "Dell", src: "/image/dell.png" }, + { name: "Zenlayer", src: "/image/zen.png" }, + { name: "Ui", src: "/image/uipath.png" }, +]; + +export default function Technology() { + return ( +
+
+ {/* Title */} +

+ TECHNOLOGY PARTNERS +

+ + {/* Slider */} +
+
+ {/* duplicated for seamless loop */} + {[...technologies, ...technologies].map((tech, index) => ( +
+ {tech.name} +
+ ))} +
+
+
+
+ ); +} diff --git a/components/layout/admin-layout.tsx b/components/layout/admin-layout.tsx new file mode 100644 index 0000000..e3ce929 --- /dev/null +++ b/components/layout/admin-layout.tsx @@ -0,0 +1,89 @@ +"use client"; + +import { useEffect, useState } from "react"; +import React, { ReactNode } from "react"; +import { SidebarProvider } from "./sidebar-context"; +import { ThemeProvider } from "./theme-context"; +import { Breadcrumbs } from "./breadcrumbs"; +import { BurgerButtonIcon } from "../icons"; + +import { motion, AnimatePresence } from "framer-motion"; +import { RetractingSidebar } from "../landing-page/retracting-sidedar"; + +export const AdminLayout = ({ children }: { children: ReactNode }) => { + const [isOpen, setIsOpen] = useState(true); + const [hasMounted, setHasMounted] = useState(false); + + const updateSidebarData = (newData: boolean) => { + setIsOpen(newData); + }; + + // Hooks + useEffect(() => { + setHasMounted(true); + }, []); + + // Render loading state until mounted + if (!hasMounted) { + return ( +
+
+
+ ); + } + + return ( + + +
+
+ + + + + {/* Header */} + +
+
+ + +
+
+
+ + {/* Main Content */} + +
{children}
+
+
+
+
+
+
+
+ ); +}; diff --git a/components/layout/breadcrumbs.tsx b/components/layout/breadcrumbs.tsx new file mode 100644 index 0000000..0d43a17 --- /dev/null +++ b/components/layout/breadcrumbs.tsx @@ -0,0 +1,154 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { usePathname, useRouter } from "next/navigation"; +import { + ArticleIcon, + DashboardIcon, + MagazineIcon, + MasterCategoryIcon, + MasterRoleIcon, + MasterUsersIcon, + StaticPageIcon, +} from "../icons/sidebar-icon"; +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbSeparator, +} from "../ui/breadcrumb"; +import React from "react"; +import { motion } from "framer-motion"; + +export const Breadcrumbs = () => { + const [currentPage, setCurrentPage] = useState(""); + const [mounted, setMounted] = useState(false); + const router = useRouter(); + const pathname = usePathname(); + const pathnameSplit = pathname.split("/"); + + pathnameSplit.shift(); + const pathnameTransformed = pathnameSplit.map((item) => { + const words = item.split("-"); + const capitalizedWords = words.map( + (word) => word.charAt(0).toUpperCase() + word.slice(1), + ); + return capitalizedWords.join(" "); + }); + + useEffect(() => { + setMounted(true); + }, []); + + useEffect(() => { + setCurrentPage(pathnameSplit[pathnameSplit.length - 1]); + }, [pathnameSplit]); + + const handleAction = (key: any) => { + const keyIndex = pathnameSplit.indexOf(key); + const combinedPath = pathnameSplit.slice(0, keyIndex + 1).join("/"); + router.push("/" + combinedPath); + }; + + const getPageIcon = () => { + if (pathname.includes("dashboard")) return ; + if (pathname.includes("article")) return ; + if (pathname.includes("master-category")) + return ; + if (pathname.includes("magazine")) return ; + if (pathname.includes("static-page")) return ; + if (pathname.includes("master-user")) return ; + if (pathname.includes("master-role")) return ; + return null; + }; + + if (!mounted) { + return ( +
+
+
+
+
+
+
+ ); + } + + return ( + + {/* Page Icon */} + + {getPageIcon()} + + + {/* Page Title and Breadcrumbs */} +
+ + {pathnameTransformed[pathnameTransformed.length - 1]} + + + + + + {pathnameTransformed + ?.filter((item) => item !== "Admin") + .map((item, index, array) => ( + + + handleAction(pathnameSplit[index])} + className={`text-sm transition-all duration-200 hover:text-blue-600 ${ + pathnameSplit[index] === currentPage + ? "font-semibold text-blue-600" + : "text-slate-500 hover:text-slate-700" + }`} + > + {item} + + + {index < array.length - 1 && ( + + + + + + )} + + ))} + + + +
+
+ ); +}; diff --git a/components/layout/chunk-error-boundary.tsx b/components/layout/chunk-error-boundary.tsx new file mode 100644 index 0000000..fda4d6f --- /dev/null +++ b/components/layout/chunk-error-boundary.tsx @@ -0,0 +1,104 @@ +"use client"; + +import React, { Component, ErrorInfo, ReactNode } from 'react'; +import { Button } from '@/components/ui/button'; +import { RefreshCw } from 'lucide-react'; + +interface Props { + children: ReactNode; + fallback?: ReactNode; +} + +interface State { + hasError: boolean; + error?: Error; +} + +class ChunkErrorBoundary extends Component { + constructor(props: Props) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError(error: Error): State { + // Check if it's a chunk loading error + if (error.name === 'ChunkLoadError' || error.message.includes('Loading chunk')) { + return { hasError: true, error }; + } + return { hasError: false }; + } + + componentDidCatch(error: Error, errorInfo: ErrorInfo) { + console.error('Chunk loading error:', error, errorInfo); + + // If it's a chunk loading error, try to reload the page + if (error.name === 'ChunkLoadError' || error.message.includes('Loading chunk')) { + this.setState({ hasError: true, error }); + } + } + + handleRetry = () => { + // Clear the error state and reload the page + this.setState({ hasError: false, error: undefined }); + window.location.reload(); + }; + + render() { + if (this.state.hasError) { + if (this.props.fallback) { + return this.props.fallback; + } + + return ( +
+
+
+
+ +
+

+ Chunk Loading Error +

+

+ There was an issue loading a part of the application. This usually happens when the application has been updated. +

+
+ +
+ + + +
+ + {process.env.NODE_ENV === 'development' && this.state.error && ( +
+ + Error Details (Development) + +
+                  {this.state.error.message}
+                
+
+ )} +
+
+ ); + } + + return this.props.children; + } +} + +export default ChunkErrorBoundary; \ No newline at end of file diff --git a/components/layout/circular-progress.tsx b/components/layout/circular-progress.tsx new file mode 100644 index 0000000..82ffbba --- /dev/null +++ b/components/layout/circular-progress.tsx @@ -0,0 +1,36 @@ +import React from "react"; + +interface CircularProgressProps { + size?: number; + strokeWidth?: number; + value: number; // 0 to 100 + className?: string; +} + +export function CircularProgress({ size = 48, strokeWidth = 4, value, className }: CircularProgressProps) { + const radius = (size - strokeWidth) / 2; + const circumference = 2 * Math.PI * radius; + const offset = circumference - (value / 100) * circumference; + + return ( + + + + + {Math.round(value)}% + + + ); +} diff --git a/components/layout/costum-circular-progress.tsx b/components/layout/costum-circular-progress.tsx new file mode 100644 index 0000000..bb4f04f --- /dev/null +++ b/components/layout/costum-circular-progress.tsx @@ -0,0 +1,54 @@ +import React from "react"; + +type CircularProgressProps = { + value: number; // antara 0 - 100 + size?: number; // diameter lingkaran (px) + strokeWidth?: number; + color?: string; + bgColor?: string; + label?: string; +}; + +export const CustomCircularProgress = ({ + value, + size = 80, + strokeWidth = 8, + color = "#f59e0b", // shadcn's warning color + bgColor = "#e5e7eb", // gray-200 + label, +}: CircularProgressProps) => { + const radius = (size - strokeWidth) / 2; + const circumference = 2 * Math.PI * radius; + const progress = Math.min(Math.max(value, 0), 100); // jaga antara 0 - 100 + const offset = circumference - (progress / 100) * circumference; + + return ( +
+ + + + + + {label ?? `${Math.round(progress)}%`} + +
+ ); +}; diff --git a/components/layout/custom-pagination.tsx b/components/layout/custom-pagination.tsx new file mode 100644 index 0000000..80dd848 --- /dev/null +++ b/components/layout/custom-pagination.tsx @@ -0,0 +1,125 @@ +"use client"; +import { + Pagination, + PaginationContent, + PaginationEllipsis, + PaginationItem, + PaginationLink, + PaginationNext, + PaginationPrevious, +} from "@/components/ui/pagination"; + +import { useEffect, useState } from "react"; + +export default function CustomPagination(props: { + totalPage: number; + onPageChange: (data: number) => void; +}) { + const { totalPage, onPageChange } = props; + const [page, setPage] = useState(1); + + useEffect(() => { + onPageChange(page); + }, [page]); + + const renderPageNumbers = () => { + const pageNumbers = []; + const halfWindow = Math.floor(5 / 2); + let startPage = Math.max(2, page - halfWindow); + let endPage = Math.min(totalPage - 1, page + halfWindow); + + if (endPage - startPage + 1 < 5) { + if (page <= halfWindow) { + endPage = Math.min( + totalPage - 1, + endPage + (5 - (endPage - startPage + 1)) + ); + } else if (page + halfWindow >= totalPage) { + startPage = Math.max(2, startPage - (5 - (endPage - startPage + 1))); + } + } + + for (let i = startPage; i <= endPage; i++) { + pageNumbers.push( + setPage(i)}> + + {i} + + + ); + } + + return pageNumbers; + }; + return ( + + + + (page > 10 ? setPage(page - 10) : "")} + > + {/* */} + + + + (page > 1 ? setPage(page - 1) : "")} + /> + + + setPage(1)} + isActive={page === 1} + > + {1} + + + {page > 4 && ( + + setPage(page - 1)} + /> + + )} + {renderPageNumbers()} + {page < totalPage - 3 && ( + + setPage(page + 1)} + /> + + )} + {totalPage > 1 && ( + + setPage(totalPage)} + isActive={page === totalPage} + > + {totalPage} + + + )} + + + (page < totalPage ? setPage(page + 1) : "")} + /> + + + (page < totalPage - 10 ? setPage(page + 10) : "")} + > + {/* */} + + + + + ); +} diff --git a/components/layout/dashboard-admin.tsx b/components/layout/dashboard-admin.tsx new file mode 100644 index 0000000..c49a9b4 --- /dev/null +++ b/components/layout/dashboard-admin.tsx @@ -0,0 +1,59 @@ +const AdminDashboard = () => { + return ( +
+
+

Admin Dashboard

+

Review and manage content submissions

+
+ + {/* STAT CARDS */} +
+ {[ + { + title: "Open Tasks", + value: 2, + color: "bg-yellow-500", + growth: "+3", + }, + { + title: "Closed Tasks", + value: 2, + color: "bg-green-600", + growth: "+5", + }, + { + title: "Total Submissions", + value: 4, + color: "bg-blue-600", + growth: "+12%", + }, + { + title: "Rejected", + value: 7, + color: "bg-red-600", + growth: "-1", + }, + ].map((card, i) => ( +
+
+

{card.title}

+

+ {card.value} +

+
+ +
+

+ {card.growth} +

+
+
+
+ ))} +
+
+ ); +}; diff --git a/components/layout/dashboard-contributor.tsx b/components/layout/dashboard-contributor.tsx new file mode 100644 index 0000000..e69de29 diff --git a/components/layout/sidebar-context.tsx b/components/layout/sidebar-context.tsx new file mode 100644 index 0000000..a566005 --- /dev/null +++ b/components/layout/sidebar-context.tsx @@ -0,0 +1,58 @@ +'use client' +import React, { createContext, useContext, useEffect, useState } from 'react'; + +interface SidebarContextType { + isOpen: boolean; + toggleSidebar: () => void; +} + +const SidebarContext = createContext(undefined); + +export const SidebarProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const [isOpen, setIsOpen] = useState(() => { + if (typeof window !== 'undefined') { + const storedValue = localStorage.getItem('sidebarOpen'); + return storedValue ? JSON.parse(storedValue) : false; + } + }); + + const toggleSidebar = () => { + setIsOpen(!isOpen); + }; + + useEffect(() => { + localStorage.setItem('sidebarOpen', JSON.stringify(isOpen)); + }, [isOpen]); + + useEffect(() => { + const handleResize = () => { + setIsOpen(window.innerWidth > 768); // Ganti 768 dengan lebar yang sesuai dengan breakpoint Anda + }; + + handleResize(); // Pastikan untuk memanggil fungsi handleResize saat komponen dimuat + + window.addEventListener('resize', handleResize); + + return () => { + window.removeEventListener('resize', handleResize); + }; + }, []); + + return ( + + {children} + + ); +}; + +export const useSidebar = () => { + const context = useContext(SidebarContext); + if (!context) { + throw new Error('useSidebar must be used within a SidebarProvider'); + } + return context; +}; + +export const useSidebarContext = () => { + return useContext(SidebarContext); +}; \ No newline at end of file diff --git a/components/layout/theme-context.tsx b/components/layout/theme-context.tsx new file mode 100644 index 0000000..b498a01 --- /dev/null +++ b/components/layout/theme-context.tsx @@ -0,0 +1,67 @@ +"use client"; + +import React, { createContext, useContext, useEffect, useState } from 'react'; + +type Theme = 'light' | 'dark'; + +interface ThemeContextType { + theme: Theme; + toggleTheme: () => void; + setTheme: (theme: Theme) => void; +} + +const ThemeContext = createContext(undefined); + +export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const [theme, setThemeState] = useState('light'); + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + // Get theme from localStorage or default to 'light' + const savedTheme = localStorage.getItem('theme') as Theme; + if (savedTheme) { + setThemeState(savedTheme); + } else { + // Check system preference + const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; + setThemeState(systemTheme); + } + }, []); + + useEffect(() => { + if (!mounted) return; + + // Update document class and localStorage + document.documentElement.classList.remove('light', 'dark'); + document.documentElement.classList.add(theme); + localStorage.setItem('theme', theme); + }, [theme, mounted]); + + const toggleTheme = () => { + setThemeState(prev => prev === 'light' ? 'dark' : 'light'); + }; + + const setTheme = (newTheme: Theme) => { + setThemeState(newTheme); + }; + + // Prevent hydration mismatch + if (!mounted) { + return <>{children}; + } + + return ( + + {children} + + ); +}; + +export const useTheme = () => { + const context = useContext(ThemeContext); + if (context === undefined) { + throw new Error('useTheme must be used within a ThemeProvider'); + } + return context; +}; \ No newline at end of file diff --git a/components/main/dashboard/chart/column-chart.tsx b/components/main/dashboard/chart/column-chart.tsx new file mode 100644 index 0000000..db77733 --- /dev/null +++ b/components/main/dashboard/chart/column-chart.tsx @@ -0,0 +1,157 @@ +"use client"; + +import React, { useEffect, useState } from "react"; + +type WeekData = { + week: number; + days: number[]; + total: number; +}; + +type RemainingDays = { + days: number[]; + total: number; +}; + +function processMonthlyData(count: number[]): { + weeks: WeekData[]; + remaining_days: RemainingDays; +} { + const weeks: WeekData[] = []; + let weekIndex = 1; + + for (let i = 0; i < count.length; i += 7) { + const weekData = count.slice(i, i + 7); + weeks.push({ + week: weekIndex, + days: weekData, + total: weekData.reduce((sum, day) => sum + day, 0), + }); + weekIndex++; + } + + const remainingDays: RemainingDays = { + days: count.length % 7 === 0 ? [] : count.slice(-count.length % 7), + total: count.slice(-count.length % 7).reduce((sum, day) => sum + day, 0), + }; + + return { + weeks, + remaining_days: remainingDays, + }; +} + +const ApexChartColumn = (props: { + type: string; + date: string; + view: string[]; +}) => { + const { date, type, view } = props; + const [categories, setCategories] = useState([]); + const [series, setSeries] = useState<{ name: string; data: number[] }[]>([]); + const [seriesComment, setSeriesComment] = useState([]); + const [seriesView, setSeriesView] = useState([]); + const [seriesShare, setSeriesShare] = useState([]); + + // useEffect(() => { + // initFetch(); + // }, [date, type, view]); + + // const initFetch = async () => { + // const splitDate = date.split(" "); + + // const res = await getStatisticMonthly(splitDate[1]); + // const data = res?.data?.data; + // const getDatas = data?.find( + // (a: any) => + // a.month == Number(splitDate[0]) && a.year === Number(splitDate[1]) + // ); + // if (getDatas) { + // const temp1 = processMonthlyData(getDatas?.comment); + // const temp2 = processMonthlyData(getDatas?.view); + // const temp3 = processMonthlyData(getDatas?.share); + + // if (type == "weekly") { + // setSeriesComment( + // temp1.weeks.map((list) => { + // return list.total; + // }) + // ); + // setSeriesView( + // temp2.weeks.map((list) => { + // return list.total; + // }) + // ); + // setSeriesShare( + // temp3.weeks.map((list) => { + // return list.total; + // }) + // ); + // } else { + // setSeriesComment(getDatas.comment); + // setSeriesView(getDatas.view); + // setSeriesShare(getDatas.share); + // } + // if (type === "weekly") { + // const category = []; + // for (let i = 1; i <= temp1.weeks.length; i++) { + // category.push(`Week ${i}`); + // } + // setCategories(category); + // } + // } else { + // setSeriesComment([]); + // } + // }; + + useEffect(() => { + const temp = [ + { + name: "Comment", + data: view.includes("comment") ? seriesComment : [], + }, + { + name: "View", + data: view.includes("view") ? seriesView : [], + }, + { + name: "Share", + data: view.includes("share") ? seriesShare : [], + }, + ]; + + console.log("temp", temp); + + setSeries(temp); + }, [view, seriesShare, seriesView, seriesComment]); + + return ( +
+
+ {/* */} +
+
+
+ ); +}; + +export default ApexChartColumn; diff --git a/components/main/dashboard/dashboard-container.tsx b/components/main/dashboard/dashboard-container.tsx new file mode 100644 index 0000000..468e23f --- /dev/null +++ b/components/main/dashboard/dashboard-container.tsx @@ -0,0 +1,481 @@ +"use client"; + +import Cookies from "js-cookie"; +import Link from "next/link"; +import { useEffect, useState } from "react"; +import { Article } from "@/types/globals"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion"; +import "react-datepicker/dist/react-datepicker.css"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Label } from "@/components/ui/label"; +import { Calendar } from "@/components/ui/calendar"; +import ApexChartColumn from "@/components/main/dashboard/chart/column-chart"; +import CustomPagination from "@/components/layout/custom-pagination"; +import { motion } from "framer-motion"; +import { Input } from "@/components/ui/input"; +import { + SelectTrigger, + SelectValue, + SelectContent, + SelectItem, + Select, +} from "@/components/ui/select"; +import { Badge } from "lucide-react"; + +type ArticleData = Article & { + no: number; + createdAt: string; +}; + +interface TopPages { + id: number; + no: number; + title: string; + viewCount: number; +} + +interface PostCount { + userLevelId: number; + no: number; + userLevelName: string; + totalArticle: number; +} + +export default function DashboardContainer() { + const [roleName, setRoleName] = useState(); + useEffect(() => { + const role = Cookies.get("roleName"); + setRoleName(role); + }, []); + + const username = Cookies.get("username"); + const fullname = Cookies.get("ufne"); + const [page, setPage] = useState(1); + const [totalPage, setTotalPage] = useState(1); + const [topPagesTotalPage, setTopPagesTotalPage] = useState(1); + const [article, setArticle] = useState([]); + // const [analyticsView, setAnalyticView] = useState(["comment", "view", "share"]); + // const [startDateValue, setStartDateValue] = useState(parseDate(convertDateFormatNoTimeV2(new Date()))); + // const [postContentDate, setPostContentDate] = useState({ + // startDate: parseDate(convertDateFormatNoTimeV2(new Date(new Date().setDate(new Date().getDate() - 7)))), + // endDate: parseDate(convertDateFormatNoTimeV2(new Date())), + // }); + + const [startDateValue, setStartDateValue] = useState(new Date()); + const [analyticsView, setAnalyticView] = useState([]); + const options = [ + { label: "Comment", value: "comment" }, + { label: "View", value: "view" }, + { label: "Share", value: "share" }, + ]; + const handleChange = (value: string, checked: boolean) => { + if (checked) { + setAnalyticView([...analyticsView, value]); + } else { + setAnalyticView(analyticsView.filter((v) => v !== value)); + } + }; + const [postContentDate, setPostContentDate] = useState({ + startDate: new Date(new Date().setDate(new Date().getDate() - 7)), + endDate: new Date(), + }); + + const [typeDate, setTypeDate] = useState("monthly"); + const [summary, setSummary] = useState(); + + const [topPages, setTopPages] = useState([]); + const [postCount, setPostCount] = useState([]); + + // useEffect(() => { + // fetchSummary(); + // }, []); + + // useEffect(() => { + // initState(); + // }, [page]); + + // async function initState() { + // const req = { + // limit: "5", + // page: page, + // search: "", + // }; + // const res = await getListArticle(req); + // setArticle(res.data?.data); + // setTotalPage(res?.data?.meta?.totalPage); + // } + + // async function fetchSummary() { + // const res = await getStatisticSummary(); + // setSummary(res?.data?.data); + // } + + // useEffect(() => { + // fetchTopPages(); + // }, [page]); + + // async function fetchTopPages() { + // const req = { + // limit: "10", + // page: page, + // search: "", + // }; + // const res = await getTopArticles(req); + // setTopPages(getTableNumber(10, res.data?.data)); + // setTopPagesTotalPage(res?.data?.meta?.totalPage); + // } + + // useEffect(() => { + // fetchPostCount(); + // }, [postContentDate]); + // async function fetchPostCount() { + // const getDate = (data: any) => { + // return `${data.year}-${data.month < 10 ? `0${data.month}` : data.month}-${ + // data.day < 10 ? `0${data.day}` : data.day + // }`; + // }; + // const res = await getUserLevelDataStat( + // getDate(postContentDate.startDate), + // getDate(postContentDate.endDate) + // ); + // setPostCount(getTableNumber(10, res?.data?.data)); + // } + + const getTableNumber = (limit: number, data: any) => { + if (data) { + const startIndex = limit * (page - 1); + let iterate = 0; + const newData = data.map((value: any) => { + iterate++; + value.no = startIndex + iterate; + return value; + }); + return newData; + } + }; + + const getMonthYear = (date: any) => { + return date.month + " " + date.year; + }; + const getMonthYearName = (date: any) => { + const newDate = new Date(date); + + const months = [ + "Januari", + "Februari", + "Maret", + "April", + "Mei", + "Juni", + "Juli", + "Agustus", + "September", + "Oktober", + "November", + "Desember", + ]; + const year = newDate.getFullYear(); + + const month = months[newDate.getMonth()]; + return month + " " + year; + }; + + if (!roleName) return null; + const AdminDashboard = () => { + const tasks = [ + { + id: "1", + title: "MediaHUB Content Aggregator", + author: "John Kontributor", + category: "Product", + date: "2026-02-13", + status: "OPEN", + }, + { + id: "2", + title: + "Mudik Nyaman Bersama Pertamina: Layanan 24 Jam, Motoris, dan Fasilitas Lengkap", + author: "Jane Kontributor", + category: "Service", + date: "2026-02-13", + status: "OPEN", + }, + { + id: "3", + title: "Artifintel Services Update", + author: "Alex Approver", + category: "Event", + date: "2026-02-13", + status: "CLOSED", + }, + ]; + + return ( +
+ {/* HEADER */} +
+

Admin Dashboard

+

+ Review and manage content submissions +

+
+ + {/* STAT CARDS */} +
+ {[ + { title: "Open Tasks", value: 2, color: "bg-yellow-500" }, + { title: "Closed Tasks", value: 2, color: "bg-green-600" }, + { title: "Total Submissions", value: 4, color: "bg-blue-600" }, + { title: "Rejected", value: 7, color: "bg-red-600" }, + ].map((card, i) => ( +
+
+

{card.title}

+

+ {card.value} +

+
+
+
+ ))} +
+ + {/* TASK LIST */} +
+ {/* Title */} +
+

+ Task List{" "} + + {tasks.length} Tasks + +

+
+ + {/* Filters */} +
+ + + +
+ + {/* Accordion List */} + + {tasks.map((task) => ( + + +
+
+

{task.title}

+

+ {task.author} • {task.category} • {task.date} +

+
+ +
+ + {task.status} + + + {/* Mini Progress */} +
+
+
+
+
+
+
+
+ + + +
+ {/* Title */} +
+
+
+
+

+ Document Flow Status +

+
+ + {/* Stepper */} +
+ {/* Line */} +
+ +
+ {/* STEP 1 */} +
+
+ ✓ +
+

+ Submission +

+

+ Feb 13, 2026 09:00 +

+
+ + {/* STEP 2 */} +
+
+ ● +
+

+ Technical Review +

+

+ Feb 13, 2026 11:00 +

+
+ + {/* STEP 3 */} +
+
+

+ Admin Verification +

+

Waiting

+
+ + {/* STEP 4 */} +
+
+

+ Final Approval +

+

Waiting

+
+
+
+
+ + + ))} + +
+
+ ); + }; + + const ContributorDashboard = () => { + return ( +
+
+

Dashboard

+
+ + {/* STAT CARDS */} +
+ {[ + { + title: "Total Content", + value: 24, + color: "bg-blue-600", + growth: "+12%", + }, + { + title: "Pending Approval", + value: 8, + color: "bg-yellow-500", + growth: "+3", + }, + { + title: "Published", + value: 16, + color: "bg-green-600", + growth: "+5", + }, + { + title: "Rejected", + value: 2, + color: "bg-red-600", + growth: "-1", + }, + ].map((card, i) => ( +
+
+

{card.title}

+

+ {card.value} +

+
+ +
+

+ {card.growth} +

+
+
+
+ ))} +
+ + {/* CONTENT + QUICK ACTIONS */} +
+
+

Recent Content

+ {/* isi list content di sini */} +
+ +
+

Quick Actions

+ + + + + + +
+
+
+ ); + }; + + return ( + <>{roleName === "Admin" ? : } + ); +} diff --git a/components/ui/accordion.tsx b/components/ui/accordion.tsx new file mode 100644 index 0000000..aa972da --- /dev/null +++ b/components/ui/accordion.tsx @@ -0,0 +1,66 @@ +"use client" + +import * as React from "react" +import { ChevronDownIcon } from "lucide-react" +import { Accordion as AccordionPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" + +function Accordion({ + ...props +}: React.ComponentProps) { + return +} + +function AccordionItem({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AccordionTrigger({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + svg]:rotate-180", + className + )} + {...props} + > + {children} + + + + ) +} + +function AccordionContent({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + +
{children}
+
+ ) +} + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/components/ui/badge.tsx b/components/ui/badge.tsx new file mode 100644 index 0000000..83750ed --- /dev/null +++ b/components/ui/badge.tsx @@ -0,0 +1,46 @@ +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "@/lib/utils"; + +const badgeVariants = cva( + "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", + secondary: + "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", + destructive: + "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +); + +function Badge({ + className, + variant, + asChild = false, + ...props +}: React.ComponentProps<"span"> & + VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot : "span"; + + return ( + + ); +} + +export { Badge, badgeVariants }; diff --git a/components/ui/breadcrumb.tsx b/components/ui/breadcrumb.tsx new file mode 100644 index 0000000..eb88f32 --- /dev/null +++ b/components/ui/breadcrumb.tsx @@ -0,0 +1,109 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { ChevronRight, MoreHorizontal } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Breadcrumb({ ...props }: React.ComponentProps<"nav">) { + return