Initial commit

This commit is contained in:
Anang Yusman 2026-02-17 17:05:22 +08:00
commit 1cdb0cd086
130 changed files with 18969 additions and 0 deletions

41
.gitignore vendored Normal file
View File

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

36
README.md Normal file
View File

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

View File

@ -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 (
<div className="h-full overflow-auto bg-gradient-to-br from-slate-50/50 via-white to-slate-50/50 flex items-center justify-center">
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-blue-500"></div>
</div>
);
}
return (
<motion.div
className="h-full overflow-auto bg-gradient-to-br from-slate-50/50 via-white to-slate-50/50"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3 }}
>
<div className="p-6">
<DashboardContainer />
</div>
</motion.div>
);
}

View File

@ -0,0 +1,7 @@
"use client";
import { AdminLayout } from "@/components/layout/admin-layout";
export default function AdminPageLayout({ children }: { children: React.ReactNode }) {
return <AdminLayout>{children}</AdminLayout>;
}

85
app/audio/filter/page.tsx Normal file
View File

@ -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 (
<div className="relative min-h-screen bg-white font-[family-name:var(--font-geist-sans)]">
<section className=" min-h-screen py-10">
<div className="container mx-auto px-6">
{/* ===== TOP BAR ===== */}
{/* ===== CONTENT ===== */}
<div className="grid grid-cols-1 lg:grid-cols-[280px_1fr] gap-8">
{/* Sidebar */}
<div className="hidden lg:block">
<FilterAudioSidebar />
</div>
{/* Mobile Sidebar */}
{openFilter && (
<div className="fixed inset-0 bg-black/40 z-50">
<div className="absolute left-0 top-0 h-full w-[280px] bg-white p-6 overflow-y-auto">
<FilterAudioSidebar />
<button
onClick={() => setOpenFilter(false)}
className="mt-4 text-sm text-[#966314]"
>
Tutup
</button>
</div>
</div>
)}
{/* Cards */}
<div>
<div className="flex items-center justify-between mb-8">
<div className="text-sm text-gray-500">
Audio &nbsp; &gt; &nbsp;
<span className="font-semibold text-black">Lihat Semua</span>
<span className="font-semibold text-black ml-3">{"|"}</span>
<span className="ml-4 text-gray-400">
Terdapat 1636 berita
</span>
</div>
<div className="flex items-center gap-4">
<span className="text-sm">Urutkan:</span>
<select className="border rounded-md px-3 py-2 text-sm mr-20">
<option>Terpopuler</option>
<option>Terbaru</option>
</select>
<FloatingMenuNews />
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
{Array.from({ length: 6 }).map((_, i) => (
<AudioCard key={i} />
))}
</div>
</div>
</div>
{/* ===== PAGINATION ===== */}
<div className="flex justify-center items-center gap-3 mt-12 text-sm">
<button className="px-3 py-1 bg-black text-white rounded">1</button>
<button className="px-3 py-1 bg-white border rounded">2</button>
<span>...</span>
<button className="px-3 py-1 bg-white border rounded">4</button>
<button className="ml-4">Selanjutnya &gt;</button>
</div>
</div>
</section>
<Footer />
</div>
);
}

7
app/auth/layout.tsx Normal file
View File

@ -0,0 +1,7 @@
export default function AuthLayout({
children,
}: {
children: React.ReactNode;
}) {
return <> {children}</>;
}

9
app/auth/page.tsx Normal file
View File

@ -0,0 +1,9 @@
import Login from "@/components/form/login";
export default function AuthPage() {
return (
<>
<Login />
</>
);
}

View File

@ -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 (
<div className="min-h-screen bg-white">
<div className="max-w-7xl mx-auto px-6 py-10">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* LEFT */}
<div className="lg:col-span-2">
{type === "video" && <VideoPlayerSection />}
{type === "image" && <ImageDetailSection />}
{type === "text" && <DocumentDetailSection />}
{type === "audio" && <AudioPlayerSection />}
</div>
{/* RIGHT */}
<div className="lg:col-span-1">
{type === "video" && <VideoSidebar />}
{type === "image" && <ImageSidebar />}
{type === "text" && <DocumentSidebar />}
{type === "audio" && <AudioSidebar />}
</div>
<FloatingMenuNews />
</div>
</div>
<Footer />
</div>
);
}

View File

@ -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 (
<div className="relative min-h-screen bg-white font-[family-name:var(--font-geist-sans)]">
<section className=" min-h-screen py-10">
<div className="container mx-auto px-6">
{/* ===== TOP BAR ===== */}
{/* ===== CONTENT ===== */}
<div className="grid grid-cols-1 lg:grid-cols-[280px_1fr] gap-8">
{/* Sidebar */}
<div className="hidden lg:block">
<FilterDocumentSidebar />
</div>
{/* Mobile Sidebar */}
{openFilter && (
<div className="fixed inset-0 bg-black/40 z-50">
<div className="absolute left-0 top-0 h-full w-[280px] bg-white p-6 overflow-y-auto">
<FilterDocumentSidebar />
<button
onClick={() => setOpenFilter(false)}
className="mt-4 text-sm text-[#966314]"
>
Tutup
</button>
</div>
</div>
)}
{/* Cards */}
<div>
<div className="flex items-center justify-between mb-8">
<div className="text-sm text-gray-500">
Document &nbsp; &gt; &nbsp;
<span className="font-semibold text-black">Lihat Semua</span>
<span className="font-semibold text-black ml-3">{"|"}</span>
<span className="ml-4 text-gray-400">
Terdapat 1636 berita
</span>
</div>
<div className="flex items-center gap-4">
<span className="text-sm">Urutkan:</span>
<select className="border rounded-md px-3 py-2 text-sm mr-20">
<option>Terpopuler</option>
<option>Terbaru</option>
</select>
<FloatingMenuNews />
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
{Array.from({ length: 6 }).map((_, i) => (
<DocumentCard key={i} />
))}
</div>
</div>
</div>
{/* ===== PAGINATION ===== */}
<div className="flex justify-center items-center gap-3 mt-12 text-sm">
<button className="px-3 py-1 bg-black text-white rounded">1</button>
<button className="px-3 py-1 bg-white border rounded">2</button>
<span>...</span>
<button className="px-3 py-1 bg-white border rounded">4</button>
<button className="ml-4">Selanjutnya &gt;</button>
</div>
</div>
</section>
<Footer />
</div>
);
}

BIN
app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

140
app/globals.css Normal file
View File

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

87
app/image/filter/page.tsx Normal file
View File

@ -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 (
<div className="relative min-h-screen bg-white font-[family-name:var(--font-geist-sans)]">
<section className=" min-h-screen py-10">
<div className="container mx-auto px-6">
{/* ===== TOP BAR ===== */}
{/* ===== CONTENT ===== */}
<div className="grid grid-cols-1 lg:grid-cols-[280px_1fr] gap-8">
{/* Sidebar */}
<div className="hidden lg:block">
<FilterImageSidebar />
</div>
{/* Mobile Sidebar */}
{openFilter && (
<div className="fixed inset-0 bg-black/40 z-50">
<div className="absolute left-0 top-0 h-full w-[280px] bg-white p-6 overflow-y-auto">
<FilterImageSidebar />
<button
onClick={() => setOpenFilter(false)}
className="mt-4 text-sm text-[#966314]"
>
Tutup
</button>
</div>
</div>
)}
{/* Cards */}
<div>
<div className="flex items-center justify-between mb-8">
<div className="text-sm text-gray-500">
Foto &nbsp; &gt; &nbsp;
<span className="font-semibold text-black">Lihat Semua</span>
<span className="font-semibold text-black ml-3">{"|"}</span>
<span className="ml-4 text-gray-400">
Terdapat 1636 berita
</span>
</div>
<div className="flex items-center gap-4">
<span className="text-sm">Urutkan:</span>
<select className="border rounded-md px-3 py-2 text-sm mr-20">
<option>Terpopuler</option>
<option>Terbaru</option>
</select>
<FloatingMenuNews />
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
{Array.from({ length: 6 }).map((_, i) => (
<ImageCard key={i} />
))}
</div>
</div>
</div>
{/* ===== PAGINATION ===== */}
<div className="flex justify-center items-center gap-3 mt-12 text-sm">
<button className="px-3 py-1 bg-black text-white rounded">1</button>
<button className="px-3 py-1 bg-white border rounded">2</button>
<span>...</span>
<button className="px-3 py-1 bg-white border rounded">4</button>
<button className="ml-4">Selanjutnya &gt;</button>
</div>
</div>
</section>
<Footer />
</div>
);
}

34
app/layout.tsx Normal file
View File

@ -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 (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
</body>
</html>
);
}

View File

@ -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 (
<div className="relative min-h-screen bg-white">
<FloatingMenuNews />
<NewsAndServicesHeader />
<ContentLatest />
<ContentPopular />
<ContentCategory />
<Footer />
</div>
);
}

24
app/page.tsx Normal file
View File

@ -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 (
<div className="relative min-h-screen bg-white font-[family-name:var(--font-geist-sans)]">
{/* FIXED MENU */}
<FloatingMenu />
{/* PAGE CONTENT */}
<Header />
<AboutSection />
<ProductSection />
<ServiceSection />
<Technology />
<Footer />
</div>
);
}

83
app/video/filter/page.tsx Normal file
View File

@ -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 (
<div className="relative min-h-screen bg-white font-[family-name:var(--font-geist-sans)]">
<section className=" min-h-screen py-10">
<div className="container mx-auto px-6">
{/* ===== TOP BAR ===== */}
{/* ===== CONTENT ===== */}
<div className="grid grid-cols-1 lg:grid-cols-[280px_1fr] gap-8">
{/* Sidebar */}
<div className="hidden lg:block">
<FilterSidebar />
</div>
{/* Mobile Sidebar */}
{openFilter && (
<div className="fixed inset-0 bg-black/40 z-50">
<div className="absolute left-0 top-0 h-full w-[280px] bg-white p-6 overflow-y-auto">
<FilterSidebar />
<button
onClick={() => setOpenFilter(false)}
className="mt-4 text-sm text-[#966314]"
>
Tutup
</button>
</div>
</div>
)}
{/* Cards */}
<div>
<div className="flex items-center justify-between mb-8">
<div className="text-sm text-gray-500">
Audio Visual &nbsp; &gt; &nbsp;
<span className="font-semibold text-black">Lihat Semua</span>
<span className="font-semibold text-black ml-3">{"|"}</span>
<span className="ml-4 text-gray-400">
Terdapat 1636 berita
</span>
</div>
<div className="flex items-center gap-4">
<span className="text-sm">Urutkan:</span>
<select className="border rounded-md px-3 py-2 text-sm mr-20">
<option>Terpopuler</option>
<option>Terbaru</option>
</select>
<FloatingMenuNews />
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
{Array.from({ length: 6 }).map((_, i) => (
<VideoCard key={i} />
))}
</div>
</div>
</div>
{/* ===== PAGINATION ===== */}
<div className="flex justify-center items-center gap-3 mt-12 text-sm">
<button className="px-3 py-1 bg-black text-white rounded">1</button>
<button className="px-3 py-1 bg-white border rounded">2</button>
<span>...</span>
<button className="px-3 py-1 bg-white border rounded">4</button>
<button className="ml-4">Selanjutnya &gt;</button>
</div>
</div>
</section>
<Footer />
</div>
);
}

22
components.json Normal file
View File

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

View File

@ -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 (
<Link href={`/details/${slug}?type=audio`}>
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm hover:shadow-md transition duration-300 overflow-hidden">
{/* IMAGE */}
<div className="relative h-[200px] w-full">
<Image
src="/image/audio.png"
alt="news"
fill
className="object-cover"
/>
</div>
{/* CONTENT */}
<div className="p-5 space-y-3">
{/* BADGE + TAG */}
<div className="flex items-center gap-2 text-xs">
<span className="bg-red-600 text-white px-2 py-[3px] rounded-md font-medium">
POLRI
</span>
<span className="text-gray-500 uppercase tracking-wide">
SEPUTAR PRESTASI
</span>
</div>
{/* DATE */}
<p className="text-xs text-gray-400">02 Februari 2024</p>
{/* TITLE */}
<h3 className="font-semibold text-[15px] leading-snug line-clamp-2 hover:text-[#966314] transition">
Bharatu Mardi Hadji Gugur Saat Bertugas, Diganjar Kenaikan Pangkat
Luar Biasa
</h3>
{/* EXCERPT */}
<p className="text-sm text-gray-500 line-clamp-2 leading-relaxed">
Jakarta - Kapolri Jenderal Polisi Drs. Listyo Sigit Prabowo
memberikan kenaikan pangkat luar biasa anumerta kepada...
</p>
</div>
</div>
</Link>
);
}

View File

@ -0,0 +1,99 @@
"use client";
import { ChevronLeft } from "lucide-react";
export default function FilterAudioSidebar() {
return (
<div className="bg-white rounded-2xl border border-gray-200 shadow-sm p-6">
{/* HEADER */}
<div className="flex items-center justify-between pb-4 border-b">
<h3 className="font-semibold text-sm flex items-center gap-2">
Filter
</h3>
<ChevronLeft size={16} className="text-gray-400" />
</div>
{/* CONTENT */}
<div className="space-y-6 mt-6">
{/* KATEGORI */}
<FilterSection title="Kategori">
<Checkbox label="Semua" count={1203} defaultChecked />
<Checkbox label="Berita Terhangat" count={123} />
<Checkbox label="Tentang Teknologi" count={24} />
<Checkbox label="Bersama Pelanggan" count={42} />
<Checkbox label="Pembicara Ahli" count={224} />
</FilterSection>
<Divider />
{/* JENIS FILE */}
<FilterSection title="Jenis File">
<Checkbox label="Semua" count={78} />
<Checkbox label="Audio Visual" count={120} />
<Checkbox label="Audio" count={34} defaultChecked />
<Checkbox label="Foto" count={234} />
<Checkbox label="Teks" count={9} />
</FilterSection>
<Divider />
{/* FORMAT */}
<FilterSection title="Format Audio ">
<Checkbox label="Semua" count={2} defaultChecked />
</FilterSection>
{/* RESET */}
<div className="text-center pt-4">
<button className="text-sm text-[#966314] font-medium hover:underline">
Reset Filter
</button>
</div>
</div>
</div>
);
}
/* ===== COMPONENTS ===== */
function FilterSection({
title,
children,
}: {
title: string;
children: React.ReactNode;
}) {
return (
<div>
<p className="text-sm font-medium mb-3">{title}</p>
<div className="space-y-2">{children}</div>
</div>
);
}
function Checkbox({
label,
count,
defaultChecked,
}: {
label: string;
count: number;
defaultChecked?: boolean;
}) {
return (
<label className="flex items-center justify-between text-sm cursor-pointer">
<div className="flex items-center gap-2">
<input
type="checkbox"
defaultChecked={defaultChecked}
className="h-4 w-4 accent-[#966314]"
/>
<span>{label}</span>
</div>
<span className="text-gray-400">({count})</span>
</label>
);
}
function Divider() {
return <div className="border-t border-gray-200"></div>;
}

View File

@ -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 (
<div className="space-y-6">
{/* ===== AUDIO PLAYER CARD ===== */}
<div className="bg-gray-50 rounded-2xl p-6 border border-gray-200">
<div className="flex items-center gap-6">
{/* PLAY BUTTON */}
<button
onClick={() => setPlaying(!playing)}
className="h-16 w-16 rounded-full bg-yellow-400 flex items-center justify-center shadow-md hover:scale-105 transition"
>
{playing ? (
<Pause size={28} className="text-black" />
) : (
<Play size={28} className="text-black ml-1" />
)}
</button>
{/* WAVEFORM + DURATION */}
<div className="flex-1">
{/* FAKE WAVEFORM */}
<div className="h-16 flex items-center gap-[3px]">
{Array.from({ length: 70 }).map((_, i) => (
<div
key={i}
className={`w-[3px] rounded-full ${
i < 35 ? "bg-black" : "bg-gray-400"
}`}
style={{
height: `${Math.random() * 40 + 10}px`,
}}
/>
))}
</div>
{/* TIME */}
<div className="flex justify-between text-xs text-gray-500 mt-2">
<span>2:14</span>
<span>5:00</span>
</div>
{/* PROGRESS */}
<div className="flex items-center gap-3 mt-3">
<Volume2 size={16} />
<input
type="range"
min={0}
max={100}
value={progress}
onChange={(e) => setProgress(Number(e.target.value))}
className="w-full accent-blue-600"
/>
</div>
</div>
</div>
</div>
{/* ===== META INFO ===== */}
<div className="flex flex-wrap items-center gap-4 text-sm text-gray-500">
<span className="bg-red-600 text-white text-xs px-2 py-1 rounded">
POLRI
</span>
<span>02 Februari 2024</span>
<span>61</span>
<span>Kreator: BPKH Jurnalis</span>
</div>
{/* ===== TITLE ===== */}
<h1 className="text-2xl font-semibold leading-snug">
Bharatu Mardi Hadji Gugur Saat Bertugas, Diganjar Kenaikan Pangkat Luar
Biasa
</h1>
{/* ===== ARTICLE ===== */}
<div className="space-y-4 text-gray-700 leading-relaxed text-[15px]">
<p>
Jakarta - Kapolri Jenderal Polisi Drs. Listyo Sigit Prabowo memberikan
kenaikan pangkat luar biasa anumerta kepada almarhum Bharatu Mardi
Hadji...
</p>
<p>
Dengan penghargaan ini, almarhum resmi dinaikkan pangkatnya satu
tingkat lebih tinggi menjadi Bharaka Anumerta...
</p>
<p>
Karo Penmas Divisi Humas Polri, Brigjen Pol. Trunoyudo Wisnu Andiko,
menyatakan bahwa kenaikan pangkat anumerta ini merupakan bentuk
penghormatan...
</p>
</div>
</div>
);
}

View File

@ -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 (
<div className="bg-white border border-gray-200 rounded-3xl p-6 shadow-sm space-y-6">
{/* TAG */}
<div>
<span className="bg-red-600 text-white text-xs px-3 py-1 rounded-md font-medium">
POLRI
</span>
<div className="flex flex-wrap gap-3 mt-4">
<Tag label="Tag 1" />
<Tag label="Tag 2" />
<Tag label="Tag 3" />
</div>
</div>
<hr />
{/* OPTIONS */}
<div>
<h3 className="text-base font-semibold mb-5">Opsi Ukuran Audio</h3>
<div className="space-y-4">
{options.map((item) => (
<div
key={item.title}
onClick={() => setSelected(item.title)}
className="cursor-pointer"
>
<div className="flex items-start justify-between">
{/* LEFT */}
<div className="flex items-start gap-4">
{/* CUSTOM RADIO */}
<div
className={`h-6 w-6 rounded-full border-2 flex items-center justify-center transition
${
selected === item.title
? "border-blue-600"
: "border-gray-400"
}`}
>
{selected === item.title && (
<div className="h-3 w-3 bg-blue-600 rounded-full" />
)}
</div>
<div>
<p className="text-lg font-semibold">{item.title}</p>
</div>
</div>
{/* RIGHT */}
<div className="text-right">
<p className="font-semibold text-sm">{item.size}</p>
<p className="text-xs text-gray-500">
{item.file} &nbsp; | &nbsp; {item.format}
</p>
</div>
</div>
<div className="mt-4 border-b border-gray-200" />
</div>
))}
</div>
</div>
{/* DOWNLOAD */}
<div className="space-y-4">
<label className="flex items-center gap-3 text-sm">
<input
type="checkbox"
defaultChecked
className="h-5 w-5 accent-green-600"
/>
Unduh Semua Berkas?
</label>
<button className="w-full bg-[#9b6a1d] hover:bg-[#815515] text-white py-4 rounded-xl flex items-center justify-center gap-3 text-base font-medium transition">
<Download size={20} />
Unduh
</button>
</div>
{/* SHARE */}
<div className="flex flex-row items-center gap-3 ">
<p className="text-sm">Bagikan:</p>
<div className="flex gap-4">
<ShareIcon>
<div className="text-[#9A691D]">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M9.198 21.5h4v-8.01h3.604l.396-3.98h-4V7.5a1 1 0 0 1 1-1h3v-4h-3a5 5 0 0 0-5 5v2.01h-2l-.396 3.98h2.396z"
/>
</svg>
</div>
</ShareIcon>
<ShareIcon>
<div className="text-[#9A691D]">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M22.46 6c-.77.35-1.6.58-2.46.69c.88-.53 1.56-1.37 1.88-2.38c-.83.5-1.75.85-2.72 1.05C18.37 4.5 17.26 4 16 4c-2.35 0-4.27 1.92-4.27 4.29c0 .34.04.67.11.98C8.28 9.09 5.11 7.38 3 4.79c-.37.63-.58 1.37-.58 2.15c0 1.49.75 2.81 1.91 3.56c-.71 0-1.37-.2-1.95-.5v.03c0 2.08 1.48 3.82 3.44 4.21a4.2 4.2 0 0 1-1.93.07a4.28 4.28 0 0 0 4 2.98a8.52 8.52 0 0 1-5.33 1.84q-.51 0-1.02-.06C3.44 20.29 5.7 21 8.12 21C16 21 20.33 14.46 20.33 8.79c0-.19 0-.37-.01-.56c.84-.6 1.56-1.36 2.14-2.23"
/>
</svg>
</div>
</ShareIcon>
<ShareIcon>
<div className="text-[#9A691D]">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 16 16"
>
<path
fill="currentColor"
d="M8 0a8 8 0 1 1-4.075 14.886L.658 15.974a.5.5 0 0 1-.632-.632l1.089-3.266A8 8 0 0 1 8 0M5.214 4.004a.7.7 0 0 0-.526.266C4.508 4.481 4 4.995 4 6.037c0 1.044.705 2.054.804 2.196c.098.138 1.388 2.28 3.363 3.2q.55.255 1.12.446c.472.16.902.139 1.242.085c.379-.06 1.164-.513 1.329-1.01c.163-.493.163-.918.113-1.007c-.049-.088-.18-.142-.378-.25c-.196-.105-1.165-.618-1.345-.687c-.18-.073-.312-.106-.443.105c-.132.213-.507.691-.623.832c-.113.139-.23.159-.425.053c-.198-.105-.831-.33-1.584-1.054c-.585-.561-.98-1.258-1.094-1.469c-.116-.213-.013-.326.085-.433c.09-.094.198-.246.296-.371c.097-.122.132-.21.198-.353c.064-.141.031-.266-.018-.371s-.443-1.152-.607-1.577c-.16-.413-.323-.355-.443-.363c-.114-.005-.245-.005-.376-.005"
/>
</svg>
</div>
</ShareIcon>
</div>
</div>
</div>
);
}
/* COMPONENTS */
function Tag({ label }: { label: string }) {
return (
<span className="text-xs border border-gray-400 px-4 py-1 rounded-full bg-white">
{label}
</span>
);
}
function ShareIcon({ children }: { children: React.ReactNode }) {
return (
<div className="h-10 w-10 bg-gray-100 rounded-full flex items-center justify-center hover:bg-gray-200 transition cursor-pointer">
{children}
</div>
);
}

View File

@ -0,0 +1,13 @@
export default function DocumentDetailSection() {
return (
<div>
<img src="/image/document.png" className="rounded-xl w-full" />
<h1 className="text-2xl font-bold mt-6">
Novita Hardini: Jangan Sampai Pariwisata Meminggirkan Warga Lokal
</h1>
<p className="text-gray-600 mt-4">PARLEMENTARIA, Mandalika...</p>
</div>
);
}

View File

@ -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 (
<div className="bg-white border border-gray-200 rounded-3xl p-6 shadow-sm space-y-6">
{/* TAG */}
<div>
<span className="bg-red-600 text-white text-xs px-3 py-1 rounded-md font-medium">
POLRI
</span>
<div className="flex flex-wrap gap-3 mt-4">
<Tag label="Tag 1" />
<Tag label="Tag 2" />
<Tag label="Tag 3" />
</div>
</div>
<hr />
{/* OPTIONS */}
<div>
<h3 className="text-base font-semibold mb-5">Opsi Ukuran Document</h3>
<div className="space-y-4">
{options.map((item) => (
<div
key={item.title}
onClick={() => setSelected(item.title)}
className="cursor-pointer"
>
<div className="flex items-start justify-between">
{/* LEFT */}
<div className="flex items-start gap-4">
{/* CUSTOM RADIO */}
<div
className={`h-6 w-6 rounded-full border-2 flex items-center justify-center transition
${
selected === item.title
? "border-blue-600"
: "border-gray-400"
}`}
>
{selected === item.title && (
<div className="h-3 w-3 bg-blue-600 rounded-full" />
)}
</div>
<div>
<p className="text-lg font-semibold">{item.title}</p>
</div>
</div>
{/* RIGHT */}
<div className="text-right">
<p className="font-semibold text-sm">{item.size}</p>
</div>
</div>
<div className="mt-4 border-b border-gray-200" />
</div>
))}
</div>
</div>
{/* DOWNLOAD */}
<div className="space-y-4">
<label className="flex items-center gap-3 text-sm">
<input
type="checkbox"
defaultChecked
className="h-5 w-5 accent-green-600"
/>
Unduh Semua Berkas?
</label>
<button className="w-full bg-[#9b6a1d] hover:bg-[#815515] text-white py-4 rounded-xl flex items-center justify-center gap-3 text-base font-medium transition">
<Download size={20} />
Unduh
</button>
</div>
{/* SHARE */}
<div className="flex flex-row items-center gap-3 ">
<p className="text-sm">Bagikan:</p>
<div className="flex gap-4">
<ShareIcon>
<div className="text-[#9A691D]">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M9.198 21.5h4v-8.01h3.604l.396-3.98h-4V7.5a1 1 0 0 1 1-1h3v-4h-3a5 5 0 0 0-5 5v2.01h-2l-.396 3.98h2.396z"
/>
</svg>
</div>
</ShareIcon>
<ShareIcon>
<div className="text-[#9A691D]">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M22.46 6c-.77.35-1.6.58-2.46.69c.88-.53 1.56-1.37 1.88-2.38c-.83.5-1.75.85-2.72 1.05C18.37 4.5 17.26 4 16 4c-2.35 0-4.27 1.92-4.27 4.29c0 .34.04.67.11.98C8.28 9.09 5.11 7.38 3 4.79c-.37.63-.58 1.37-.58 2.15c0 1.49.75 2.81 1.91 3.56c-.71 0-1.37-.2-1.95-.5v.03c0 2.08 1.48 3.82 3.44 4.21a4.2 4.2 0 0 1-1.93.07a4.28 4.28 0 0 0 4 2.98a8.52 8.52 0 0 1-5.33 1.84q-.51 0-1.02-.06C3.44 20.29 5.7 21 8.12 21C16 21 20.33 14.46 20.33 8.79c0-.19 0-.37-.01-.56c.84-.6 1.56-1.36 2.14-2.23"
/>
</svg>
</div>
</ShareIcon>
<ShareIcon>
<div className="text-[#9A691D]">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 16 16"
>
<path
fill="currentColor"
d="M8 0a8 8 0 1 1-4.075 14.886L.658 15.974a.5.5 0 0 1-.632-.632l1.089-3.266A8 8 0 0 1 8 0M5.214 4.004a.7.7 0 0 0-.526.266C4.508 4.481 4 4.995 4 6.037c0 1.044.705 2.054.804 2.196c.098.138 1.388 2.28 3.363 3.2q.55.255 1.12.446c.472.16.902.139 1.242.085c.379-.06 1.164-.513 1.329-1.01c.163-.493.163-.918.113-1.007c-.049-.088-.18-.142-.378-.25c-.196-.105-1.165-.618-1.345-.687c-.18-.073-.312-.106-.443.105c-.132.213-.507.691-.623.832c-.113.139-.23.159-.425.053c-.198-.105-.831-.33-1.584-1.054c-.585-.561-.98-1.258-1.094-1.469c-.116-.213-.013-.326.085-.433c.09-.094.198-.246.296-.371c.097-.122.132-.21.198-.353c.064-.141.031-.266-.018-.371s-.443-1.152-.607-1.577c-.16-.413-.323-.355-.443-.363c-.114-.005-.245-.005-.376-.005"
/>
</svg>
</div>
</ShareIcon>
</div>
</div>
</div>
);
}
/* COMPONENTS */
function Tag({ label }: { label: string }) {
return (
<span className="text-xs border border-gray-400 px-4 py-1 rounded-full bg-white">
{label}
</span>
);
}
function ShareIcon({ children }: { children: React.ReactNode }) {
return (
<div className="h-10 w-10 bg-gray-100 rounded-full flex items-center justify-center hover:bg-gray-200 transition cursor-pointer">
{children}
</div>
);
}

View File

@ -0,0 +1,13 @@
export default function ImageDetailSection() {
return (
<div>
<img src="/image/novita2.png" className="rounded-xl w-full" />
<h1 className="text-2xl font-bold mt-6">
Novita Hardini: Jangan Sampai Pariwisata Meminggirkan Warga Lokal
</h1>
<p className="text-gray-600 mt-4">PARLEMENTARIA, Mandalika...</p>
</div>
);
}

View File

@ -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 (
<div className="bg-white border border-gray-200 rounded-3xl p-6 shadow-sm space-y-6">
{/* TAG */}
<div>
<span className="bg-red-600 text-white text-xs px-3 py-1 rounded-md font-medium">
POLRI
</span>
<div className="flex flex-wrap gap-3 mt-4">
<Tag label="Tag 1" />
<Tag label="Tag 2" />
<Tag label="Tag 3" />
</div>
</div>
<hr />
{/* OPTIONS */}
<div>
<h3 className="text-base font-semibold mb-5">Opsi Ukuran Foto</h3>
<div className="space-y-4">
{options.map((item) => (
<div
key={item.title}
onClick={() => setSelected(item.title)}
className="cursor-pointer"
>
<div className="flex items-start justify-between">
{/* LEFT */}
<div className="flex items-start gap-4">
{/* CUSTOM RADIO */}
<div
className={`h-6 w-6 rounded-full border-2 flex items-center justify-center transition
${
selected === item.title
? "border-blue-600"
: "border-gray-400"
}`}
>
{selected === item.title && (
<div className="h-3 w-3 bg-blue-600 rounded-full" />
)}
</div>
<div>
<p className="text-lg font-semibold">{item.title}</p>
</div>
</div>
{/* RIGHT */}
<div className="text-right">
<p className="font-semibold text-sm">{item.size}</p>
</div>
</div>
<div className="mt-4 border-b border-gray-200" />
</div>
))}
</div>
</div>
{/* DOWNLOAD */}
<div className="space-y-4">
<label className="flex items-center gap-3 text-sm">
<input
type="checkbox"
defaultChecked
className="h-5 w-5 accent-green-600"
/>
Unduh Semua Berkas?
</label>
<button className="w-full bg-[#9b6a1d] hover:bg-[#815515] text-white py-4 rounded-xl flex items-center justify-center gap-3 text-base font-medium transition">
<Download size={20} />
Unduh
</button>
</div>
{/* SHARE */}
<div className="flex flex-row items-center gap-3 ">
<p className="text-sm">Bagikan:</p>
<div className="flex gap-4">
<ShareIcon>
<div className="text-[#9A691D]">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M9.198 21.5h4v-8.01h3.604l.396-3.98h-4V7.5a1 1 0 0 1 1-1h3v-4h-3a5 5 0 0 0-5 5v2.01h-2l-.396 3.98h2.396z"
/>
</svg>
</div>
</ShareIcon>
<ShareIcon>
<div className="text-[#9A691D]">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M22.46 6c-.77.35-1.6.58-2.46.69c.88-.53 1.56-1.37 1.88-2.38c-.83.5-1.75.85-2.72 1.05C18.37 4.5 17.26 4 16 4c-2.35 0-4.27 1.92-4.27 4.29c0 .34.04.67.11.98C8.28 9.09 5.11 7.38 3 4.79c-.37.63-.58 1.37-.58 2.15c0 1.49.75 2.81 1.91 3.56c-.71 0-1.37-.2-1.95-.5v.03c0 2.08 1.48 3.82 3.44 4.21a4.2 4.2 0 0 1-1.93.07a4.28 4.28 0 0 0 4 2.98a8.52 8.52 0 0 1-5.33 1.84q-.51 0-1.02-.06C3.44 20.29 5.7 21 8.12 21C16 21 20.33 14.46 20.33 8.79c0-.19 0-.37-.01-.56c.84-.6 1.56-1.36 2.14-2.23"
/>
</svg>
</div>
</ShareIcon>
<ShareIcon>
<div className="text-[#9A691D]">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 16 16"
>
<path
fill="currentColor"
d="M8 0a8 8 0 1 1-4.075 14.886L.658 15.974a.5.5 0 0 1-.632-.632l1.089-3.266A8 8 0 0 1 8 0M5.214 4.004a.7.7 0 0 0-.526.266C4.508 4.481 4 4.995 4 6.037c0 1.044.705 2.054.804 2.196c.098.138 1.388 2.28 3.363 3.2q.55.255 1.12.446c.472.16.902.139 1.242.085c.379-.06 1.164-.513 1.329-1.01c.163-.493.163-.918.113-1.007c-.049-.088-.18-.142-.378-.25c-.196-.105-1.165-.618-1.345-.687c-.18-.073-.312-.106-.443.105c-.132.213-.507.691-.623.832c-.113.139-.23.159-.425.053c-.198-.105-.831-.33-1.584-1.054c-.585-.561-.98-1.258-1.094-1.469c-.116-.213-.013-.326.085-.433c.09-.094.198-.246.296-.371c.097-.122.132-.21.198-.353c.064-.141.031-.266-.018-.371s-.443-1.152-.607-1.577c-.16-.413-.323-.355-.443-.363c-.114-.005-.245-.005-.376-.005"
/>
</svg>
</div>
</ShareIcon>
</div>
</div>
</div>
);
}
/* COMPONENTS */
function Tag({ label }: { label: string }) {
return (
<span className="text-xs border border-gray-400 px-4 py-1 rounded-full bg-white">
{label}
</span>
);
}
function ShareIcon({ children }: { children: React.ReactNode }) {
return (
<div className="h-10 w-10 bg-gray-100 rounded-full flex items-center justify-center hover:bg-gray-200 transition cursor-pointer">
{children}
</div>
);
}

View File

@ -0,0 +1,80 @@
"use client";
import { Eye, Calendar } from "lucide-react";
export default function VideoMeta() {
return (
<div className="mt-6 space-y-6">
{/* INFO */}
<div className="flex flex-wrap items-center gap-4 text-sm text-gray-500">
<span className="bg-red-600 text-white text-xs px-2 py-1 rounded">
POLRI
</span>
<div className="flex items-center gap-1">
<Calendar size={14} />
02 Februari 2024
</div>
<div className="flex items-center gap-1">
<Eye size={14} />
61
</div>
<span>Kreator: POLRI Jurnalis</span>
</div>
{/* TITLE */}
<h1 className="text-2xl font-semibold leading-snug">
Bharatu Mardi Hadji Gugur Saat Bertugas, Diganjar Kenaikan Pangkat Luar
Biasa
</h1>
{/* ARTICLE */}
<div className="space-y-4 text-gray-700 leading-relaxed text-[15px]">
<p className="">
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.
</p>
</div>
</div>
);
}

View File

@ -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 (
<div>
{/* VIDEO THUMB */}
<div className="relative w-full h-[250px] md:h-[450px] rounded-xl overflow-hidden bg-black">
<Image
src="/image/bharatu.jpg"
alt="video"
fill
className="object-cover opacity-90"
/>
{/* Play Button */}
<div className="absolute inset-0 flex items-center justify-center">
<div className="bg-white/90 rounded-full p-4 shadow-lg">
<Play size={28} className="text-black ml-1" />
</div>
</div>
{/* Duration */}
<div className="absolute bottom-3 left-3 text-xs text-white bg-black/70 px-2 py-1 rounded">
1:58 / 3:00
</div>
</div>
{/* META & CONTENT */}
<VideoMeta />
</div>
);
}

View File

@ -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 (
<div className="bg-white border border-gray-200 rounded-3xl p-6 shadow-sm space-y-6">
{/* TAG */}
<div>
<span className="bg-red-600 text-white text-xs px-3 py-1 rounded-md font-medium">
POLRI
</span>
<div className="flex flex-wrap gap-3 mt-4">
<Tag label="Tag 1" />
<Tag label="Tag 2" />
<Tag label="Tag 3" />
</div>
</div>
<hr />
{/* OPTIONS */}
<div>
<h3 className="text-base font-semibold mb-5">
Opsi Ukuran Audio Visual
</h3>
<div className="space-y-4">
{options.map((item) => (
<div
key={item.title}
onClick={() => setSelected(item.title)}
className="cursor-pointer"
>
<div className="flex items-start justify-between">
{/* LEFT */}
<div className="flex items-start gap-4">
{/* CUSTOM RADIO */}
<div
className={`h-6 w-6 rounded-full border-2 flex items-center justify-center transition
${
selected === item.title
? "border-blue-600"
: "border-gray-400"
}`}
>
{selected === item.title && (
<div className="h-3 w-3 bg-blue-600 rounded-full" />
)}
</div>
<div>
<p className="text-lg font-semibold">{item.title}</p>
</div>
</div>
{/* RIGHT */}
<div className="text-right">
<p className="font-semibold text-sm">{item.size}</p>
<p className="text-xs text-gray-500">
{item.file} &nbsp; | &nbsp; {item.format}
</p>
</div>
</div>
<div className="mt-4 border-b border-gray-200" />
</div>
))}
</div>
</div>
{/* DOWNLOAD */}
<div className="space-y-4">
<label className="flex items-center gap-3 text-sm">
<input
type="checkbox"
defaultChecked
className="h-5 w-5 accent-green-600"
/>
Unduh Semua Berkas?
</label>
<button className="w-full bg-[#9b6a1d] hover:bg-[#815515] text-white py-4 rounded-xl flex items-center justify-center gap-3 text-base font-medium transition">
<Download size={20} />
Unduh
</button>
</div>
{/* SHARE */}
<div className="flex flex-row items-center gap-3 ">
<p className="text-sm">Bagikan:</p>
<div className="flex gap-4">
<ShareIcon>
<div className="text-[#9A691D]">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M9.198 21.5h4v-8.01h3.604l.396-3.98h-4V7.5a1 1 0 0 1 1-1h3v-4h-3a5 5 0 0 0-5 5v2.01h-2l-.396 3.98h2.396z"
/>
</svg>
</div>
</ShareIcon>
<ShareIcon>
<div className="text-[#9A691D]">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M22.46 6c-.77.35-1.6.58-2.46.69c.88-.53 1.56-1.37 1.88-2.38c-.83.5-1.75.85-2.72 1.05C18.37 4.5 17.26 4 16 4c-2.35 0-4.27 1.92-4.27 4.29c0 .34.04.67.11.98C8.28 9.09 5.11 7.38 3 4.79c-.37.63-.58 1.37-.58 2.15c0 1.49.75 2.81 1.91 3.56c-.71 0-1.37-.2-1.95-.5v.03c0 2.08 1.48 3.82 3.44 4.21a4.2 4.2 0 0 1-1.93.07a4.28 4.28 0 0 0 4 2.98a8.52 8.52 0 0 1-5.33 1.84q-.51 0-1.02-.06C3.44 20.29 5.7 21 8.12 21C16 21 20.33 14.46 20.33 8.79c0-.19 0-.37-.01-.56c.84-.6 1.56-1.36 2.14-2.23"
/>
</svg>
</div>
</ShareIcon>
<ShareIcon>
<div className="text-[#9A691D]">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 16 16"
>
<path
fill="currentColor"
d="M8 0a8 8 0 1 1-4.075 14.886L.658 15.974a.5.5 0 0 1-.632-.632l1.089-3.266A8 8 0 0 1 8 0M5.214 4.004a.7.7 0 0 0-.526.266C4.508 4.481 4 4.995 4 6.037c0 1.044.705 2.054.804 2.196c.098.138 1.388 2.28 3.363 3.2q.55.255 1.12.446c.472.16.902.139 1.242.085c.379-.06 1.164-.513 1.329-1.01c.163-.493.163-.918.113-1.007c-.049-.088-.18-.142-.378-.25c-.196-.105-1.165-.618-1.345-.687c-.18-.073-.312-.106-.443.105c-.132.213-.507.691-.623.832c-.113.139-.23.159-.425.053c-.198-.105-.831-.33-1.584-1.054c-.585-.561-.98-1.258-1.094-1.469c-.116-.213-.013-.326.085-.433c.09-.094.198-.246.296-.371c.097-.122.132-.21.198-.353c.064-.141.031-.266-.018-.371s-.443-1.152-.607-1.577c-.16-.413-.323-.355-.443-.363c-.114-.005-.245-.005-.376-.005"
/>
</svg>
</div>
</ShareIcon>
</div>
</div>
</div>
);
}
/* COMPONENTS */
function Tag({ label }: { label: string }) {
return (
<span className="text-xs border border-gray-400 px-4 py-1 rounded-full bg-white">
{label}
</span>
);
}
function ShareIcon({ children }: { children: React.ReactNode }) {
return (
<div className="h-10 w-10 bg-gray-100 rounded-full flex items-center justify-center hover:bg-gray-200 transition cursor-pointer">
{children}
</div>
);
}

View File

@ -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 (
<Link href={`/details/${slug}?type=text`}>
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm hover:shadow-md transition duration-300 overflow-hidden">
{/* IMAGE */}
<div className="relative h-[200px] w-full">
<Image
src="/image/document.png"
alt="news"
fill
className="object-cover"
/>
</div>
{/* CONTENT */}
<div className="p-5 space-y-3">
{/* BADGE + TAG */}
<div className="flex items-center gap-2 text-xs">
<span className="bg-red-600 text-white px-2 py-[3px] rounded-md font-medium">
POLRI
</span>
<span className="text-gray-500 uppercase tracking-wide">
SEPUTAR PRESTASI
</span>
</div>
{/* DATE */}
<p className="text-xs text-gray-400">02 Februari 2024</p>
{/* TITLE */}
<h3 className="font-semibold text-[15px] leading-snug line-clamp-2 hover:text-[#966314] transition">
Bharatu Mardi Hadji Gugur Saat Bertugas, Diganjar Kenaikan Pangkat
Luar Biasa
</h3>
{/* EXCERPT */}
<p className="text-sm text-gray-500 line-clamp-2 leading-relaxed">
Jakarta - Kapolri Jenderal Polisi Drs. Listyo Sigit Prabowo
memberikan kenaikan pangkat luar biasa anumerta kepada...
</p>
</div>
</div>
</Link>
);
}

View File

@ -0,0 +1,99 @@
"use client";
import { ChevronLeft } from "lucide-react";
export default function FilterDocumentSidebar() {
return (
<div className="bg-white rounded-2xl border border-gray-200 shadow-sm p-6">
{/* HEADER */}
<div className="flex items-center justify-between pb-4 border-b">
<h3 className="font-semibold text-sm flex items-center gap-2">
Filter
</h3>
<ChevronLeft size={16} className="text-gray-400" />
</div>
{/* CONTENT */}
<div className="space-y-6 mt-6">
{/* KATEGORI */}
<FilterSection title="Kategori">
<Checkbox label="Semua" count={1203} defaultChecked />
<Checkbox label="Berita Terhangat" count={123} />
<Checkbox label="Tentang Teknologi" count={24} />
<Checkbox label="Bersama Pelanggan" count={42} />
<Checkbox label="Pembicara Ahli" count={224} />
</FilterSection>
<Divider />
{/* JENIS FILE */}
<FilterSection title="Jenis File">
<Checkbox label="Semua" count={78} />
<Checkbox label="Audio Visual" count={120} />
<Checkbox label="Audio" count={34} />
<Checkbox label="Foto" count={234} />
<Checkbox label="Teks" count={9} defaultChecked />
</FilterSection>
<Divider />
{/* FORMAT */}
<FilterSection title="Format Document ">
<Checkbox label="Semua" count={2} defaultChecked />
</FilterSection>
{/* RESET */}
<div className="text-center pt-4">
<button className="text-sm text-[#966314] font-medium hover:underline">
Reset Filter
</button>
</div>
</div>
</div>
);
}
/* ===== COMPONENTS ===== */
function FilterSection({
title,
children,
}: {
title: string;
children: React.ReactNode;
}) {
return (
<div>
<p className="text-sm font-medium mb-3">{title}</p>
<div className="space-y-2">{children}</div>
</div>
);
}
function Checkbox({
label,
count,
defaultChecked,
}: {
label: string;
count: number;
defaultChecked?: boolean;
}) {
return (
<label className="flex items-center justify-between text-sm cursor-pointer">
<div className="flex items-center gap-2">
<input
type="checkbox"
defaultChecked={defaultChecked}
className="h-4 w-4 accent-[#966314]"
/>
<span>{label}</span>
</div>
<span className="text-gray-400">({count})</span>
</label>
);
}
function Divider() {
return <div className="border-t border-gray-200"></div>;
}

206
components/form/login.tsx Normal file
View File

@ -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 (
<div className="min-h-screen flex">
{/* LEFT IMAGE SECTION */}
<div className="hidden lg:block lg:w-1/2 relative">
<Image
src="/image/login.jpg"
alt="Login Illustration"
fill
priority
className="object-cover"
/>
</div>
{/* RIGHT FORM SECTION */}
<div className="w-full lg:w-1/2 flex items-center justify-center bg-gray-50 px-6">
<div className="w-full max-w-md">
{/* LOGO */}
<div className="flex justify-center mb-8">
<Image
src="/image/qudo1.png"
alt="Qudoco Logo"
width={90}
height={90}
className="object-contain"
/>
</div>
{/* TITLE */}
<h1 className="text-3xl font-bold text-gray-900 mb-8 text-center">
Welcome Back
</h1>
{/* FORM */}
<div className="space-y-6">
{/* Username */}
<div>
<label className="block text-sm text-gray-600 mb-2">
Username
</label>
<Input
type="text"
placeholder="Enter username"
value={username}
onChange={(e) => setValUsername(e.target.value)}
className="w-full border-b border-gray-300 focus:border-[#9c6b16] outline-none py-2 bg-transparent transition"
/>
</div>
{/* Password */}
<div>
<label className="block text-sm text-gray-600 mb-2">
Password
</label>
<div className="relative">
<Input
type={showPassword ? "text" : "password"}
placeholder="Enter password"
value={password}
onChange={(e) => 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
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-0 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
>
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
</button>
</div>
</div>
{/* BUTTON */}
<button
onClick={onSubmit}
className="w-full bg-[#9c6b16] hover:bg-[#805512] text-white py-3 rounded-lg transition font-medium"
>
Login
</button>
</div>
{/* REGISTER */}
<p className="text-center text-sm text-gray-500 mt-6">
Dont have an account?{" "}
<span className="text-blue-600 cursor-pointer hover:underline">
Register
</span>
</p>
{/* FOOTER */}
<div className="text-center text-xs text-gray-400 mt-12 space-x-4">
<span className="hover:text-gray-600 cursor-pointer">Terms</span>
<span className="hover:text-gray-600 cursor-pointer">
Privacy Policy
</span>
<span className="hover:text-gray-600 cursor-pointer">Security</span>
<div className="mt-4">
© 2024 Copyrights by company. All Rights Reserved.
<br />
Designed by <span className="font-medium">Qudoco Team</span>
</div>
</div>
</div>
</div>
</div>
);
}

2734
components/icons.tsx Normal file

File diff suppressed because one or more lines are too long

View File

@ -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) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size || width}
height={size || height}
{...props}
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M12 3c2.21 0 4 1.79 4 4s-1.79 4-4 4s-4-1.79-4-4s1.79-4 4-4m4 10.54c0 1.06-.28 3.53-2.19 6.29L13 15l.94-1.88c-.62-.07-1.27-.12-1.94-.12s-1.32.05-1.94.12L11 15l-.81 4.83C8.28 17.07 8 14.6 8 13.54c-2.39.7-4 1.96-4 3.46v4h16v-4c0-1.5-1.6-2.76-4-3.46"
/>
</svg>
);
export const DashboardBriefcaseIcon = ({
size,
height = 48,
width = 48,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
fill="none"
height={size || height}
viewBox="0 0 48 48"
width={size || width}
{...props}
>
<path fill="#f5bc00" d="M44,41H4V10h40V41z" />
<polygon fill="#eb7900" points="44,26 24,26 4,26 4,10 44,10" />
<path fill="#eb7900" d="M17,26h-6v3h6V26z" />
<path fill="#eb7900" d="M37,26h-6v3h6V26z" />
<rect width="14" height="3" x="17" y="7" fill="#f5bc00" />
<path fill="#eb0000" d="M17,23h-6v3h6V23z" />
<path fill="#eb0000" d="M37,23h-6v3h6V23z" />
</svg>
);
export const DashboardMailboxIcon = ({
size,
height = 48,
width = 48,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
fill="none"
height={size || height}
width={size || width}
{...props}
viewBox="0 0 48 48"
>
<path fill="#3dd9eb" d="M43,36H13V11h22c4.418,0,8,3.582,8,8V36z" />
<path
fill="#7debf5"
d="M21,36H5V19c0-4.418,3.582-8,8-8l0,0c4.418,0,8,3.582,8,8V36z"
/>
<path fill="#6c19ff" d="M21,36h5v8h-5V36z" />
<polygon fill="#eb0000" points="27,16 27,20 35,20 35,24 39,24 39,16" />
<rect width="8" height="3" x="9" y="20" fill="#3dd9eb" />
</svg>
);
export const DashboardShareIcon = ({
size,
height = 48,
width = 48,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
height={size || height}
width={size || width}
{...props}
viewBox="0 0 512 512"
>
<path
fill="currentColor"
d="M503.691 189.836L327.687 37.851C312.281 24.546 288 35.347 288 56.015v80.053C127.371 137.907 0 170.1 0 322.326c0 61.441 39.581 122.309 83.333 154.132c13.653 9.931 33.111-2.533 28.077-18.631C66.066 312.814 132.917 274.316 288 272.085V360c0 20.7 24.3 31.453 39.687 18.164l176.004-152c11.071-9.562 11.086-26.753 0-36.328"
/>
</svg>
);
export const DashboardSpeecIcon = ({
size,
height = 48,
width = 48,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
height={size || height}
width={size || width}
{...props}
viewBox="0 0 20 20"
>
<path
fill="currentColor"
d="M7 0a2 2 0 0 0-2 2h9a2 2 0 0 1 2 2v12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2z"
/>
<path
fill="currentColor"
d="M13 20a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2zM9 5h4v5H9zM4 5h4v1H4zm0 2h4v1H4zm0 2h4v1H4zm0 2h9v1H4zm0 2h9v1H4zm0 2h9v1H4z"
/>
</svg>
);
export const DashboardConnectIcon = ({
size,
height = 48,
width = 48,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
height={size || height}
width={size || width}
{...props}
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M2 22V4q0-.825.588-1.412T4 2h16q.825 0 1.413.588T22 4v12q0 .825-.587 1.413T20 18H6zm7.075-7.75L12 12.475l2.925 1.775l-.775-3.325l2.6-2.25l-3.425-.275L12 5.25L10.675 8.4l-3.425.275l2.6 2.25z"
/>
</svg>
);
export const DashboardTopLeftPointIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
fill="none"
height={size || height}
viewBox="0 0 24 24"
width={size || width}
{...props}
>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
d="M18 18L6 6m0 0h9M6 6v9"
/>
</svg>
);
export const DashboardRightDownPointIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
fill="none"
height={size || height}
viewBox="0 0 24 24"
width={size || width}
{...props}
>
<path
fill="currentColor"
fillRule="evenodd"
d="M5.47 5.47a.75.75 0 0 1 1.06 0l10.72 10.72V9a.75.75 0 0 1 1.5 0v9a.75.75 0 0 1-.75.75H9a.75.75 0 0 1 0-1.5h7.19L5.47 6.53a.75.75 0 0 1 0-1.06"
clipRule="evenodd"
/>
</svg>
);
export const DashboardCommentIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
height={size || height}
width={size || width}
{...props}
viewBox="0 0 48 48"
>
<defs>
<mask id="ipSComment0">
<g
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="4"
>
<path fill="#fff" stroke="#fff" d="M44 6H4v30h9v5l10-5h21z" />
<path stroke="#000" d="M14 19.5v3m10-3v3m10-3v3" />
</g>
</mask>
</defs>
<path fill="currentColor" d="M0 0h48v48H0z" mask="url(#ipSComment0)" />
</svg>
);

View File

@ -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) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size || width}
height={size || height}
viewBox="0 0 15 15"
{...props}
>
<path
fill="currentColor"
d="M3.5 8H3V7h.5a.5.5 0 0 1 0 1M7 10V7h.5a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5z"
/>
<path
fill="currentColor"
fillRule="evenodd"
d="M1 1.5A1.5 1.5 0 0 1 2.5 0h8.207L14 3.293V13.5a1.5 1.5 0 0 1-1.5 1.5h-10A1.5 1.5 0 0 1 1 13.5zM3.5 6H2v5h1V9h.5a1.5 1.5 0 1 0 0-3m4 0H6v5h1.5A1.5 1.5 0 0 0 9 9.5v-2A1.5 1.5 0 0 0 7.5 6m2.5 5V6h3v1h-2v1h1v1h-1v2z"
clipRule="evenodd"
/>
</svg>
);
export const CsvIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size || width}
height={size || height}
{...props}
viewBox="0 0 15 15"
>
<path
fill="currentColor"
fillRule="evenodd"
d="M1 1.5A1.5 1.5 0 0 1 2.5 0h8.207L14 3.293V13.5a1.5 1.5 0 0 1-1.5 1.5h-10A1.5 1.5 0 0 1 1 13.5zM2 6h3v1H3v3h2v1H2zm7 0H6v3h2v1H6v1h3V8H7V7h2zm2 0h-1v3.707l1.5 1.5l1.5-1.5V6h-1v3.293l-.5.5l-.5-.5z"
clipRule="evenodd"
/>
</svg>
);
export const ExcelIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 15 15"
width={size || width}
height={size || height}
{...props}
>
<path
fill="currentColor"
d="M3.793 7.5L2.146 5.854l.708-.708L4.5 6.793l1.646-1.647l.708.708L5.207 7.5l1.647 1.646l-.708.708L4.5 8.207L2.854 9.854l-.708-.708z"
/>
<path
fill="currentColor"
fillRule="evenodd"
d="M3.5 0A1.5 1.5 0 0 0 2 1.5V3h-.5A1.5 1.5 0 0 0 0 4.5v6A1.5 1.5 0 0 0 1.5 12H2v1.5A1.5 1.5 0 0 0 3.5 15h10a1.5 1.5 0 0 0 1.5-1.5v-12A1.5 1.5 0 0 0 13.5 0zm-2 4a.5.5 0 0 0-.5.5v6a.5.5 0 0 0 .5.5h6a.5.5 0 0 0 .5-.5v-6a.5.5 0 0 0-.5-.5z"
clipRule="evenodd"
/>
</svg>
);
export const WordIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size || width}
height={size || height}
{...props}
viewBox="0 0 15 15"
>
<path
fill="currentColor"
d="m2.015 5.621l1 4a.5.5 0 0 0 .901.156l.584-.876l.584.876a.5.5 0 0 0 .901-.156l1-4l-.97-.242l-.726 2.903l-.373-.56a.5.5 0 0 0-.832 0l-.373.56l-.726-2.903z"
/>
<path
fill="currentColor"
fillRule="evenodd"
d="M3.5 0A1.5 1.5 0 0 0 2 1.5V3h-.5A1.5 1.5 0 0 0 0 4.5v6A1.5 1.5 0 0 0 1.5 12H2v1.5A1.5 1.5 0 0 0 3.5 15h10a1.5 1.5 0 0 0 1.5-1.5v-12A1.5 1.5 0 0 0 13.5 0zm-2 4a.5.5 0 0 0-.5.5v6a.5.5 0 0 0 .5.5h6a.5.5 0 0 0 .5-.5v-6a.5.5 0 0 0-.5-.5z"
clipRule="evenodd"
/>
</svg>
);
export const PptIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size || width}
height={size || height}
{...props}
viewBox="0 0 15 15"
>
<path
fill="currentColor"
d="M3 8h.5a.5.5 0 0 0 0-1H3zm4 0h.5a.5.5 0 0 0 0-1H7z"
/>
<path
fill="currentColor"
fillRule="evenodd"
d="M1 1.5A1.5 1.5 0 0 1 2.5 0h8.207L14 3.293V13.5a1.5 1.5 0 0 1-1.5 1.5h-10A1.5 1.5 0 0 1 1 13.5zM2 6h1.5a1.5 1.5 0 1 1 0 3H3v2H2zm4 0h1.5a1.5 1.5 0 1 1 0 3H7v2H6zm5 5h1V7h1V6h-3v1h1z"
clipRule="evenodd"
/>
</svg>
);
export const FileIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size || width}
height={size || height}
{...props}
viewBox="0 0 15 15"
>
<path
fill="currentColor"
d="m10.5.5l.354-.354L10.707 0H10.5zm3 3h.5v-.207l-.146-.147zm-1 10.5h-10v1h10zM2 13.5v-12H1v12zM2.5 1h8V0h-8zM13 3.5v10h1v-10zM10.146.854l3 3l.708-.708l-3-3zM2.5 14a.5.5 0 0 1-.5-.5H1A1.5 1.5 0 0 0 2.5 15zm10 1a1.5 1.5 0 0 0 1.5-1.5h-1a.5.5 0 0 1-.5.5zM2 1.5a.5.5 0 0 1 .5-.5V0A1.5 1.5 0 0 0 1 1.5z"
/>
</svg>
);
export const UserProfileIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size || width}
height={size || height}
{...props}
viewBox="0 0 16 16"
>
<path
fill="currentColor"
d="M11 7c0 1.66-1.34 3-3 3S5 8.66 5 7s1.34-3 3-3s3 1.34 3 3"
/>
<path
fill="currentColor"
fillRule="evenodd"
d="M16 8c0 4.42-3.58 8-8 8s-8-3.58-8-8s3.58-8 8-8s8 3.58 8 8M4 13.75C4.16 13.484 5.71 11 7.99 11c2.27 0 3.83 2.49 3.99 2.75A6.98 6.98 0 0 0 14.99 8c0-3.87-3.13-7-7-7s-7 3.13-7 7c0 2.38 1.19 4.49 3.01 5.75"
clipRule="evenodd"
/>
</svg>
);
export const SettingsIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size || width}
height={size || height}
{...props}
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="m9.25 22l-.4-3.2q-.325-.125-.612-.3t-.563-.375L4.7 19.375l-2.75-4.75l2.575-1.95Q4.5 12.5 4.5 12.338v-.675q0-.163.025-.338L1.95 9.375l2.75-4.75l2.975 1.25q.275-.2.575-.375t.6-.3l.4-3.2h5.5l.4 3.2q.325.125.613.3t.562.375l2.975-1.25l2.75 4.75l-2.575 1.95q.025.175.025.338v.674q0 .163-.05.338l2.575 1.95l-2.75 4.75l-2.95-1.25q-.275.2-.575.375t-.6.3l-.4 3.2zm2.8-6.5q1.45 0 2.475-1.025T15.55 12t-1.025-2.475T12.05 8.5q-1.475 0-2.488 1.025T8.55 12t1.013 2.475T12.05 15.5"
/>
</svg>
);

View File

@ -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) => (
<svg
fill="none"
height={size || height}
viewBox="0 0 24 24"
width={size || width}
{...props}
>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2.5"
d="M3 6h18M3 12h18M3 18h18"
/>
</svg>
);
export const DashboardIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
fill="none"
height={size || height}
viewBox="0 0 24 24"
width={size || width}
{...props}
>
<path
fill="currentColor"
d="M13 9V3h8v6zM3 13V3h8v10zm10 8V11h8v10zM3 21v-6h8v6z"
/>
</svg>
);
export const HomeIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
fill="none"
height={size || height}
viewBox="0 0 24 24"
width={size || width}
{...props}
>
<g fill="none" stroke="currentColor" strokeWidth="1.5">
<path d="M2 12.204c0-2.289 0-3.433.52-4.381c.518-.949 1.467-1.537 3.364-2.715l2-1.241C9.889 2.622 10.892 2 12 2c1.108 0 2.11.622 4.116 1.867l2 1.241c1.897 1.178 2.846 1.766 3.365 2.715c.519.948.519 2.092.519 4.38v1.522c0 3.9 0 5.851-1.172 7.063C19.657 22 17.771 22 14 22h-4c-3.771 0-5.657 0-6.828-1.212C2 19.576 2 17.626 2 13.725z" />
<path strokeLinecap="round" d="M12 15v3" />
</g>
</svg>
);
export const Submenu1Icon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
fill="none"
height={size || height}
viewBox="0 0 48 48"
width={size || width}
{...props}
>
<defs>
<mask id="ipTData0">
<g
fill="none"
stroke="#fff"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="4"
>
<path d="M44 11v27c0 3.314-8.954 6-20 6S4 41.314 4 38V11" />
<path d="M44 29c0 3.314-8.954 6-20 6S4 32.314 4 29m40-9c0 3.314-8.954 6-20 6S4 23.314 4 20" />
<ellipse cx="24" cy="10" fill="#555" rx="20" ry="6" />
</g>
</mask>
</defs>
<path fill="currentColor" d="M0 0h48v48H0z" mask="url(#ipTData0)" />
</svg>
);
export const Submenu2Icon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
fill="none"
height={size || height}
viewBox="0 0 256 256"
width={size || width}
{...props}
>
<path
fill="currentColor"
d="M230.93 220a8 8 0 0 1-6.93 4H32a8 8 0 0 1-6.92-12c15.23-26.33 38.7-45.21 66.09-54.16a72 72 0 1 1 73.66 0c27.39 8.95 50.86 27.83 66.09 54.16a8 8 0 0 1 .01 8"
/>
</svg>
);
export const InfoCircleIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
fill="none"
height={size || height}
viewBox="0 0 1024 1024"
width={size || width}
{...props}
>
<path
fill="currentColor"
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448s448-200.6 448-448S759.4 64 512 64m0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372s372 166.6 372 372s-166.6 372-372 372"
/>
<path
fill="currentColor"
d="M464 336a48 48 0 1 0 96 0a48 48 0 1 0-96 0m72 112h-48c-4.4 0-8 3.6-8 8v272c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8V456c0-4.4-3.6-8-8-8"
/>
</svg>
);
export const MinusCircleIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
fill="none"
height={size || height}
viewBox="0 0 16 16"
width={size || width}
{...props}
>
<g fill="currentColor">
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16" />
<path d="M4 8a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7A.5.5 0 0 1 4 8" />
</g>
</svg>
);
export const TableIcon = ({
size,
height = 24,
width = 22,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
fill="none"
height={size || height}
viewBox="0 0 24 22"
width={size || width}
strokeWidth="1.5"
stroke="currentColor"
{...props}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M3.375 19.5h17.25m-17.25 0a1.125 1.125 0 0 1-1.125-1.125M3.375 19.5h7.5c.621 0 1.125-.504 1.125-1.125m-9.75 0V5.625m0 12.75v-1.5c0-.621.504-1.125 1.125-1.125m18.375 2.625V5.625m0 12.75c0 .621-.504 1.125-1.125 1.125m1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125m0 3.75h-7.5A1.125 1.125 0 0 1 12 18.375m9.75-12.75c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125m19.5 0v1.5c0 .621-.504 1.125-1.125 1.125M2.25 5.625v1.5c0 .621.504 1.125 1.125 1.125m0 0h17.25m-17.25 0h7.5c.621 0 1.125.504 1.125 1.125M3.375 8.25c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125m17.25-3.75h-7.5c-.621 0-1.125.504-1.125 1.125m8.625-1.125c.621 0 1.125.504 1.125 1.125v1.5c0 .621-.504 1.125-1.125 1.125m-17.25 0h7.5m-7.5 0c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125M12 10.875v-1.5m0 1.5c0 .621-.504 1.125-1.125 1.125M12 10.875c0 .621.504 1.125 1.125 1.125m-2.25 0c.621 0 1.125.504 1.125 1.125M13.125 12h7.5m-7.5 0c-.621 0-1.125.504-1.125 1.125M20.625 12c.621 0 1.125.504 1.125 1.125v1.5c0 .621-.504 1.125-1.125 1.125m-17.25 0h7.5M12 14.625v-1.5m0 1.5c0 .621-.504 1.125-1.125 1.125M12 14.625c0 .621.504 1.125 1.125 1.125m-2.25 0c.621 0 1.125.504 1.125 1.125m0 1.5v-1.5m0 0c0-.621.504-1.125 1.125-1.125m0 0h7.5"
/>
</svg>
);
export const ArticleIcon = ({
size,
height = 20,
width = 20,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
height={size || height}
width={size || width}
viewBox="0 0 20 20"
{...props}
>
<path
fill="currentColor"
d="M5 1a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2zm0 3h5v1H5zm0 2h5v1H5zm0 2h5v1H5zm10 7H5v-1h10zm0-2H5v-1h10zm0-2H5v-1h10zm0-2h-4V4h4z"
/>
</svg>
);
export const MagazineIcon = ({
size,
height = 20,
width = 20,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
height={size || height}
width={size || width}
viewBox="0 0 128 128"
{...props}
>
<path
fill="#bdbdbd"
d="M-125.7 124.54V11.79c29.36 1.85 58.81 1.91 88.18.19c1.77-.1 3.21 1.08 3.21 2.66v107.04c0 1.58-1.44 2.94-3.21 3.05a727 727 0 0 1-88.18-.19"
/>
<path
fill="#e0e0e0"
d="M-125.7 124.54V11.79c27.11-5.31 54.34-8.57 81.45-9.76c1.64-.07 2.96 1.15 2.96 2.73V111.8c0 1.58-1.33 2.9-2.96 2.98c-27.11 1.19-54.34 4.45-81.45 9.76"
/>
<g fill="#757575">
<path d="M-92.84 42.86c-7.46 1-14.91 2.16-22.36 3.47v-3.22c7.45-1.31 14.9-2.47 22.36-3.47zm-12.76-15.72c-3.2.51-6.4 1.04-9.6 1.6v-8.47c3.2-.56 6.4-1.1 9.6-1.6zm12.17-1.78c-3.2.43-6.4.9-9.6 1.39v-8.47c3.2-.49 6.4-.95 9.6-1.39zm12.17-1.52q-4.8.54-9.6 1.17v-8.47q4.8-.63 9.6-1.17zm12.17-1.23c-3.2.29-6.4.61-9.6.95v-8.47c3.2-.35 6.4-.66 9.6-.95zm17.21 5.12a548 548 0 0 0-63.31 7.42v-.81c21.09-3.72 42.23-6.19 63.31-7.42zm-32.67 14.08c-2.21.26-4.41.54-6.62.83v-3.22c2.21-.29 4.41-.57 6.62-.83z" />
<path d="M-75.06 40.77c-3.6.37-7.21.77-10.81 1.2v-3.22c3.6-.44 7.21-.84 10.81-1.2zm12.29-1.11l-3.66.3v-3.22l3.66-.3z" />
<path d="M-65.38 39.87c-2.47.21-4.95.43-7.42.67v-3.22c2.47-.24 4.95-.46 7.42-.67zm10.32-.76c-2.02.13-4.05.27-6.07.42v-3.22c2.02-.15 4.05-.29 6.07-.42zm-49.89 14.57c-3.42.54-6.83 1.11-10.25 1.71v-3.22c3.41-.6 6.83-1.17 10.25-1.71zm9.51-1.4c-2.63.37-5.26.75-7.89 1.16v-3.22c2.63-.41 5.26-.79 7.89-1.16z" />
<path d="M-84.55 50.87c-3.95.47-7.89.98-11.84 1.54v-3.22c3.95-.56 7.89-1.07 11.84-1.54z" />
<path d="M-75.06 49.82c-3.6.37-7.21.77-10.81 1.2V47.8c3.6-.44 7.21-.84 10.81-1.2zm12.29-1.1l-3.66.3V45.8l3.66-.3z" />
<path d="M-61.13 48.59c-3.89.29-7.78.63-11.67 1.01v-3.22c3.89-.38 7.78-.71 11.67-1.01z" />
<path d="M-51.89 47.97c-3.08.18-6.16.39-9.25.62v-3.22c3.08-.23 6.17-.44 9.25-.62zm-49.5 14.22q-6.9 1.035-13.8 2.25v-3.22q6.9-1.215 13.8-2.25z" />
<path d="M-95.44 61.33c-2.63.37-5.26.75-7.89 1.16v-3.22c2.63-.41 5.26-.79 7.89-1.16zm10.89-1.4c-2.76.33-5.53.68-8.29 1.05v-3.22c2.76-.37 5.53-.72 8.29-1.05z" />
<path d="M-78.26 59.21c-2.54.27-5.07.56-7.61.87v-3.22c2.54-.31 5.07-.6 7.61-.87zm26.37 24.99q-12.075.705-24.18 1.95V55.76q12.09-1.245 24.18-1.95zm-38.55-14.48c-8.25 1.07-16.51 2.34-24.75 3.79v-3.22c8.24-1.45 16.5-2.72 24.75-3.79z" />
<path d="M-95.44 70.39c-1.31.18-2.63.37-3.94.56v-3.22c1.31-.19 2.63-.38 3.94-.56zm10.89-1.41c-2.21.26-4.41.54-6.62.83v-3.22c2.21-.29 4.41-.57 6.62-.83z" />
<path d="M-78.32 68.28c-2.51.27-5.03.56-7.54.86v-3.22c2.51-.31 5.03-.59 7.54-.86zm-23.07 12.03q-6.9 1.035-13.8 2.25v-3.22q6.9-1.215 13.8-2.25z" />
<path d="M-98.16 79.83c-1.72.25-3.44.51-5.17.77v-3.22c1.72-.27 3.44-.52 5.17-.77zm13.61-1.79q-5.445.645-10.89 1.41v-3.22c3.63-.51 7.26-.97 10.89-1.41z" />
<path d="M-80.46 77.57c-1.8.2-3.6.41-5.41.63v-3.22c1.8-.22 3.6-.43 5.41-.63zm-16.95 11.21c-5.93.85-11.86 1.79-17.79 2.84V88.4c5.92-1.04 11.85-1.99 17.79-2.84z" />
<path d="M-92.54 88.1c-2.28.31-4.56.62-6.84.96v-3.22q3.42-.495 6.84-.96zm7.99-1.01c-1.75.21-3.5.43-5.25.65v-3.22c1.75-.23 3.5-.44 5.25-.65z" />
<path d="M-78.32 86.39c-2.51.27-5.03.56-7.54.86v-3.22c2.51-.31 5.03-.59 7.54-.86zm-23.07 12.03q-6.9 1.035-13.8 2.25v-3.22q6.9-1.215 13.8-2.25zm14.22-1.95q-6.105.75-12.21 1.65V94.9q6.105-.9 12.21-1.65z" />
<path d="M-84.55 96.15c-2.21.26-4.41.54-6.62.83v-3.22c2.21-.29 4.41-.57 6.62-.83z" />
<path d="M-75.06 95.1c-3.6.37-7.21.77-10.81 1.2v-3.22c3.6-.44 7.21-.84 10.81-1.2zm12.29-1.1l-3.66.3v-3.22l3.66-.3zm-5.64.47c-1.46.13-2.93.27-4.39.41v-3.22c1.46-.14 2.93-.28 4.39-.41z" />
<path d="M-51.89 93.25c-6 .35-12.01.8-18.01 1.35v-3.22c6.01-.55 12.01-1 18.01-1.35zm-43.56 13.36c-6.59.92-13.17 1.96-19.75 3.11v-3.22c6.58-1.16 13.16-2.2 19.75-3.11zm22.09-2.62c-3.48.34-6.96.72-10.44 1.13v-3.22c3.48-.41 6.96-.78 10.44-1.13zm-13.53 1.5c-2.18.27-4.36.55-6.55.85v-3.22c2.18-.3 4.36-.58 6.55-.85z" />
</g>
<path
fill="#eee"
d="M15.71 280.41V170.86h76.08a2.77 2.77 0 0 1 2.77 2.77v104.01a2.77 2.77 0 0 1-2.77 2.77z"
/>
<g fill="#757575">
<path d="M25.53 203.19h20.88v3.13H25.53zm0-22.19h8.96v8.23h-8.96zm11.36 0h8.96v8.23h-8.96zm11.36 0h8.96v8.23h-8.96zm11.36 0h8.96v8.23h-8.96zm-34.08 13.66h59.12v.79H25.53zm22.44 8.53h6.18v3.13h-6.18z" />
<path d="M52.92 203.19h10.09v3.13H52.92zm18.14 0h3.42v3.13h-3.42z" />
<path d="M65.11 203.19h6.93v3.13h-6.93zm10.9 0h5.67v3.13h-5.67zm-50.48 8.8h9.57v3.13h-9.57zm11.08 0h7.37v3.13h-7.37z" />
<path d="M43.1 211.99h11.05v3.13H43.1z" />
<path d="M52.92 211.99h10.09v3.13H52.92zm18.14 0h3.42v3.13h-3.42z" />
<path d="M65.11 211.99H76v3.13H65.11zm10.9 0h8.64v3.13h-8.64zm-50.48 8.8h12.89v3.13H25.53z" />
<path d="M36.61 220.79h7.37v3.13h-7.37zm9.8 0h7.74v3.13h-7.74z" />
<path d="M52.92 220.79h7.1v3.13h-7.1zm9.15 0h22.58v29.53H62.07zm-36.54 8.8h23.11v3.13H25.53z" />
<path d="M40.3 229.59h3.68v3.13H40.3zm7.67 0h6.18v3.13h-6.18z" />
<path d="M52.92 229.59h7.04v3.13h-7.04zm-27.39 8.8h12.89v3.13H25.53z" />
<path d="M36.61 238.39h4.82v3.13h-4.82zm7.37 0h10.17v3.13H43.98z" />
<path d="M52.92 238.39h5.04v3.13h-5.04zm-27.39 8.79h16.61v3.13H25.53z" />
<path d="M40.3 247.18h6.38v3.13H40.3zm8.95 0h4.9v3.13h-4.9z" />
<path d="M52.92 247.18h7.04v3.13h-7.04zm-27.39 8.8h12.89v3.13H25.53zm14.77 0h11.39v3.13H40.3z" />
<path d="M47.97 255.98h6.18v3.13h-6.18z" />
<path d="M52.92 255.98h10.09v3.13H52.92zm18.14 0h3.42v3.13h-3.42zm-5.95 0h4.1v3.13h-4.1z" />
<path d="M67.82 255.98h16.82v3.13H67.82zm-42.29 8.8h18.44v3.13H25.53zm29.32 0h9.74v3.13h-9.74zm-9 0h6.11v3.13h-6.11z" />
</g>
<path
fill="#bdbdbd"
d="M16.62 124.27V14.04c30.52 2.2 61.18 2.27 91.71.21c1.68-.11 3.05 1.04 3.05 2.58v104.65c0 1.54-1.36 2.89-3.05 3a659 659 0 0 1-91.71-.21"
/>
<path
fill="#e0e0e0"
d="M16.62 124.25V14.02C44.36 7.91 72.21 3.9 99.95 2.03c1.53-.1 2.77 1.07 2.77 2.61v104.65c0 1.54-1.24 2.87-2.77 2.97c-27.74 1.87-55.59 5.88-83.33 11.99"
/>
<path
fill="none"
stroke="#616161"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="10"
strokeWidth="5"
d="M28.75 49.34c20.6-4.08 41.25-7 61.84-8.74M28.75 63.23a565 565 0 0 1 26.45-4.6M28.75 77.11a565 565 0 0 1 26.45-4.6m-26.45 33.06c20.6-4.08 41.25-7 61.84-8.74M28.75 91a565 565 0 0 1 26.45-4.6"
/>
<path
fill="#616161"
d="M64.86 87.55a560 560 0 0 1 24.67-2.69c1.49-.13 2.69-1.44 2.69-2.94V54.54c0-1.5-1.21-2.61-2.69-2.48c-8.22.71-16.44 1.61-24.67 2.69c-1.49.2-2.7 1.58-2.7 3.07V85.2c.01 1.5 1.21 2.55 2.7 2.35m-34.4-52.14c2.03-.4 4.05-.78 6.08-1.15c1.49-.27 2.69-1.7 2.69-3.2v-7.02c0-1.5-1.21-2.49-2.69-2.22c-2.03.37-4.05.76-6.08 1.15c-1.49.29-2.69 1.75-2.69 3.24v7.02c-.01 1.5 1.2 2.47 2.69 2.18m15.96-2.88c2.03-.34 4.05-.66 6.08-.97c1.49-.23 2.7-1.62 2.7-3.12v-7.02c0-1.5-1.21-2.53-2.7-2.3c-2.03.31-4.06.64-6.08.97c-1.49.25-2.69 1.67-2.69 3.16v7.02c0 1.5 1.2 2.51 2.69 2.26m15.97-2.41c2.03-.28 4.06-.54 6.08-.8c1.49-.19 2.7-1.54 2.7-3.04v-7.02c0-1.5-1.21-2.57-2.7-2.38c-2.03.25-4.06.52-6.08.8c-1.49.2-2.7 1.59-2.7 3.08v7.02c.01 1.5 1.22 2.54 2.7 2.34"
/>
<path
fill="#e0e0e0"
d="M374.07 165.73V44.63h92.1a3.06 3.06 0 0 1 3.06 3.06v114.98a3.06 3.06 0 0 1-3.06 3.06z"
/>
<path
fill="none"
stroke="#616161"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="10"
strokeWidth="5"
d="M387.48 86.21h68.34m-68.34 15.26h29.23m-29.23 15.26h29.23M387.48 148h68.34m-68.34-16.01h29.23"
/>
<path
fill="#616161"
d="M427.38 134.75h27.26c1.64 0 2.98-1.33 2.98-2.98v-30.08c0-1.64-1.33-2.98-2.98-2.98h-27.26c-1.64 0-2.98 1.33-2.98 2.98v30.08a2.987 2.987 0 0 0 2.98 2.98m-38.01-63.47h6.72c1.64 0 2.98-1.33 2.98-2.98v-7.71c0-1.64-1.33-2.98-2.98-2.98h-6.72c-1.64 0-2.98 1.33-2.98 2.98v7.71c0 1.65 1.33 2.98 2.98 2.98m17.64 0h6.72c1.64 0 2.98-1.33 2.98-2.98v-7.71c0-1.64-1.33-2.98-2.98-2.98h-6.72c-1.64 0-2.98 1.33-2.98 2.98v7.71a2.987 2.987 0 0 0 2.98 2.98m17.65 0h6.72c1.64 0 2.98-1.33 2.98-2.98v-7.71c0-1.64-1.33-2.98-2.98-2.98h-6.72c-1.64 0-2.98 1.33-2.98 2.98v7.71c0 1.65 1.33 2.98 2.98 2.98"
/>
<path
fill="#bdbdbd"
d="M479.86 165.73V44.63h92.1a3.06 3.06 0 0 1 3.06 3.06v114.98a3.06 3.06 0 0 1-3.06 3.06z"
/>
</svg>
);
export const StaticPageIcon = ({
size,
height = 20,
width = 20,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size || width}
height={size || height}
viewBox="0 0 2048 2048"
{...props}
>
<path
fill="currentColor"
d="M1755 512h-475V37zm37 128v1408H128V0h1024v640z"
/>
</svg>
);
export const MasterUsersIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size || width}
height={size || height}
viewBox="0 0 640 512"
{...props}
>
<path
fill="currentColor"
d="M144 0a80 80 0 1 1 0 160a80 80 0 1 1 0-160m368 0a80 80 0 1 1 0 160a80 80 0 1 1 0-160M0 298.7C0 239.8 47.8 192 106.7 192h42.7c15.9 0 31 3.5 44.6 9.7c-1.3 7.2-1.9 14.7-1.9 22.3c0 38.2 16.8 72.5 43.3 96H21.3C9.6 320 0 310.4 0 298.7M405.3 320h-.7c26.6-23.5 43.3-57.8 43.3-96c0-7.6-.7-15-1.9-22.3c13.6-6.3 28.7-9.7 44.6-9.7h42.7c58.9 0 106.7 47.8 106.7 106.7c0 11.8-9.6 21.3-21.3 21.3H405.4zM224 224a96 96 0 1 1 192 0a96 96 0 1 1-192 0m-96 261.3c0-73.6 59.7-133.3 133.3-133.3h117.3c73.7 0 133.4 59.7 133.4 133.3c0 14.7-11.9 26.7-26.7 26.7H154.6c-14.7 0-26.7-11.9-26.7-26.7z"
/>
</svg>
);
export const MasterRoleIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size || width}
height={size || height}
viewBox="0 0 24 24"
{...props}
>
<path
fill="currentColor"
d="M15 21h-2a2 2 0 0 1 0-4h2v-2h-2a4 4 0 0 0 0 8h2Zm8-2a4 4 0 0 1-4 4h-2v-2h2a2 2 0 0 0 0-4h-2v-2h2a4 4 0 0 1 4 4"
/>
<path
fill="currentColor"
d="M14 18h4v2h-4zm-7 1a6 6 0 0 1 .09-1H3v-1.4c0-2 4-3.1 6-3.1a8.6 8.6 0 0 1 1.35.125A5.95 5.95 0 0 1 13 13h5V4a2.006 2.006 0 0 0-2-2h-4.18a2.988 2.988 0 0 0-5.64 0H2a2.006 2.006 0 0 0-2 2v14a2.006 2.006 0 0 0 2 2h5.09A6 6 0 0 1 7 19M9 2a1 1 0 1 1-1 1a1.003 1.003 0 0 1 1-1m0 4a3 3 0 1 1-3 3a2.996 2.996 0 0 1 3-3"
/>
</svg>
);
export const MasterUserLevelIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size || width}
height={size || height}
viewBox="0 0 640 512"
{...props}
>
<path
fill="currentColor"
d="M192 256c61.9 0 112-50.1 112-112S253.9 32 192 32S80 82.1 80 144s50.1 112 112 112m76.8 32h-8.3c-20.8 10-43.9 16-68.5 16s-47.6-6-68.5-16h-8.3C51.6 288 0 339.6 0 403.2V432c0 26.5 21.5 48 48 48h288c26.5 0 48-21.5 48-48v-28.8c0-63.6-51.6-115.2-115.2-115.2M480 256c53 0 96-43 96-96s-43-96-96-96s-96 43-96 96s43 96 96 96m48 32h-3.8c-13.9 4.8-28.6 8-44.2 8s-30.3-3.2-44.2-8H432c-20.4 0-39.2 5.9-55.7 15.4c24.4 26.3 39.7 61.2 39.7 99.8v38.4c0 2.2-.5 4.3-.6 6.4H592c26.5 0 48-21.5 48-48c0-61.9-50.1-112-112-112"
/>
</svg>
);
export const MasterCategoryIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size || width}
height={size || height}
viewBox="0 0 32 32"
{...props}
>
<path
fill="currentColor"
d="M14 25h14v2H14zm-6.83 1l-2.58 2.58L6 30l4-4l-4-4l-1.42 1.41zM14 15h14v2H14zm-6.83 1l-2.58 2.58L6 20l4-4l-4-4l-1.42 1.41zM14 5h14v2H14zM7.17 6L4.59 8.58L6 10l4-4l-4-4l-1.42 1.41z"
/>
</svg>
);
export const AddvertiseIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size || width}
height={size || height}
{...props}
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M12 8q-1.65 0-2.825 1.175T8 12q0 1.125.563 2.075t1.562 1.475q.4.2.563.587t-.013.788q-.175.35-.525.525t-.7 0q-1.575-.75-2.512-2.225T6 12q0-2.5 1.75-4.25T12 6q1.775 0 3.263.938T17.475 9.5q.15.35-.012.7t-.513.5q-.4.175-.8 0t-.6-.575q-.525-1-1.475-1.562T12 8m0-4Q8.65 4 6.325 6.325T4 12q0 3.15 2.075 5.4t5.2 2.55q.425.05.737.375t.288.75t-.313.7t-.712.25q-1.95-.125-3.638-.975t-2.95-2.213t-1.975-3.125T2 12q0-2.075.788-3.9t2.137-3.175T8.1 2.788T12 2q3.925 0 6.838 2.675t3.187 6.6q.05.4-.237.688t-.713.312t-.762-.275t-.388-.725q-.375-3-2.612-5.137T12 4m7.55 17.5l-3.3-3.275l-.75 2.275q-.125.35-.475.338t-.475-.363L12.275 12.9q-.1-.275.125-.5t.5-.125l7.575 2.275q.35.125.363.475t-.338.475l-2.275.75l3.3 3.3q.425.425.425.975t-.425.975t-.987.425t-.988-.425"
/>
</svg>
);
export const SuggestionsIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size || width}
height={size || height}
{...props}
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M17.175 14H7.5q-1.875 0-3.187-1.312T3 9.5t1.313-3.187T7.5 5q.425 0 .713.288T8.5 6t-.288.713T7.5 7q-1.05 0-1.775.725T5 9.5t.725 1.775T7.5 12h9.675L14.3 9.1q-.275-.275-.288-.687T14.3 7.7q.275-.275.7-.275t.7.275l4.6 4.6q.3.3.3.7t-.3.7l-4.6 4.6q-.3.3-.7.288t-.7-.313q-.275-.3-.288-.7t.288-.7z"
/>
</svg>
);
export const CommentIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size || width}
height={size || height}
{...props}
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M6.5 13.5h11v-1h-11zm0-3h11v-1h-11zm0-3h11v-1h-11zM4.616 17q-.691 0-1.153-.462T3 15.385V4.615q0-.69.463-1.153T4.615 3h14.77q.69 0 1.152.462T21 4.615v15.462L17.923 17z"
/>
</svg>
);

View File

@ -0,0 +1,99 @@
"use client";
import { ChevronLeft } from "lucide-react";
export default function FilterImageSidebar() {
return (
<div className="bg-white rounded-2xl border border-gray-200 shadow-sm p-6">
{/* HEADER */}
<div className="flex items-center justify-between pb-4 border-b">
<h3 className="font-semibold text-sm flex items-center gap-2">
Filter
</h3>
<ChevronLeft size={16} className="text-gray-400" />
</div>
{/* CONTENT */}
<div className="space-y-6 mt-6">
{/* KATEGORI */}
<FilterSection title="Kategori">
<Checkbox label="Semua" count={1203} defaultChecked />
<Checkbox label="Berita Terhangat" count={123} />
<Checkbox label="Tentang Teknologi" count={24} />
<Checkbox label="Bersama Pelanggan" count={42} />
<Checkbox label="Pembicara Ahli" count={224} />
</FilterSection>
<Divider />
{/* JENIS FILE */}
<FilterSection title="Jenis File">
<Checkbox label="Semua" count={78} />
<Checkbox label="Audio Visual" count={120} />
<Checkbox label="Audio" count={34} />
<Checkbox label="Foto" count={234} defaultChecked />
<Checkbox label="Teks" count={9} />
</FilterSection>
<Divider />
{/* FORMAT */}
<FilterSection title="Format Foto ">
<Checkbox label="Semua" count={2} defaultChecked />
</FilterSection>
{/* RESET */}
<div className="text-center pt-4">
<button className="text-sm text-[#966314] font-medium hover:underline">
Reset Filter
</button>
</div>
</div>
</div>
);
}
/* ===== COMPONENTS ===== */
function FilterSection({
title,
children,
}: {
title: string;
children: React.ReactNode;
}) {
return (
<div>
<p className="text-sm font-medium mb-3">{title}</p>
<div className="space-y-2">{children}</div>
</div>
);
}
function Checkbox({
label,
count,
defaultChecked,
}: {
label: string;
count: number;
defaultChecked?: boolean;
}) {
return (
<label className="flex items-center justify-between text-sm cursor-pointer">
<div className="flex items-center gap-2">
<input
type="checkbox"
defaultChecked={defaultChecked}
className="h-4 w-4 accent-[#966314]"
/>
<span>{label}</span>
</div>
<span className="text-gray-400">({count})</span>
</label>
);
}
function Divider() {
return <div className="border-t border-gray-200"></div>;
}

View File

@ -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 (
<Link href={`/details/${slug}?type=image`}>
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm hover:shadow-md transition duration-300 overflow-hidden">
{/* IMAGE */}
<div className="relative h-[200px] w-full">
<Image
src="/image/novita2.png"
alt="news"
fill
className="object-cover"
/>
</div>
{/* CONTENT */}
<div className="p-5 space-y-3">
{/* BADGE + TAG */}
<div className="flex items-center gap-2 text-xs">
<span className="bg-red-600 text-white px-2 py-[3px] rounded-md font-medium">
POLRI
</span>
<span className="text-gray-500 uppercase tracking-wide">
SEPUTAR PRESTASI
</span>
</div>
{/* DATE */}
<p className="text-xs text-gray-400">02 Februari 2024</p>
{/* TITLE */}
<h3 className="font-semibold text-[15px] leading-snug line-clamp-2 hover:text-[#966314] transition">
Bharatu Mardi Hadji Gugur Saat Bertugas, Diganjar Kenaikan Pangkat
Luar Biasa
</h3>
{/* EXCERPT */}
<p className="text-sm text-gray-500 line-clamp-2 leading-relaxed">
Jakarta - Kapolri Jenderal Polisi Drs. Listyo Sigit Prabowo
memberikan kenaikan pangkat luar biasa anumerta kepada...
</p>
</div>
</div>
</Link>
);
}

View File

@ -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 (
<section className="relative bg-[#f7f0e3] py-24">
{/* TOP CENTER CONTENT */}
<div className="absolute left-1/2 top-8 flex -translate-x-1/2 flex-col items-center gap-6">
<p className="text-sm font-semibold uppercase tracking-widest text-gray-400">
Manage All your channels from Multipool
</p>
{/* SOCIAL ICONS */}
<div className="flex gap-6">
{socials.map((item) => (
<div
key={item.name}
className="flex items-center justify-center rounded-full"
>
<Image src={item.icon} alt={item.name} width={40} height={40} />
</div>
))}
</div>
</div>
<div className="container mx-auto grid grid-cols-1 items-center gap-16 px-6 md:grid-cols-2">
{/* PHONE IMAGE */}
<div className="flex justify-center">
<Image
src="/image/phone.png"
alt="App Preview"
width={320}
height={640}
className="object-contain"
/>
</div>
{/* TEXT CONTENT */}
<div className="max-w-xl">
<p className="mb-4 text-sm font-semibold uppercase tracking-widest text-gray-400">
About Us
</p>
<h2 className="mb-6 text-4xl font-extrabold leading-tight">
Helping you find the right{" "}
<span className="relative inline-block">
<span className="absolute bottom-1 left-0 h-3 w-full bg-[#966314]/40"></span>
<span className="relative">Solution</span>
</span>
</h2>
<p className="text-sm leading-relaxed text-gray-600">
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.
</p>
</div>
</div>
</section>
);
}

View File

@ -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 (
<section className="py-20 ">
<div className="container mx-auto px-6">
{/* ===== Title ===== */}
<h2 className="text-3xl font-bold text-center mb-12">
Kategori Konten
</h2>
{/* ===== Card ===== */}
<Card className="rounded-2xl shadow-xl border-1 ">
<CardContent className="p-10 space-y-8">
{categories.map((item, index) => (
<div key={index} className="flex items-center justify-between">
{/* Left */}
<div className="flex items-center gap-4">
{/* Bullet */}
<div
className={`w-3 h-3 rounded-full ${
index % 2 === 0
? "bg-[#0f3b63]" // biru tua
: "bg-[#b07c18]" // gold
}`}
/>
<span className="text-lg text-[#0f3b63] font-medium">
{item.name}
</span>
</div>
{/* Right total */}
<span className="text-gray-500 text-lg">{item.total}</span>
</div>
))}
</CardContent>
</Card>
</div>
</section>
);
}

View File

@ -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 (
<section className=" py-20">
<div className="container mx-auto px-6">
{/* ===== HEADER ===== */}
<div className="flex flex-col items-center mb-12">
<h2 className="text-3xl font-bold mb-6">Konten Terbaru</h2>
<Tabs defaultValue="audio-visual" className="w-full">
{/* Tabs + Explore */}
<div className="relative w-full pb-3 ">
{/* Tabs Center */}
<div className="flex justify-center">
<TabsList className="bg-transparent p-0 gap-8" variant={"line"}>
<TabsTrigger
value="audio-visual"
className="data-[state=active]:text-[#b07c18] px-0 pb-2"
>
Audio Visual
</TabsTrigger>
<TabsTrigger
value="audio"
className="data-[state=active]:text-[#b07c18] px-0 pb-2"
>
Audio
</TabsTrigger>
<TabsTrigger
value="foto"
className="data-[state=active]:text-[#b07c18] px-0 pb-2"
>
Foto
</TabsTrigger>
<TabsTrigger
value="teks"
className="data-[state=active]:text-[#b07c18] px-0 pb-2"
>
Teks
</TabsTrigger>
</TabsList>
</div>
{/* Explore Right */}
<div className="hidden md:block absolute right-0 top-1/2 -translate-y-1/2 text-sm text-muted-foreground hover:text-black cursor-pointer">
Explore more Trending
</div>
</div>
{/* ===== CONTENT ===== */}
<TabsContent value="audio-visual" className="mt-12">
<CardGrid />
</TabsContent>
<TabsContent value="audio" className="mt-12">
<CardGrid />
</TabsContent>
<TabsContent value="foto" className="mt-12">
<CardGrid />
</TabsContent>
<TabsContent value="teks" className="mt-12">
<CardGrid />
</TabsContent>
</Tabs>
</div>
</div>
</section>
);
}
/* ================= CARD GRID ================= */
function CardGrid() {
return (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8">
{data.map((item) => (
<div
key={item.id}
className="bg-white rounded-2xl overflow-hidden shadow-sm hover:shadow-lg transition-all duration-300"
>
<div className="relative h-[220px]">
<Image
src={item.image}
alt={item.title}
fill
className="object-cover"
/>
</div>
<div className="p-5 space-y-3">
<div className="flex items-center gap-3 flex-wrap">
<Badge
className={`${item.categoryColor} text-white text-xs px-2 py-1`}
>
{item.category}
</Badge>
<span className="text-xs text-muted-foreground">{item.tag}</span>
</div>
<p className="text-xs text-muted-foreground">{item.date}</p>
<h3 className="text-sm font-semibold leading-snug line-clamp-3 hover:text-[#b07c18] transition">
{item.title}
</h3>
</div>
</div>
))}
</div>
);
}

View File

@ -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 (
<section className="py-20">
<div className="container mx-auto px-6">
{/* ===== HEADER ===== */}
<div className="flex flex-col items-center mb-12">
<h2 className="text-3xl font-bold mb-6">Konten Terpopuler</h2>
<Tabs defaultValue="audio-visual" className="w-full">
{/* Tabs + Explore */}
<div className="relative w-full pb-3 ">
{/* Tabs Center */}
<div className="flex justify-center">
<TabsList className="bg-transparent p-0 gap-8" variant={"line"}>
<TabsTrigger
value="audio-visual"
className="data-[state=active]:text-[#b07c18] px-0 pb-2"
>
Audio Visual
</TabsTrigger>
<TabsTrigger
value="audio"
className="data-[state=active]:text-[#b07c18] px-0 pb-2"
>
Audio
</TabsTrigger>
<TabsTrigger
value="foto"
className="data-[state=active]:text-[#b07c18] px-0 pb-2"
>
Foto
</TabsTrigger>
<TabsTrigger
value="teks"
className="data-[state=active]:text-[#b07c18] px-0 pb-2"
>
Teks
</TabsTrigger>
</TabsList>
</div>
{/* Explore Right */}
<div className="hidden md:block absolute right-0 top-1/2 -translate-y-1/2 text-sm text-muted-foreground hover:text-black cursor-pointer">
Explore more Trending
</div>
</div>
{/* ===== CONTENT ===== */}
<TabsContent value="audio-visual" className="mt-12">
<CardGrid />
</TabsContent>
<TabsContent value="audio" className="mt-12">
<CardGrid />
</TabsContent>
<TabsContent value="foto" className="mt-12">
<CardGrid />
</TabsContent>
<TabsContent value="teks" className="mt-12">
<CardGrid />
</TabsContent>
</Tabs>
</div>
</div>
</section>
);
}
/* ================= CARD GRID ================= */
function CardGrid() {
return (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8">
{data.map((item) => (
<div
key={item.id}
className="bg-white rounded-2xl overflow-hidden shadow-sm hover:shadow-lg transition-all duration-300"
>
<div className="relative h-[220px]">
<Image
src={item.image}
alt={item.title}
fill
className="object-cover"
/>
</div>
<div className="p-5 space-y-3">
<div className="flex items-center gap-3 flex-wrap">
<Badge
className={`${item.categoryColor} text-white text-xs px-2 py-1`}
>
{item.category}
</Badge>
<span className="text-xs text-muted-foreground">{item.tag}</span>
</div>
<p className="text-xs text-muted-foreground">{item.date}</p>
<h3 className="text-sm font-semibold leading-snug line-clamp-3 hover:text-[#b07c18] transition">
{item.title}
</h3>
</div>
</div>
))}
</div>
);
}

View File

@ -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 */}
<button
onClick={() => setOpen(true)}
className="fixed right-6 top-6 z-[100] flex h-12 w-12 items-center justify-center rounded-md bg-[#966314] text-white shadow-lg"
>
<Menu />
</button>
{/* OVERLAY */}
{open && (
<div
onClick={() => setOpen(false)}
className="fixed inset-0 z-[90] bg-black/40"
/>
)}
{/* SIDEBAR */}
<aside
className={`fixed right-0 top-0 z-[110] h-full w-[280px] bg-[#8a5a0c] text-white transition-transform duration-300 ${
open ? "translate-x-0" : "translate-x-full"
}`}
>
{/* HEADER */}
<div className="flex items-center justify-between border-b border-white/20 p-6">
<div className="flex rounded-full bg-white text-sm font-semibold text-[#8a5a0c]">
<button className="rounded-full bg-white px-3 py-1">ID</button>
<button className="px-3 py-1">EN</button>
</div>
<button onClick={() => setOpen(false)}>
<X />
</button>
</div>
{/* MENU */}
<nav className="flex flex-col text-base font-medium">
{/* HOME */}
<Link href="/news-services">
<div className="flex items-center justify-between border-b border-white/20 px-6 py-5 hover:bg-white/10 transition">
<span>Home</span>
<Home size={20} />
</div>
</Link>
{/* KONTEN */}
<div>
<button
onClick={() => setOpenKonten(!openKonten)}
className="flex w-full items-center justify-between border-b border-white/20 px-6 py-5 hover:bg-white/10 transition"
>
<span>Konten</span>
<ChevronDown
size={20}
className={`transition-transform duration-300 ${
openKonten ? "rotate-180" : ""
}`}
/>
</button>
{/* SUBMENU */}
<div
className={`overflow-hidden bg-[#a8772a] transition-all duration-300 ${
openKonten ? "max-h-60" : "max-h-0"
}`}
>
<Link href={"/video/filter"}>
<SubMenuItem icon={<Video size={18} />} label="Audio Visual" />
</Link>
<Link href={"/audio/filter"}>
<SubMenuItem icon={<Music size={18} />} label="Audio" />
</Link>
<Link href={"/image/filter"}>
<SubMenuItem icon={<ImageIcon size={18} />} label="Foto" />
</Link>
<Link href={"/document/filter"}>
<SubMenuItem icon={<FileText size={18} />} label="Teks" />
</Link>
</div>
</div>
</nav>
</aside>
</>
);
}
/* ================= SUBMENU ================= */
function SubMenuItem({
icon,
label,
}: {
icon: React.ReactNode;
label: string;
}) {
return (
<div className="flex items-center justify-between px-8 py-4 text-sm hover:bg-white/20 transition cursor-pointer">
<span>{label}</span>
{icon}
</div>
);
}

View File

@ -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 */}
<button
onClick={() => setOpen(true)}
className="fixed right-6 top-6 z-[100] flex h-12 w-12 items-center justify-center rounded-md bg-[#966314] text-white shadow-lg"
>
<Menu />
</button>
{/* OVERLAY */}
{open && (
<div
onClick={() => setOpen(false)}
className="fixed inset-0 z-[90] bg-black/40"
/>
)}
{/* SIDEBAR */}
<aside
className={`fixed right-0 top-0 z-[110] h-full w-[280px] bg-[#966314] text-white transition-transform duration-300 ${
open ? "translate-x-0" : "translate-x-full"
}`}
>
{/* HEADER */}
<div className="flex items-center justify-between border-b border-white/20 p-6">
<div className="flex rounded-full bg-white text-sm font-semibold text-[#966314]">
<button className="rounded-full bg-white px-3 py-1">ID</button>
<button className="px-3 py-1">EN</button>
</div>
<button onClick={() => setOpen(false)}>
<X />
</button>
</div>
{/* MENU */}
<nav className="flex flex-col gap-6 p-6 text-sm font-medium">
<MenuItem icon={<Home size={18} />} label="Home" />
<MenuItem icon={<Box size={18} />} label="Product" />
<MenuItem icon={<Briefcase size={18} />} label="Services" />
<Link href="/news-services?highlight=1">
<MenuItem
icon={<Newspaper size={18} />}
label="News and Services"
/>
</Link>
<Link href="/auth">
<MenuItem icon={<LogIn size={18} />} label="Login" />
</Link>
</nav>
</aside>
</>
);
}
function MenuItem({ icon, label }: { icon: React.ReactNode; label: string }) {
return (
<div className="flex cursor-pointer items-center justify-between border-b border-white/20 pb-3">
<span>{label}</span>
{icon}
</div>
);
}

View File

@ -0,0 +1,93 @@
import Image from "next/image";
import { Mail, Facebook, Twitter, Youtube, Instagram } from "lucide-react";
export default function Footer() {
return (
<footer className="bg-[#9c6a16] text-white">
<div className="container mx-auto px-6 py-16">
<div className="grid gap-12 md:grid-cols-5">
{/* Logo */}
<div className="md:col-span-1">
<div className="flex items-center gap-3">
<Image
src="/image/qudo1.png"
alt="Qudoco"
width={150}
height={150}
/>
</div>
</div>
{/* Information */}
<div>
<h4 className="mb-4 font-semibold">Information</h4>
<ul className="space-y-2 text-sm opacity-90">
<li>Home</li>
<li>Blog</li>
<li>Document</li>
</ul>
</div>
{/* Product */}
<div>
<h4 className="mb-4 font-semibold">Product</h4>
<ul className="space-y-2 text-sm opacity-90">
<li>MediaHUB Content Aggregator</li>
<li>Multipool Reputation Management</li>
<li>PR Room Opinion Management</li>
</ul>
</div>
{/* Service */}
<div>
<h4 className="mb-4 font-semibold">Service</h4>
<ul className="space-y-2 text-sm opacity-90">
<li>Artifintel</li>
<li>Produksi Video Animasi</li>
<li>Reelithic</li>
<li>Qudoin</li>
<li>Talkshow AI</li>
</ul>
</div>
{/* Get in Touch */}
<div>
<h4 className="mb-4 font-semibold">Get in Touch.</h4>
<p className="mb-4 text-sm opacity-90">
Indonesia India USA Oman
</p>
{/* Email */}
<div className="flex items-center rounded-full bg-white px-4 py-2 text-gray-700">
<Mail size={16} className="mr-2" />
<span className="text-sm flex-1">sales@qudoco.com</span>
<span className="ml-2 font-semibold"></span>
</div>
{/* Social */}
<div className="mt-5 flex gap-3">
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-white text-[#9c6a16]">
<Facebook size={16} />
</div>
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-white text-[#9c6a16]">
<Twitter size={16} />
</div>
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-white text-[#9c6a16]">
<Youtube size={16} />
</div>
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-white text-[#9c6a16]">
<Instagram size={16} />
</div>
</div>
</div>
</div>
{/* Divider */}
<div className="mt-14 border-t border-white/20 pt-6 text-center text-xs opacity-80">
© 2024 Copyrights by company. All Rights Reserved. Designed by{" "}
<span className="font-semibold">Qudoco Team</span>
</div>
</div>
</footer>
);
}

View File

@ -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 ================= */}
<section className="relative w-full bg-[#f8f8f8] py-24">
<div className="container mx-auto px-6 relative">
{/* ===== OUTER NAVIGATION ===== */}
<button
onClick={headerPrev}
className="hidden lg:flex absolute -left-6 top-1/2 -translate-y-1/2 w-12 h-12 rounded-full bg-white shadow-md items-center justify-center z-10"
>
<ChevronLeft />
</button>
<button
onClick={headerNext}
className="hidden lg:flex absolute -right-6 top-1/2 -translate-y-1/2 w-12 h-12 rounded-full bg-white shadow-md items-center justify-center z-10"
>
<ChevronRight />
</button>
<div className="flex flex-col lg:flex-row items-center gap-14">
{/* IMAGE */}
<div className="relative w-full lg:w-1/2">
<div className="relative h-[420px] rounded-3xl overflow-hidden">
<Image
src={data1[activeHeader].image}
alt={data1[activeHeader].title}
fill
className="object-cover"
priority
/>
</div>
{/* DOTS */}
<div className="flex justify-center gap-2 mt-4">
{data1.map((_, i) => (
<span
key={i}
className={`h-2.5 rounded-full transition-all ${
activeHeader === i
? "w-8 bg-[#b07c18]"
: "w-2.5 bg-gray-300"
}`}
/>
))}
</div>
</div>
{/* CONTENT */}
<div className="w-full lg:w-1/2">
<h1 className="text-4xl lg:text-5xl font-bold leading-tight mb-6">
{data1[activeHeader].title}
</h1>
<div className="flex flex-wrap items-center gap-3 text-sm text-gray-500 mb-5">
<span>{data1[activeHeader].date}</span>
<span></span>
<span>{data1[activeHeader].category}</span>
<span></span>
<span className="px-2 py-0.5 rounded bg-[#f2c94c] text-black text-xs font-semibold">
{data1[activeHeader].tag}
</span>
</div>
<p className="text-gray-700 leading-relaxed mb-8">
{data1[activeHeader].excerpt}
</p>
<button
onClick={() => setOpen(true)}
className="inline-flex items-center justify-center rounded-xl bg-[#b07c18] px-7 py-3 text-white font-medium hover:opacity-90 transition"
>
Baca Selengkapnya
</button>
</div>
</div>
{/* ===== SEARCH SECTION ===== */}
<div className="mt-20">
<div className="max-w-3xl mx-auto flex items-center bg-white rounded-2xl shadow-md overflow-hidden border">
<div className="px-4 text-gray-400">🔍</div>
<input
type="text"
placeholder="Cari berita, artikel, atau topik..."
className="flex-1 px-4 py-4 outline-none"
/>
<button className="bg-[#b07c18] text-white px-8 py-4 font-medium">
Cari
</button>
</div>
</div>
</div>
</section>
{/* ================= MODAL ================= */}
<AnimatePresence>
{open && (
<motion.div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
<motion.div
className="relative w-[90%] max-w-5xl rounded-3xl overflow-hidden bg-[#9c8414]"
initial={{ scale: 0.95, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.95, opacity: 0 }}
>
<button
onClick={closeModal}
className="absolute top-4 right-4 z-10 w-10 h-10 rounded-full bg-black/40 text-white flex items-center justify-center"
>
<X />
</button>
<div className="relative h-[520px]">
<Image
src={data[activeModal].image}
alt={data[activeModal].title}
fill
className="object-cover"
priority
/>
<div className="absolute bottom-6 left-6 right-6 text-white">
<h2 className="text-xl md:text-2xl font-semibold">
{data[activeModal].title}
</h2>
</div>
<button
onClick={modalPrev}
className="absolute left-4 top-1/2 -translate-y-1/2 w-10 h-10 rounded-full bg-black/40 text-white"
>
<ChevronLeft />
</button>
<button
onClick={modalNext}
className="absolute right-4 top-1/2 -translate-y-1/2 w-10 h-10 rounded-full bg-black/40 text-white"
>
<ChevronRight />
</button>
</div>
<div className="flex justify-center gap-2 py-4">
{data.map((_, i) => (
<button
key={i}
onClick={() => setActiveModal(i)}
className={`w-2.5 h-2.5 rounded-full ${
activeModal === i ? "bg-white" : "bg-white/40"
}`}
/>
))}
</div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
</>
);
}

View File

@ -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 (
<header className="relative w-full bg-white overflow-hidden">
<aside
className={`fixed right-0 top-0 z-50 h-full w-[280px] bg-[#966314] text-white transition-transform duration-300 ${
open ? "translate-x-0" : "translate-x-full"
}`}
>
<div className="flex items-center justify-between border-b border-white/20 p-6">
<div className="flex rounded-full bg-white text-sm font-semibold text-[#966314]">
<button className="rounded-full bg-white px-3 py-1">ID</button>
<button className="px-3 py-1">EN</button>
</div>
<button onClick={() => setOpen(false)}>
<X />
</button>
</div>
<nav className="flex flex-col gap-6 p-6 text-sm font-medium">
<MenuItem icon={<Home size={18} />} label="Home" />
<MenuItem icon={<Box size={18} />} label="Product" />
<MenuItem icon={<Briefcase size={18} />} label="Services" />
<MenuItem icon={<Newspaper size={18} />} label="News and Services" />
</nav>
</aside>
<div className="container mx-auto flex min-h-[90vh] items-center px-6">
<div className="flex-1 space-y-6">
<h1 className="text-4xl font-extrabold leading-tight md:text-6xl">
<span className="relative inline-block">
<span className="absolute bottom-1 left-0 h-3 w-full bg-[#966314]"></span>
<span className="relative">Beyond Expectations</span>
</span>
<br />
Build <span className="text-[#966314]">Reputation.</span>
</h1>
<Button
size="lg"
className="rounded-full bg-[#966314] px-8 py-6 text-base hover:bg-[#7c520f]"
>
Contact Us
</Button>
</div>
<div className="relative hidden flex-1 justify-end md:flex">
<Image
src="/image/img1.png"
alt="Illustration"
width={520}
height={520}
className="object-contain"
/>
</div>
</div>
</header>
);
}
function MenuItem({ icon, label }: { icon: React.ReactNode; label: string }) {
return (
<div className="flex cursor-pointer items-center justify-between border-b border-white/20 pb-3">
<span>{label}</span>
{icon}
</div>
);
}

View File

@ -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<SetStateAction<string>>;
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 (
<motion.button
layout
onClick={() => 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 && (
<motion.div
layoutId="activeIndicator"
className="absolute left-0 top-1/2 -translate-y-1/2 w-1 h-8 bg-white rounded-r-full shadow-sm"
initial={{ opacity: 0, scaleY: 0 }}
animate={{ opacity: 1, scaleY: 1 }}
transition={{ duration: 0.2 }}
/>
)}
<motion.div
layout
className={`h-full flex items-center justify-center ${
open ? "w-12" : "w-full"
}`}
>
<div className={`text-lg transition-all duration-200 ${
isActive
? "text-white"
: "text-slate-500 group-hover:text-slate-700"
}`}>
<Icon />
</div>
</motion.div>
{open && (
<motion.span
layout
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.1, duration: 0.2 }}
className={`text-sm font-medium transition-colors duration-200 ${
isActive ? "text-white" : "text-slate-700"
}`}
>
{title}
</motion.span>
)}
{/* Tooltip for collapsed state */}
{!open && hovered && (
<motion.div
initial={{ opacity: 0, x: 8, scale: 0.8 }}
animate={{ opacity: 1, x: 0, scale: 1 }}
exit={{ opacity: 0, x: 8, scale: 0.8 }}
transition={{ type: "spring", stiffness: 300, damping: 20 }}
className="absolute left-full ml-3 whitespace-nowrap rounded-lg bg-slate-800 px-3 py-2 text-sm text-white shadow-xl z-50"
>
<div className="relative">
{title}
{/* Tooltip arrow */}
<div className="absolute -left-1 top-1/2 -translate-y-1/2 w-2 h-2 bg-slate-800 rotate-45"></div>
</div>
</motion.div>
)}
{/* Notification badge */}
{notifs && open && (
<motion.span
initial={{ scale: 0, opacity: 0 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.3, type: "spring" }}
className={`absolute right-3 top-1/2 -translate-y-1/2 size-5 rounded-full text-xs font-semibold flex items-center justify-center ${
isActive
? "bg-white text-emerald-500"
: "bg-red-500 text-white"
}`}
>
{notifs}
</motion.span>
)}
{/* Hover effect overlay */}
{hovered && !isActive && (
<motion.div
layoutId="hoverOverlay"
className="absolute inset-0 bg-gradient-to-r from-slate-100/50 to-slate-200/50 rounded-xl"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
/>
)}
</motion.button>
);
};
export default Option;

View File

@ -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 (
<section className="bg-white py-32">
<div className="container mx-auto px-6">
{/* TITLE */}
<div className="mb-20 text-center">
<p className="mb-4 text-sm font-semibold uppercase tracking-widest text-gray-400">
Our Product
</p>
<h2 className="mx-auto max-w-3xl text-4xl font-extrabold leading-tight md:text-5xl">
The product we offer is{" "}
<span className="relative inline-block">
<span className="absolute bottom-2 left-0 h-3 w-full bg-[#f5d28a]"></span>
<span className="relative italic text-[#966314]">designed</span>
</span>{" "}
to meet your business needs.
</h2>
</div>
{/* CONTENT */}
<div className="grid grid-cols-1 items-center gap-16 md:grid-cols-2">
{/* LEFT IMAGE */}
<div className="flex justify-center">
<Image
src="/image/p1.png"
alt="Product Illustration"
width={520}
height={420}
className="object-contain"
/>
</div>
{/* RIGHT CONTENT */}
<div className="max-w-xl">
{/* ICON */}
<div className="mb-6 flex h-12 w-12 items-center justify-center rounded-xl bg-[#fdecc8]">
<Image
src="/image/product-icon.png"
alt="Product Icon"
width={22}
height={22}
/>
</div>
<h3 className="mb-4 text-2xl font-bold text-gray-900">
MediaHUB Content Aggregator
</h3>
<p className="mb-8 text-sm leading-relaxed text-gray-600">
Social media marketing services are provided by companies or
individuals who specialize in marketing strategies through social
media platforms.
</p>
{/* FEATURES */}
<ul className="mb-6 space-y-4">
{features.map((item) => (
<li key={item} className="flex gap-3 text-sm text-gray-600">
<span className="mt-1 flex h-8 w-16 items-center justify-center rounded-full bg-[#fdecc8]">
<Check size={12} className="text-[#966314]" />
</span>
{item}
</li>
))}
</ul>
{/* CTA */}
<button className="text-sm font-semibold text-[#966314] hover:underline">
Learn More
</button>
</div>
</div>
<div className="grid grid-cols-1 items-center gap-16 md:grid-cols-2 mt-10">
{/* LEFT IMAGE */}
{/* RIGHT CONTENT */}
<div className="max-w-xl ml-10">
{/* ICON */}
<div className="mb-6 flex h-12 w-12 items-center justify-center rounded-xl bg-[#fdecc8]">
<Image
src="/image/product-icon.png"
alt="Product Icon"
width={22}
height={22}
/>
</div>
<h3 className="mb-4 text-2xl font-bold text-gray-900">
Multipool Reputation Management
</h3>
<p className="mb-8 text-sm leading-relaxed text-gray-600">
Social media marketing services are provided by companies or
individuals who specialize in marketing strategies through social
media platforms.
</p>
{/* FEATURES */}
<ul className="mb-6 space-y-4">
{features.map((item) => (
<li key={item} className="flex gap-3 text-sm text-gray-600">
<span className="mt-1 flex h-8 w-16 items-center justify-center rounded-full bg-[#fdecc8]">
<Check size={12} className="text-[#966314]" />
</span>
{item}
</li>
))}
</ul>
{/* CTA */}
<button className="text-sm font-semibold text-[#966314] hover:underline">
Learn More
</button>
</div>
<div className="flex justify-center">
<Image
src="/image/p2.png"
alt="Product Illustration"
width={520}
height={420}
className="object-contain"
/>
</div>
</div>
<div className="grid grid-cols-1 items-center gap-16 md:grid-cols-2 mt-10">
{/* LEFT IMAGE */}
<div className="flex justify-center">
<Image
src="/image/p3.png"
alt="Product Illustration"
width={520}
height={420}
className="object-contain"
/>
</div>
{/* RIGHT CONTENT */}
<div className="max-w-xl">
{/* ICON */}
<div className="mb-6 flex h-12 w-12 items-center justify-center rounded-xl bg-[#fdecc8]">
<Image
src="/image/product-icon.png"
alt="Product Icon"
width={22}
height={22}
/>
</div>
<h3 className="mb-4 text-2xl font-bold text-gray-900">
PR Room Opinion Management
</h3>
<p className="mb-8 text-sm leading-relaxed text-gray-600">
Social media marketing services are provided by companies or
individuals who specialize in marketing strategies through social
media platforms.
</p>
{/* FEATURES */}
<ul className="mb-6 space-y-4">
{features.map((item) => (
<li key={item} className="flex gap-3 text-sm text-gray-600">
<span className="mt-1 flex h-8 w-16 items-center justify-center rounded-full bg-[#fdecc8]">
<Check size={12} className="text-[#966314]" />
</span>
{item}
</li>
))}
</ul>
{/* CTA */}
<button className="text-sm font-semibold text-[#966314] hover:underline">
Learn More
</button>
</div>
</div>
</div>
</section>
);
}

View File

@ -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: () => (
<Icon icon="material-symbols:dashboard" className="text-lg" />
),
link: "/admin/dashboard",
},
],
},
];
}
if (role === "Approver" || role === "Kontributor") {
return [
{
title: "Dashboard",
items: [
{
title: "Dashboard",
icon: () => (
<Icon icon="material-symbols:dashboard" className="text-lg" />
),
link: "/admin/dashboard",
},
],
},
{
title: "Content Management",
items: [
{
title: "Articles",
icon: () => <Icon icon="ri:article-line" className="text-lg" />,
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 */}
<AnimatePresence mode="wait">
<motion.nav
key="desktop-sidebar"
layout
className="hidden md:flex sticky top-0 h-screen shrink-0 bg-gradient-to-b from-slate-50 to-white dark:from-slate-800 dark:to-slate-900 border-r border-slate-200/60 dark:border-slate-700/60 shadow-lg backdrop-blur-sm flex-col justify-between"
style={{
width: sidebarData ? "280px" : "80px",
}}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
transition={{ duration: 0.3 }}
>
<SidebarContent
open={sidebarData}
pathname={pathname}
updateSidebarData={updateSidebarData}
/>
</motion.nav>
</AnimatePresence>
{/* Desktop Toggle Button - appears when sidebar is collapsed */}
<AnimatePresence>
{!sidebarData && (
<motion.button
key="desktop-toggle"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.8 }}
className="hidden md:flex fixed top-4 left-20 z-40 p-3 bg-white rounded-xl shadow-lg border border-slate-200/60 hover:shadow-xl transition-all duration-200 hover:bg-slate-50"
onClick={() => updateSidebarData(true)}
>
<Icon
icon="heroicons:chevron-right"
className="w-5 h-5 text-slate-600"
/>
</motion.button>
)}
</AnimatePresence>
{/* Mobile Toggle Button */}
<AnimatePresence>
{!sidebarData && (
<motion.button
key="mobile-toggle"
initial={{ opacity: 0, scale: 0 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0 }}
className="md:hidden fixed top-4 left-4 z-50 p-3 bg-white dark:bg-slate-800 rounded-xl shadow-lg border border-slate-200/60 dark:border-slate-700/60 hover:shadow-xl transition-all duration-200"
onClick={() => updateSidebarData(true)}
>
<Icon
icon="heroicons:chevron-right"
className="w-6 h-6 text-slate-600"
/>
</motion.button>
)}
</AnimatePresence>
{/* MOBILE SIDEBAR */}
<AnimatePresence>
{sidebarData && (
<motion.div
key="mobile-sidebar"
initial={{ x: "-100%" }}
animate={{ x: 0 }}
exit={{ x: "-100%" }}
transition={{ type: "tween", duration: 0.3 }}
className="fixed top-0 left-0 z-50 w-[280px] h-full bg-gradient-to-b from-slate-50 to-white dark:from-slate-800 dark:to-slate-900 p-4 flex flex-col md:hidden shadow-2xl backdrop-blur-sm"
>
{/* <button onClick={() => updateSidebarData(false)} className="mb-4 self-end text-zinc-500">
</button> */}
<SidebarContent
open={true}
pathname={pathname}
updateSidebarData={updateSidebarData}
/>
</motion.div>
)}
</AnimatePresence>
</>
);
};
const SidebarContent = ({
open,
pathname,
updateSidebarData,
}: {
open: boolean;
pathname: string;
updateSidebarData: (newData: boolean) => void;
}) => {
const { theme, toggleTheme } = useTheme();
const [username, setUsername] = useState<string>("Guest");
const [roleName, setRoleName] = useState<string>("");
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 (
<div className="flex flex-col h-full">
{/* SCROLLABLE TOP SECTION */}
<div className="flex-1 overflow-y-auto">
{/* HEADER SECTION */}
<div className="flex flex-col space-y-6">
{/* Logo and Toggle */}
<div className="flex items-center justify-between px-4 py-6">
<Link href="/" className="flex items-center space-x-3">
<div className="relative">
<img
src="/arah-negeri.png"
className="w-10 h-10 rounded-lg shadow-sm"
/>
<div className="absolute -inset-1 bg-gradient-to-r from-blue-500 to-purple-500 rounded-lg opacity-20 blur-sm"></div>
</div>
{open && (
<motion.div
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.1 }}
className="flex flex-col"
>
<span className="text-lg font-bold bg-gradient-to-r from-slate-800 to-slate-600 bg-clip-text text-transparent">
Arah Negeri
</span>
<span className="text-xs text-slate-500">Admin Panel</span>
</motion.div>
)}
</Link>
{open && (
<motion.button
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.2 }}
className="p-2 rounded-lg hover:bg-slate-100 transition-colors duration-200 group"
onClick={() => updateSidebarData(false)}
>
<Icon
icon="heroicons:chevron-left"
className="w-5 h-5 text-slate-500 group-hover:text-slate-700 transition-colors"
/>
</motion.button>
)}
</div>
{/* Navigation Sections */}
<div className="space-y-3 px-3 pb-6">
{sidebarSections.map((section, sectionIndex) => (
<motion.div
key={section.title}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 + sectionIndex * 0.1 }}
className="space-y-3"
>
{open && (
<motion.h3
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.2 + sectionIndex * 0.1 }}
className="text-xs font-semibold text-slate-500 uppercase tracking-wider px-3"
>
{section.title}
</motion.h3>
)}
<div className="space-y-2">
{section.items.map((item, itemIndex) => (
<Link href={item.link} key={item.title}>
<Option
Icon={item.icon}
title={item.title}
active={pathname === item.link}
open={open}
/>
</Link>
))}
</div>
</motion.div>
))}
</div>
</div>
</div>
{/* FIXED BOTTOM SECTION */}
<div className="flex-shrink-0 space-y-1 border-t border-slate-200/60 dark:border-slate-700/60 bg-white/50 dark:bg-slate-800/50 backdrop-blur-sm">
{/* Divider */}
{/* <div className="px-3 pb-2">
<div className="h-px bg-gradient-to-r from-transparent via-slate-300 to-transparent"></div>
</div> */}
{/* Theme Toggle */}
<div className="px-3 pt-1">
<motion.button
onClick={toggleTheme}
className={`relative flex h-12 w-full items-center rounded-xl transition-all duration-200 cursor-pointer group ${
open ? "px-3" : "justify-center"
} ${
theme === "dark"
? "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 dark:text-slate-300 dark:hover:bg-slate-700/50"
}`}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<motion.div
className={`h-full flex items-center justify-center ${
open ? "w-12" : "w-full"
}`}
>
<div
className={`text-lg transition-all duration-200 ${
theme === "dark"
? "text-white"
: "text-slate-500 group-hover:text-slate-700 dark:text-slate-400 dark:group-hover:text-slate-200"
}`}
>
{theme === "dark" ? (
<Icon icon="solar:sun-bold" className="text-lg" />
) : (
<Icon icon="solar:moon-bold" className="text-lg" />
)}
</div>
</motion.div>
{open && (
<motion.span
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.1, duration: 0.2 }}
className={`text-sm font-medium transition-colors duration-200 ${
theme === "dark"
? "text-white"
: "text-slate-700 dark:text-slate-300"
}`}
>
{theme === "dark" ? "Light Mode" : "Dark Mode"}
</motion.span>
)}
</motion.button>
</div>
{/* Settings */}
<div className="px-3">
<Link href="/settings">
<Option
Icon={() => (
<Icon icon="lets-icons:setting-fill" className="text-lg" />
)}
title="Settings"
active={pathname === "/settings"}
open={open}
/>
</Link>
</div>
{/* User Profile */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4 }}
className="px-3 py-3 border-t border-slate-200/60"
>
<div
className={`${
open
? "flex items-center space-x-3"
: "flex items-center justify-center"
} p-3 rounded-xl bg-gradient-to-r from-slate-50 to-slate-100/50 hover:from-slate-100 hover:to-slate-200/50 transition-all duration-200 cursor-pointer group`}
>
<div className="relative">
<div className="w-10 h-10 rounded-full bg-gradient-to-r from-blue-500 to-purple-500 flex items-center justify-center text-white font-semibold text-sm shadow-lg">
A
</div>
<div className="absolute -bottom-1 -right-1 w-4 h-4 bg-green-500 rounded-full border-2 border-white"></div>
</div>
{open && (
<motion.div
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.5 }}
className="flex-1 min-w-0"
>
<p className="text-sm font-medium text-slate-800 truncate">
{username}
</p>
<Link href="/auth">
<p className="text-xs text-slate-500 hover:text-blue-600 transition-colors duration-200">
Sign out
</p>
</Link>
</motion.div>
)}
</div>
</motion.div>
{/* Expand Button for Collapsed State */}
{/* {!open && (
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.6 }}
className="px-3 pt-2"
>
<button
onClick={() => updateSidebarData(true)}
className="w-full p-3 rounded-xl bg-gradient-to-r from-blue-500 to-purple-500 hover:from-blue-600 hover:to-purple-600 text-white shadow-lg transition-all duration-200 hover:shadow-xl group"
>
<div className="flex items-center justify-center">
<Icon
icon="heroicons:chevron-right"
className="w-5 h-5 group-hover:scale-110 transition-transform duration-200"
/>
</div>
</button>
</motion.div>
)} */}
</div>
</div>
);
};

View File

@ -0,0 +1,189 @@
import Image from "next/image";
export default function ServiceSection() {
return (
<section className="py-20 bg-white">
<div className="container mx-auto px-6">
{/* Heading */}
<div className="text-center mb-16">
<p className="text-sm uppercase tracking-widest text-gray-400">
Our Services
</p>
<h2 className="mt-2 text-3xl md:text-4xl font-bold text-gray-900">
Innovative solutions for your{" "}
<span className="relative inline-block">
business growth
<span className="absolute left-0 -bottom-1 w-full h-2 bg-yellow-300/60 -z-10" />
</span>
.
</h2>
</div>
{/* Service 1 */}
<div className="grid md:grid-cols-2 gap-12 items-center mb-20">
{/* Image */}
<div className="rounded-2xl overflow-hidden shadow-lg">
<Image
src="/image/s1.png"
alt="Artifintel Soundworks"
width={600}
height={400}
className="w-full h-auto object-cover"
/>
</div>
{/* Content */}
<div>
<h3 className="text-2xl font-semibold text-gray-900 mb-4">
Artifintel
</h3>
<p className="text-gray-600 mb-6 leading-relaxed">
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.
</p>
<ul className="space-y-3 text-gray-700">
<li> AI Music Composition & Songwriting</li>
<li> Vocal Synthesis & AI Musicians</li>
<li> Genre Exploration & Sound Innovation</li>
<li> AI Collaboration & Creative Experimentation</li>
<li> Music Release & Digital Distribution</li>
</ul>
</div>
</div>
{/* Service 2 */}
<div className="grid md:grid-cols-2 gap-12 items-center mb-20">
{/* Content */}
<div>
<h3 className="text-2xl font-semibold text-gray-900 mb-4">
Produksi Video Animasi
</h3>
<p className="text-gray-600 mb-6 leading-relaxed">
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.
</p>
<ul className="space-y-3 text-gray-700">
<li> 2D & 3D Animation Production</li>
<li> Motion Graphics & Visual Effects</li>
<li> Character Design & Storyboarding</li>
</ul>
</div>
{/* Image */}
<div className="rounded-2xl overflow-hidden shadow-lg">
<Image
src="/image/s2.png"
alt="Animasee"
width={600}
height={400}
className="w-full h-auto object-cover"
/>
</div>
</div>
<div className="grid md:grid-cols-2 gap-12 items-center mb-20 mt-10">
{/* Image */}
<div className="rounded-2xl overflow-hidden shadow-lg">
<Image
src="/image/s3.png"
alt="Artifintel Soundworks"
width={600}
height={400}
className="w-full h-auto object-cover"
/>
</div>
{/* Content */}
<div>
<h3 className="text-2xl font-semibold text-gray-900 mb-4">
Reelithic
</h3>
<p className="text-gray-600 mb-6 leading-relaxed">
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.
</p>
<ul className="space-y-3 text-gray-700">
<li> AI Music Composition & Songwriting</li>
<li> Vocal Synthesis & AI Musicians</li>
<li> Genre Exploration & Sound Innovation</li>
<li> AI Collaboration & Creative Experimentation</li>
<li> Music Release & Digital Distribution</li>
</ul>
</div>
</div>
{/* Service 3 */}
<div className="grid md:grid-cols-2 gap-12 items-center mb-20">
{/* Content */}
<div>
<h3 className="text-2xl font-semibold text-gray-900 mb-4">
Qudoin
</h3>
<p className="text-gray-600 mb-6 leading-relaxed">
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.
</p>
<ul className="space-y-3 text-gray-700">
<li> 2D & 3D Animation Production</li>
<li> Motion Graphics & Visual Effects</li>
<li> Character Design & Storyboarding</li>
</ul>
</div>
{/* Image */}
<div className="rounded-2xl overflow-hidden shadow-lg">
<Image
src="/image/s4.png"
alt="Animasee"
width={600}
height={400}
className="w-full h-auto object-cover"
/>
</div>
</div>
<div className="grid md:grid-cols-2 gap-12 items-center mb-20 mt-10">
{/* Image */}
<div className="rounded-2xl overflow-hidden shadow-lg">
<Image
src="/image/s5.png"
alt="Artifintel Soundworks"
width={600}
height={400}
className="w-full h-auto object-cover"
/>
</div>
{/* Content */}
<div>
<h3 className="text-2xl font-semibold text-gray-900 mb-4">
Talkshow AI
</h3>
<p className="text-gray-600 mb-6 leading-relaxed">
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.
</p>
<ul className="space-y-3 text-gray-700">
<li> AI Music Composition & Songwriting</li>
<li> Vocal Synthesis & AI Musicians</li>
<li> Genre Exploration & Sound Innovation</li>
<li> AI Collaboration & Creative Experimentation</li>
<li> Music Release & Digital Distribution</li>
</ul>
</div>
</div>
</div>
</section>
);
}

View File

@ -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 (
<section className="relative overflow-hidden bg-gradient-to-r from-[#faf6ee] to-[#f4efe6] py-14">
<div className="container mx-auto px-6">
{/* Title */}
<p className="text-center text-lg font-semibold tracking-widest text-gray-500 mb-8">
TECHNOLOGY PARTNERS
</p>
{/* Slider */}
<div className="relative w-full overflow-hidden">
<div className="flex w-max animate-tech-scroll gap-14">
{/* duplicated for seamless loop */}
{[...technologies, ...technologies].map((tech, index) => (
<div
key={index}
className="flex items-center justify-center min-w-[140px] opacity-80 hover:opacity-100 transition"
>
<Image
src={tech.src}
alt={tech.name}
width={120}
height={50}
className="object-contain"
/>
</div>
))}
</div>
</div>
</div>
</section>
);
}

View File

@ -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 (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-slate-50 flex items-center justify-center">
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-blue-500"></div>
</div>
);
}
return (
<ThemeProvider>
<SidebarProvider>
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-slate-50 dark:from-slate-900 dark:via-slate-800 dark:to-slate-900">
<div className="flex h-screen overflow-hidden">
<RetractingSidebar
sidebarData={isOpen}
updateSidebarData={updateSidebarData}
/>
<AnimatePresence mode="wait">
<motion.div
key="main-content"
className="flex-1 flex flex-col overflow-hidden"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.3 }}
>
{/* Header */}
<motion.header
className="bg-white/80 dark:bg-slate-800/80 backdrop-blur-sm border-b border-slate-200/60 dark:border-slate-700/60 shadow-sm"
initial={{ y: -20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ delay: 0.2, duration: 0.3 }}
>
<div className="flex items-center justify-between px-6 py-4">
<div className="flex items-center space-x-4">
<button
className="md:hidden p-2 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors duration-200"
onClick={() => updateSidebarData(true)}
>
<BurgerButtonIcon />
</button>
<Breadcrumbs />
</div>
</div>
</motion.header>
{/* Main Content */}
<motion.main
className="flex-1 overflow-auto"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3, duration: 0.3 }}
>
<div className="h-full">{children}</div>
</motion.main>
</motion.div>
</AnimatePresence>
</div>
</div>
</SidebarProvider>
</ThemeProvider>
);
};

View File

@ -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<React.Key>("");
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 <DashboardIcon size={40} />;
if (pathname.includes("article")) return <ArticleIcon size={40} />;
if (pathname.includes("master-category"))
return <MasterCategoryIcon size={40} />;
if (pathname.includes("magazine")) return <MagazineIcon size={40} />;
if (pathname.includes("static-page")) return <StaticPageIcon size={40} />;
if (pathname.includes("master-user")) return <MasterUsersIcon size={40} />;
if (pathname.includes("master-role")) return <MasterRoleIcon size={40} />;
return null;
};
if (!mounted) {
return (
<div className="flex items-center space-x-6">
<div className="w-10 h-10 bg-slate-200 rounded-lg animate-pulse"></div>
<div className="flex flex-col space-y-2">
<div className="h-8 w-32 bg-slate-200 rounded animate-pulse"></div>
<div className="h-4 w-48 bg-slate-200 rounded animate-pulse"></div>
</div>
</div>
);
}
return (
<motion.div
className="flex items-center space-x-6"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.3 }}
>
{/* Page Icon */}
<motion.div
initial={{ scale: 0, rotate: -180 }}
animate={{ scale: 1, rotate: 0 }}
transition={{ delay: 0.2, type: "spring", stiffness: 200 }}
className="flex-shrink-0"
>
{getPageIcon()}
</motion.div>
{/* Page Title and Breadcrumbs */}
<div className="flex flex-col space-y-2">
<motion.h1
className="text-2xl font-bold bg-gradient-to-r from-slate-800 to-slate-600 bg-clip-text text-transparent"
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3, duration: 0.3 }}
>
{pathnameTransformed[pathnameTransformed.length - 1]}
</motion.h1>
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4, duration: 0.3 }}
>
<Breadcrumb>
<BreadcrumbList className="flex items-center space-x-2">
{pathnameTransformed
?.filter((item) => item !== "Admin")
.map((item, index, array) => (
<React.Fragment key={pathnameSplit[index]}>
<BreadcrumbItem>
<BreadcrumbLink
onClick={() => 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}
</BreadcrumbLink>
</BreadcrumbItem>
{index < array.length - 1 && (
<BreadcrumbSeparator className="text-slate-400">
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 5l7 7-7 7"
/>
</svg>
</BreadcrumbSeparator>
)}
</React.Fragment>
))}
</BreadcrumbList>
</Breadcrumb>
</motion.div>
</div>
</motion.div>
);
};

View File

@ -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<Props, State> {
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 (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-50 via-white to-slate-50">
<div className="text-center p-8 max-w-md">
<div className="mb-6">
<div className="w-16 h-16 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-4">
<RefreshCw className="w-8 h-8 text-red-600" />
</div>
<h2 className="text-xl font-semibold text-gray-900 mb-2">
Chunk Loading Error
</h2>
<p className="text-gray-600 mb-6">
There was an issue loading a part of the application. This usually happens when the application has been updated.
</p>
</div>
<div className="space-y-3">
<Button
onClick={this.handleRetry}
className="w-full bg-blue-600 hover:bg-blue-700 text-white"
>
<RefreshCw className="w-4 h-4 mr-2" />
Reload Application
</Button>
<Button
onClick={() => window.location.href = '/'}
variant="outline"
className="w-full"
>
Go to Homepage
</Button>
</div>
{process.env.NODE_ENV === 'development' && this.state.error && (
<details className="mt-6 text-left">
<summary className="cursor-pointer text-sm text-gray-500 hover:text-gray-700">
Error Details (Development)
</summary>
<pre className="mt-2 p-3 bg-gray-100 rounded text-xs overflow-auto">
{this.state.error.message}
</pre>
</details>
)}
</div>
</div>
);
}
return this.props.children;
}
}
export default ChunkErrorBoundary;

View File

@ -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 (
<svg width={size} height={size} className={className} viewBox={`0 0 ${size} ${size}`}>
<circle className="text-gray-200" stroke="currentColor" strokeWidth={strokeWidth} fill="transparent" r={radius} cx={size / 2} cy={size / 2} />
<circle
className="text-primary"
stroke="currentColor"
strokeWidth={strokeWidth}
strokeLinecap="round"
fill="transparent"
r={radius}
cx={size / 2}
cy={size / 2}
strokeDasharray={circumference}
strokeDashoffset={offset}
style={{ transition: "stroke-dashoffset 0.35s" }}
/>
<text x="50%" y="50%" dy=".3em" textAnchor="middle" className="text-xs fill-primary font-medium">
{Math.round(value)}%
</text>
</svg>
);
}

View File

@ -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 (
<div className="relative flex items-center justify-center" style={{ width: size, height: size }}>
<svg width={size} height={size}>
<circle
stroke={bgColor}
fill="transparent"
strokeWidth={strokeWidth}
r={radius}
cx={size / 2}
cy={size / 2}
/>
<circle
stroke={color}
fill="transparent"
strokeWidth={strokeWidth}
strokeLinecap="round"
strokeDasharray={circumference}
strokeDashoffset={offset}
r={radius}
cx={size / 2}
cy={size / 2}
style={{ transition: "stroke-dashoffset 0.35s" }}
/>
</svg>
<span className="absolute text-sm font-semibold text-gray-800 dark:text-white">
{label ?? `${Math.round(progress)}%`}
</span>
</div>
);
};

View File

@ -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(
<PaginationItem key={i} onClick={() => setPage(i)}>
<PaginationLink className="cursor-pointer" isActive={page === i}>
{i}
</PaginationLink>
</PaginationItem>
);
}
return pageNumbers;
};
return (
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationLink
className="cursor-pointer"
onClick={() => (page > 10 ? setPage(page - 10) : "")}
>
{/* <DoubleArrowLeftIcon /> */}
</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationPrevious
className="cursor-pointer"
onClick={() => (page > 1 ? setPage(page - 1) : "")}
/>
</PaginationItem>
<PaginationItem>
<PaginationLink
className="cursor-pointer"
onClick={() => setPage(1)}
isActive={page === 1}
>
{1}
</PaginationLink>
</PaginationItem>
{page > 4 && (
<PaginationItem>
<PaginationEllipsis
className="cursor-pointer"
onClick={() => setPage(page - 1)}
/>
</PaginationItem>
)}
{renderPageNumbers()}
{page < totalPage - 3 && (
<PaginationItem>
<PaginationEllipsis
className="cursor-pointer"
onClick={() => setPage(page + 1)}
/>
</PaginationItem>
)}
{totalPage > 1 && (
<PaginationItem>
<PaginationLink
className="cursor-pointer"
onClick={() => setPage(totalPage)}
isActive={page === totalPage}
>
{totalPage}
</PaginationLink>
</PaginationItem>
)}
<PaginationItem>
<PaginationNext
className="cursor-pointer"
onClick={() => (page < totalPage ? setPage(page + 1) : "")}
/>
</PaginationItem>
<PaginationItem>
<PaginationLink
onClick={() => (page < totalPage - 10 ? setPage(page + 10) : "")}
>
{/* <DoubleArrowRightIcon /> */}
</PaginationLink>
</PaginationItem>
</PaginationContent>
</Pagination>
);
}

View File

@ -0,0 +1,59 @@
const AdminDashboard = () => {
return (
<div className="space-y-8">
<div>
<h1 className="text-2xl font-bold text-slate-800">Admin Dashboard</h1>
<p className="text-slate-500">Review and manage content submissions</p>
</div>
{/* STAT CARDS */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{[
{
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) => (
<div
key={i}
className="bg-white rounded-2xl shadow border p-6 flex justify-between items-center"
>
<div>
<p className="text-sm text-slate-500">{card.title}</p>
<h2 className="text-3xl font-bold text-slate-800 mt-2">
{card.value}
</h2>
</div>
<div className="text-right">
<p className="text-sm text-green-600 font-medium">
{card.growth}
</p>
<div className={`w-10 h-10 rounded-xl mt-2 ${card.color}`} />
</div>
</div>
))}
</div>
</div>
);
};

View File

@ -0,0 +1,58 @@
'use client'
import React, { createContext, useContext, useEffect, useState } from 'react';
interface SidebarContextType {
isOpen: boolean;
toggleSidebar: () => void;
}
const SidebarContext = createContext<SidebarContextType | undefined>(undefined);
export const SidebarProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [isOpen, setIsOpen] = useState(() => {
if (typeof window !== 'undefined') {
const storedValue = localStorage.getItem('sidebarOpen');
return storedValue ? JSON.parse(storedValue) : false;
}
});
const toggleSidebar = () => {
setIsOpen(!isOpen);
};
useEffect(() => {
localStorage.setItem('sidebarOpen', JSON.stringify(isOpen));
}, [isOpen]);
useEffect(() => {
const handleResize = () => {
setIsOpen(window.innerWidth > 768); // Ganti 768 dengan lebar yang sesuai dengan breakpoint Anda
};
handleResize(); // Pastikan untuk memanggil fungsi handleResize saat komponen dimuat
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
return (
<SidebarContext.Provider value={{ isOpen, toggleSidebar }}>
{children}
</SidebarContext.Provider>
);
};
export const useSidebar = () => {
const context = useContext(SidebarContext);
if (!context) {
throw new Error('useSidebar must be used within a SidebarProvider');
}
return context;
};
export const useSidebarContext = () => {
return useContext(SidebarContext);
};

View File

@ -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<ThemeContextType | undefined>(undefined);
export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [theme, setThemeState] = useState<Theme>('light');
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
// Get theme from localStorage or default to 'light'
const savedTheme = localStorage.getItem('theme') as Theme;
if (savedTheme) {
setThemeState(savedTheme);
} else {
// Check system preference
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
setThemeState(systemTheme);
}
}, []);
useEffect(() => {
if (!mounted) return;
// Update document class and localStorage
document.documentElement.classList.remove('light', 'dark');
document.documentElement.classList.add(theme);
localStorage.setItem('theme', theme);
}, [theme, mounted]);
const toggleTheme = () => {
setThemeState(prev => prev === 'light' ? 'dark' : 'light');
};
const setTheme = (newTheme: Theme) => {
setThemeState(newTheme);
};
// Prevent hydration mismatch
if (!mounted) {
return <>{children}</>;
}
return (
<ThemeContext.Provider value={{ theme, toggleTheme, setTheme }}>
{children}
</ThemeContext.Provider>
);
};
export const useTheme = () => {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
};

View File

@ -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<string[]>([]);
const [series, setSeries] = useState<{ name: string; data: number[] }[]>([]);
const [seriesComment, setSeriesComment] = useState<number[]>([]);
const [seriesView, setSeriesView] = useState<number[]>([]);
const [seriesShare, setSeriesShare] = useState<number[]>([]);
// 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 (
<div className="h-full">
<div id="chart" className="h-full">
{/* <SafeReactApexChart
options={{
chart: {
height: "100%",
type: "area",
},
stroke: {
curve: "smooth",
},
dataLabels: {
enabled: false,
},
xaxis: {
categories: type == "weekly" ? categories : [],
},
}}
series={series}
type="area"
height="100%"
/> */}
</div>
<div id="html-dist"></div>
</div>
);
};
export default ApexChartColumn;

View File

@ -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<string | undefined>();
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<ArticleData[]>([]);
// const [analyticsView, setAnalyticView] = useState<string[]>(["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<string[]>([]);
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<any>();
const [topPages, setTopPages] = useState<TopPages[]>([]);
const [postCount, setPostCount] = useState<PostCount[]>([]);
// 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 (
<div className="space-y-8">
{/* HEADER */}
<div>
<h1 className="text-2xl font-bold text-slate-800">Admin Dashboard</h1>
<p className="text-slate-500">
Review and manage content submissions
</p>
</div>
{/* STAT CARDS */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{[
{ 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) => (
<div
key={i}
className="bg-white rounded-2xl shadow border p-6 flex justify-between items-center"
>
<div>
<p className="text-sm text-slate-500">{card.title}</p>
<h2 className="text-3xl font-bold text-slate-800 mt-2">
{card.value}
</h2>
</div>
<div className={`w-12 h-12 rounded-xl ${card.color}`} />
</div>
))}
</div>
{/* TASK LIST */}
<div className="bg-white rounded-2xl shadow border p-6 space-y-6">
{/* Title */}
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold">
Task List{" "}
<span className="ml-2 text-xs bg-amber-100 text-amber-600 px-3 py-1 rounded-full">
{tasks.length} Tasks
</span>
</h2>
</div>
{/* Filters */}
<div className="flex flex-wrap gap-4">
<Input type="date" className="w-[180px]" />
<Input type="date" className="w-[180px]" />
<Select>
<SelectTrigger className="w-[150px]">
<SelectValue placeholder="Status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="open">Open</SelectItem>
<SelectItem value="closed">Closed</SelectItem>
</SelectContent>
</Select>
</div>
{/* Accordion List */}
<Accordion type="single" collapsible className="space-y-4">
{tasks.map((task) => (
<AccordionItem
key={task.id}
value={task.id}
className="border rounded-xl px-4"
>
<AccordionTrigger className="hover:no-underline">
<div className="flex items-center justify-between w-full">
<div className="text-left">
<p className="font-medium text-slate-800">{task.title}</p>
<p className="text-sm text-slate-500 mt-1">
{task.author} {task.category} {task.date}
</p>
</div>
<div className="flex items-center gap-4">
<Badge
className={
task.status === "OPEN"
? "bg-yellow-100 text-yellow-600"
: "bg-green-100 text-green-600"
}
>
{task.status}
</Badge>
{/* Mini Progress */}
<div className="flex gap-1">
<div className="w-6 h-1 bg-green-500 rounded" />
<div className="w-6 h-1 bg-yellow-500 rounded" />
<div className="w-6 h-1 bg-gray-300 rounded" />
<div className="w-6 h-1 bg-gray-300 rounded" />
</div>
</div>
</div>
</AccordionTrigger>
<AccordionContent>
<div className="mt-4 bg-slate-50 rounded-xl border p-6 space-y-6">
{/* Title */}
<div className="flex items-center gap-2">
<div className="w-5 h-5 rounded-full border-2 border-amber-500 flex items-center justify-center">
<div className="w-2 h-2 bg-amber-500 rounded-full" />
</div>
<h3 className="font-semibold text-slate-700">
Document Flow Status
</h3>
</div>
{/* Stepper */}
<div className="relative">
{/* Line */}
<div className="absolute top-5 left-0 right-0 h-[2px] bg-slate-200" />
<div className="relative grid grid-cols-4 text-center">
{/* STEP 1 */}
<div className="flex flex-col items-center">
<div className="w-10 h-10 rounded-full bg-green-500 flex items-center justify-center text-white">
</div>
<p className="mt-2 text-sm font-medium text-slate-700">
Submission
</p>
<p className="text-xs text-slate-500">
Feb 13, 2026 09:00
</p>
</div>
{/* STEP 2 */}
<div className="flex flex-col items-center">
<div className="w-10 h-10 rounded-full bg-amber-500 flex items-center justify-center text-white">
</div>
<p className="mt-2 text-sm font-medium text-slate-700">
Technical Review
</p>
<p className="text-xs text-slate-500">
Feb 13, 2026 11:00
</p>
</div>
{/* STEP 3 */}
<div className="flex flex-col items-center">
<div className="w-10 h-10 rounded-full border-2 border-slate-300 bg-white" />
<p className="mt-2 text-sm font-medium text-slate-400">
Admin Verification
</p>
<p className="text-xs text-slate-400">Waiting</p>
</div>
{/* STEP 4 */}
<div className="flex flex-col items-center">
<div className="w-10 h-10 rounded-full border-2 border-slate-300 bg-white" />
<p className="mt-2 text-sm font-medium text-slate-400">
Final Approval
</p>
<p className="text-xs text-slate-400">Waiting</p>
</div>
</div>
</div>
</div>
</AccordionContent>
</AccordionItem>
))}
</Accordion>
</div>
</div>
);
};
const ContributorDashboard = () => {
return (
<div className="space-y-8">
<div>
<h1 className="text-2xl font-bold text-slate-800">Dashboard</h1>
</div>
{/* STAT CARDS */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{[
{
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) => (
<div
key={i}
className="bg-white rounded-2xl shadow border p-6 flex justify-between items-center"
>
<div>
<p className="text-sm text-slate-500">{card.title}</p>
<h2 className="text-3xl font-bold text-slate-800 mt-2">
{card.value}
</h2>
</div>
<div className="text-right">
<p className="text-sm text-green-600 font-medium">
{card.growth}
</p>
<div className={`w-10 h-10 rounded-xl mt-2 ${card.color}`} />
</div>
</div>
))}
</div>
{/* CONTENT + QUICK ACTIONS */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2 bg-white rounded-2xl shadow border p-6">
<h2 className="font-semibold mb-4">Recent Content</h2>
{/* isi list content di sini */}
</div>
<div className="bg-amber-700 text-white rounded-2xl shadow p-6 space-y-4">
<h2 className="font-semibold">Quick Actions</h2>
<button className="w-full bg-amber-600 py-2 rounded-lg">
+ Create New Article
</button>
<button className="w-full bg-amber-600 py-2 rounded-lg">
+ Update Product
</button>
<button className="w-full bg-white text-amber-700 py-2 rounded-lg">
View All Actions
</button>
</div>
</div>
</div>
);
};
return (
<>{roleName === "Admin" ? <AdminDashboard /> : <ContributorDashboard />}</>
);
}

View File

@ -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<typeof AccordionPrimitive.Root>) {
return <AccordionPrimitive.Root data-slot="accordion" {...props} />
}
function AccordionItem({
className,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
return (
<AccordionPrimitive.Item
data-slot="accordion-item"
className={cn("border-b last:border-b-0", className)}
{...props}
/>
)
}
function AccordionTrigger({
className,
children,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
return (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
data-slot="accordion-trigger"
className={cn(
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
className
)}
{...props}
>
{children}
<ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
)
}
function AccordionContent({
className,
children,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
return (
<AccordionPrimitive.Content
data-slot="accordion-content"
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
{...props}
>
<div className={cn("pt-0 pb-4", className)}>{children}</div>
</AccordionPrimitive.Content>
)
}
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }

46
components/ui/badge.tsx Normal file
View File

@ -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<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "span";
return (
<Comp
data-slot="badge"
className={cn(badgeVariants({ variant }), className)}
{...props}
/>
);
}
export { Badge, badgeVariants };

View File

@ -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 <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />
}
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
return (
<ol
data-slot="breadcrumb-list"
className={cn(
"text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
className
)}
{...props}
/>
)
}
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
return (
<li
data-slot="breadcrumb-item"
className={cn("inline-flex items-center gap-1.5", className)}
{...props}
/>
)
}
function BreadcrumbLink({
asChild,
className,
...props
}: React.ComponentProps<"a"> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : "a"
return (
<Comp
data-slot="breadcrumb-link"
className={cn("hover:text-foreground transition-colors", className)}
{...props}
/>
)
}
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
data-slot="breadcrumb-page"
role="link"
aria-disabled="true"
aria-current="page"
className={cn("text-foreground font-normal", className)}
{...props}
/>
)
}
function BreadcrumbSeparator({
children,
className,
...props
}: React.ComponentProps<"li">) {
return (
<li
data-slot="breadcrumb-separator"
role="presentation"
aria-hidden="true"
className={cn("[&>svg]:size-3.5", className)}
{...props}
>
{children ?? <ChevronRight />}
</li>
)
}
function BreadcrumbEllipsis({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="breadcrumb-ellipsis"
role="presentation"
aria-hidden="true"
className={cn("flex size-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontal className="size-4" />
<span className="sr-only">More</span>
</span>
)
}
export {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
}

59
components/ui/button.tsx Normal file
View File

@ -0,0 +1,59 @@
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 buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-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",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
destructive:
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : "button"
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }

View File

@ -0,0 +1,75 @@
"use client";
import * as React from "react";
import { DayPicker } from "react-day-picker";
import { cn } from "@/lib/utils";
import { buttonVariants } from "@/components/ui/button";
function Calendar({
className,
classNames,
showOutsideDays = true,
...props
}: React.ComponentProps<typeof DayPicker>) {
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn("p-3", className)}
classNames={{
months: "flex flex-col sm:flex-row gap-2",
month: "flex flex-col gap-4",
caption: "flex justify-center pt-1 relative items-center w-full",
caption_label: "text-sm font-medium",
nav: "flex items-center gap-1",
nav_button: cn(
buttonVariants({ variant: "outline" }),
"size-7 bg-transparent p-0 opacity-50 hover:opacity-100"
),
nav_button_previous: "absolute left-1",
nav_button_next: "absolute right-1",
table: "w-full border-collapse space-x-1",
head_row: "flex",
head_cell:
"text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]",
row: "flex w-full mt-2",
cell: cn(
"relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-range-end)]:rounded-r-md",
props.mode === "range"
? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
: "[&:has([aria-selected])]:rounded-md"
),
day: cn(
buttonVariants({ variant: "ghost" }),
"size-8 p-0 font-normal aria-selected:opacity-100"
),
day_range_start:
"day-range-start aria-selected:bg-primary aria-selected:text-primary-foreground",
day_range_end:
"day-range-end aria-selected:bg-primary aria-selected:text-primary-foreground",
day_selected:
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
day_today: "bg-accent text-accent-foreground",
day_outside:
"day-outside text-muted-foreground aria-selected:text-muted-foreground",
day_disabled: "text-muted-foreground opacity-50",
day_range_middle:
"aria-selected:bg-accent aria-selected:text-accent-foreground",
day_hidden: "invisible",
...classNames,
}}
components={
{
// IconLeft: ({ className, ...props }) => (
// <ChevronLeft className={cn("size-4", className)} {...props} />
// ),
// IconRight: ({ className, ...props }) => (
// <ChevronRight className={cn("size-4", className)} {...props} />
// ),
}
}
{...props}
/>
);
}
export { Calendar };

92
components/ui/card.tsx Normal file
View File

@ -0,0 +1,92 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card"
className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
className
)}
{...props}
/>
)
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className
)}
{...props}
/>
)
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn("leading-none font-semibold", className)}
{...props}
/>
)
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
{...props}
/>
)
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-6", className)}
{...props}
/>
)
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
{...props}
/>
)
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
}

View File

@ -0,0 +1,32 @@
"use client"
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { CheckIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Checkbox({
className,
...props
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
return (
<CheckboxPrimitive.Root
data-slot="checkbox"
className={cn(
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
data-slot="checkbox-indicator"
className="flex items-center justify-center text-current transition-none"
>
<CheckIcon className="size-3.5" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
)
}
export { Checkbox }

23
components/ui/chip.tsx Normal file
View File

@ -0,0 +1,23 @@
import React from "react";
import clsx from "clsx";
export type ChipColor = "default" | "primary" | "success" | "danger";
export interface ChipProps {
children: React.ReactNode;
color?: ChipColor;
className?: string;
size?: string;
variant?: string;
}
const colorMap: Record<ChipColor, string> = {
default: "bg-gray-200 text-gray-800",
primary: "bg-blue-100 text-blue-800",
success: "bg-green-100 text-green-800",
danger: "bg-red-100 text-red-800",
};
export const Chip: React.FC<ChipProps> = ({ children, color = "default", className }) => {
return <span className={clsx("inline-flex items-center px-3 py-1 rounded-full text-sm font-medium", colorMap[color], className)}>{children}</span>;
};

143
components/ui/dialog.tsx Normal file
View File

@ -0,0 +1,143 @@
"use client"
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { XIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Dialog({
...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />
}
function DialogTrigger({
...props
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
}
function DialogPortal({
...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
}
function DialogClose({
...props
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
}
function DialogOverlay({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
return (
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
)
}
function DialogContent({
className,
children,
showCloseButton = true,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean
}) {
return (
<DialogPortal data-slot="dialog-portal">
<DialogOverlay />
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close
data-slot="dialog-close"
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
>
<XIcon />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
)
}
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
)
}
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className
)}
{...props}
/>
)
}
function DialogTitle({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn("text-lg leading-none font-semibold", className)}
{...props}
/>
)
}
function DialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
}

View File

@ -0,0 +1,257 @@
"use client"
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function DropdownMenu({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
}
function DropdownMenuPortal({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
return (
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
)
}
function DropdownMenuTrigger({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
return (
<DropdownMenuPrimitive.Trigger
data-slot="dropdown-menu-trigger"
{...props}
/>
)
}
function DropdownMenuContent({
className,
sideOffset = 4,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
)
}
function DropdownMenuGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
return (
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
)
}
function DropdownMenuItem({
className,
inset,
variant = "default",
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
variant?: "default" | "destructive"
}) {
return (
<DropdownMenuPrimitive.Item
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function DropdownMenuCheckboxItem({
className,
children,
checked,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
return (
<DropdownMenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
)
}
function DropdownMenuRadioGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
return (
<DropdownMenuPrimitive.RadioGroup
data-slot="dropdown-menu-radio-group"
{...props}
/>
)
}
function DropdownMenuRadioItem({
className,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
return (
<DropdownMenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
)
}
function DropdownMenuLabel({
className,
inset,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.Label
data-slot="dropdown-menu-label"
data-inset={inset}
className={cn(
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className
)}
{...props}
/>
)
}
function DropdownMenuSeparator({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
return (
<DropdownMenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
)
}
function DropdownMenuShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="dropdown-menu-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className
)}
{...props}
/>
)
}
function DropdownMenuSub({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
}
function DropdownMenuSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.SubTrigger
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger>
)
}
function DropdownMenuSubContent({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
return (
<DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className
)}
{...props}
/>
)
}
export {
DropdownMenu,
DropdownMenuPortal,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
}

21
components/ui/input.tsx Normal file
View File

@ -0,0 +1,21 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<input
type={type}
data-slot="input"
className={cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"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",
className
)}
{...props}
/>
)
}
export { Input }

24
components/ui/label.tsx Normal file
View File

@ -0,0 +1,24 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cn } from "@/lib/utils"
function Label({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
return (
<LabelPrimitive.Root
data-slot="label"
className={cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
className
)}
{...props}
/>
)
}
export { Label }

View File

@ -0,0 +1,127 @@
import * as React from "react"
import {
ChevronLeftIcon,
ChevronRightIcon,
MoreHorizontalIcon,
} from "lucide-react"
import { cn } from "@/lib/utils"
import { Button, buttonVariants } from "@/components/ui/button"
function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
return (
<nav
role="navigation"
aria-label="pagination"
data-slot="pagination"
className={cn("mx-auto flex w-full justify-center", className)}
{...props}
/>
)
}
function PaginationContent({
className,
...props
}: React.ComponentProps<"ul">) {
return (
<ul
data-slot="pagination-content"
className={cn("flex flex-row items-center gap-1", className)}
{...props}
/>
)
}
function PaginationItem({ ...props }: React.ComponentProps<"li">) {
return <li data-slot="pagination-item" {...props} />
}
type PaginationLinkProps = {
isActive?: boolean
} & Pick<React.ComponentProps<typeof Button>, "size"> &
React.ComponentProps<"a">
function PaginationLink({
className,
isActive,
size = "icon",
...props
}: PaginationLinkProps) {
return (
<a
aria-current={isActive ? "page" : undefined}
data-slot="pagination-link"
data-active={isActive}
className={cn(
buttonVariants({
variant: isActive ? "outline" : "ghost",
size,
}),
className
)}
{...props}
/>
)
}
function PaginationPrevious({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) {
return (
<PaginationLink
aria-label="Go to previous page"
size="default"
className={cn("gap-1 px-2.5 sm:pl-2.5", className)}
{...props}
>
<ChevronLeftIcon />
<span className="hidden sm:block">Previous</span>
</PaginationLink>
)
}
function PaginationNext({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) {
return (
<PaginationLink
aria-label="Go to next page"
size="default"
className={cn("gap-1 px-2.5 sm:pr-2.5", className)}
{...props}
>
<span className="hidden sm:block">Next</span>
<ChevronRightIcon />
</PaginationLink>
)
}
function PaginationEllipsis({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
aria-hidden
data-slot="pagination-ellipsis"
className={cn("flex size-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontalIcon className="size-4" />
<span className="sr-only">More pages</span>
</span>
)
}
export {
Pagination,
PaginationContent,
PaginationLink,
PaginationItem,
PaginationPrevious,
PaginationNext,
PaginationEllipsis,
}

48
components/ui/popover.tsx Normal file
View File

@ -0,0 +1,48 @@
"use client"
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import { cn } from "@/lib/utils"
function Popover({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
return <PopoverPrimitive.Root data-slot="popover" {...props} />
}
function PopoverTrigger({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
}
function PopoverContent({
className,
align = "center",
sideOffset = 4,
...props
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
return (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
data-slot="popover-content"
align={align}
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
)
}
function PopoverAnchor({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
}
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }

View File

@ -0,0 +1,45 @@
"use client"
import * as React from "react"
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
import { CircleIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function RadioGroup({
className,
...props
}: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {
return (
<RadioGroupPrimitive.Root
data-slot="radio-group"
className={cn("grid gap-3", className)}
{...props}
/>
)
}
function RadioGroupItem({
className,
...props
}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {
return (
<RadioGroupPrimitive.Item
data-slot="radio-group-item"
className={cn(
"border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<RadioGroupPrimitive.Indicator
data-slot="radio-group-indicator"
className="relative flex items-center justify-center"
>
<CircleIcon className="fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
)
}
export { RadioGroup, RadioGroupItem }

View File

@ -0,0 +1,58 @@
"use client"
import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
import { cn } from "@/lib/utils"
function ScrollArea({
className,
children,
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
return (
<ScrollAreaPrimitive.Root
data-slot="scroll-area"
className={cn("relative", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport
data-slot="scroll-area-viewport"
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
>
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
)
}
function ScrollBar({
className,
orientation = "vertical",
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
return (
<ScrollAreaPrimitive.ScrollAreaScrollbar
data-slot="scroll-area-scrollbar"
orientation={orientation}
className={cn(
"flex touch-none p-px transition-colors select-none",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent",
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb
data-slot="scroll-area-thumb"
className="bg-border relative flex-1 rounded-full"
/>
</ScrollAreaPrimitive.ScrollAreaScrollbar>
)
}
export { ScrollArea, ScrollBar }

185
components/ui/select.tsx Normal file
View File

@ -0,0 +1,185 @@
"use client"
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Select({
...props
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
return <SelectPrimitive.Root data-slot="select" {...props} />
}
function SelectGroup({
...props
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
return <SelectPrimitive.Group data-slot="select-group" {...props} />
}
function SelectValue({
...props
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
return <SelectPrimitive.Value data-slot="select-value" {...props} />
}
function SelectTrigger({
className,
size = "default",
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
size?: "sm" | "default"
}) {
return (
<SelectPrimitive.Trigger
data-slot="select-trigger"
data-size={size}
className={cn(
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDownIcon className="size-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
)
}
function SelectContent({
className,
children,
position = "popper",
...props
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
data-slot="select-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
)
}
function SelectLabel({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
return (
<SelectPrimitive.Label
data-slot="select-label"
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
{...props}
/>
)
}
function SelectItem({
className,
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
return (
<SelectPrimitive.Item
data-slot="select-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className
)}
{...props}
>
<span className="absolute right-2 flex size-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
)
}
function SelectSeparator({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
return (
<SelectPrimitive.Separator
data-slot="select-separator"
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
{...props}
/>
)
}
function SelectScrollUpButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
return (
<SelectPrimitive.ScrollUpButton
data-slot="select-scroll-up-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUpIcon className="size-4" />
</SelectPrimitive.ScrollUpButton>
)
}
function SelectScrollDownButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
return (
<SelectPrimitive.ScrollDownButton
data-slot="select-scroll-down-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDownIcon className="size-4" />
</SelectPrimitive.ScrollDownButton>
)
}
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
}

31
components/ui/switch.tsx Normal file
View File

@ -0,0 +1,31 @@
"use client"
import * as React from "react"
import * as SwitchPrimitive from "@radix-ui/react-switch"
import { cn } from "@/lib/utils"
function Switch({
className,
...props
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
return (
<SwitchPrimitive.Root
data-slot="switch"
className={cn(
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<SwitchPrimitive.Thumb
data-slot="switch-thumb"
className={cn(
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitive.Root>
)
}
export { Switch }

116
components/ui/table.tsx Normal file
View File

@ -0,0 +1,116 @@
"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
function Table({ className, ...props }: React.ComponentProps<"table">) {
return (
<div
data-slot="table-container"
className="relative w-full overflow-x-auto"
>
<table
data-slot="table"
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
)
}
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
return (
<thead
data-slot="table-header"
className={cn("[&_tr]:border-b", className)}
{...props}
/>
)
}
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
return (
<tbody
data-slot="table-body"
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
)
}
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
return (
<tfoot
data-slot="table-footer"
className={cn(
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
className
)}
{...props}
/>
)
}
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
return (
<tr
data-slot="table-row"
className={cn(
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
className
)}
{...props}
/>
)
}
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
return (
<th
data-slot="table-head"
className={cn(
"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props}
/>
)
}
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
return (
<td
data-slot="table-cell"
className={cn(
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props}
/>
)
}
function TableCaption({
className,
...props
}: React.ComponentProps<"caption">) {
return (
<caption
data-slot="table-caption"
className={cn("text-muted-foreground mt-4 text-sm", className)}
{...props}
/>
)
}
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}

91
components/ui/tabs.tsx Normal file
View File

@ -0,0 +1,91 @@
"use client"
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { Tabs as TabsPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
function Tabs({
className,
orientation = "horizontal",
...props
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
return (
<TabsPrimitive.Root
data-slot="tabs"
data-orientation={orientation}
orientation={orientation}
className={cn(
"group/tabs flex gap-2 data-[orientation=horizontal]:flex-col",
className
)}
{...props}
/>
)
}
const tabsListVariants = cva(
"rounded-lg p-[3px] group-data-[orientation=horizontal]/tabs:h-9 data-[variant=line]:rounded-none group/tabs-list text-muted-foreground inline-flex w-fit items-center justify-center group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col",
{
variants: {
variant: {
default: "bg-muted",
line: "gap-1 bg-transparent",
},
},
defaultVariants: {
variant: "default",
},
}
)
function TabsList({
className,
variant = "default",
...props
}: React.ComponentProps<typeof TabsPrimitive.List> &
VariantProps<typeof tabsListVariants>) {
return (
<TabsPrimitive.List
data-slot="tabs-list"
data-variant={variant}
className={cn(tabsListVariants({ variant }), className)}
{...props}
/>
)
}
function TabsTrigger({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
return (
<TabsPrimitive.Trigger
data-slot="tabs-trigger"
className={cn(
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring text-foreground/60 hover:text-foreground dark:text-muted-foreground dark:hover:text-foreground relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent",
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 data-[state=active]:text-foreground",
"after:bg-foreground after:absolute after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100",
className
)}
{...props}
/>
)
}
function TabsContent({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
return (
<TabsPrimitive.Content
data-slot="tabs-content"
className={cn("flex-1 outline-none", className)}
{...props}
/>
)
}
export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants }

View File

@ -0,0 +1,18 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
return (
<textarea
data-slot="textarea"
className={cn(
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
{...props}
/>
)
}
export { Textarea }

61
components/ui/tooltip.tsx Normal file
View File

@ -0,0 +1,61 @@
"use client"
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import { cn } from "@/lib/utils"
function TooltipProvider({
delayDuration = 0,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
return (
<TooltipPrimitive.Provider
data-slot="tooltip-provider"
delayDuration={delayDuration}
{...props}
/>
)
}
function Tooltip({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
return (
<TooltipProvider>
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
</TooltipProvider>
)
}
function TooltipTrigger({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
}
function TooltipContent({
className,
sideOffset = 0,
children,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
data-slot="tooltip-content"
sideOffset={sideOffset}
className={cn(
"bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
className
)}
{...props}
>
{children}
<TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>
)
}
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }

View File

@ -0,0 +1,99 @@
"use client";
import { ChevronLeft } from "lucide-react";
export default function FilterSidebar() {
return (
<div className="bg-white rounded-2xl border border-gray-200 shadow-sm p-6">
{/* HEADER */}
<div className="flex items-center justify-between pb-4 border-b">
<h3 className="font-semibold text-sm flex items-center gap-2">
Filter
</h3>
<ChevronLeft size={16} className="text-gray-400" />
</div>
{/* CONTENT */}
<div className="space-y-6 mt-6">
{/* KATEGORI */}
<FilterSection title="Kategori">
<Checkbox label="Semua" count={1203} defaultChecked />
<Checkbox label="Berita Terhangat" count={123} />
<Checkbox label="Tentang Teknologi" count={24} />
<Checkbox label="Bersama Pelanggan" count={42} />
<Checkbox label="Pembicara Ahli" count={224} />
</FilterSection>
<Divider />
{/* JENIS FILE */}
<FilterSection title="Jenis File">
<Checkbox label="Semua" count={78} />
<Checkbox label="Audio Visual" count={120} defaultChecked />
<Checkbox label="Audio" count={34} />
<Checkbox label="Foto" count={234} />
<Checkbox label="Teks" count={9} />
</FilterSection>
<Divider />
{/* FORMAT */}
<FilterSection title="Format Audio Visual">
<Checkbox label="Semua" count={2} defaultChecked />
</FilterSection>
{/* RESET */}
<div className="text-center pt-4">
<button className="text-sm text-[#966314] font-medium hover:underline">
Reset Filter
</button>
</div>
</div>
</div>
);
}
/* ===== COMPONENTS ===== */
function FilterSection({
title,
children,
}: {
title: string;
children: React.ReactNode;
}) {
return (
<div>
<p className="text-sm font-medium mb-3">{title}</p>
<div className="space-y-2">{children}</div>
</div>
);
}
function Checkbox({
label,
count,
defaultChecked,
}: {
label: string;
count: number;
defaultChecked?: boolean;
}) {
return (
<label className="flex items-center justify-between text-sm cursor-pointer">
<div className="flex items-center gap-2">
<input
type="checkbox"
defaultChecked={defaultChecked}
className="h-4 w-4 accent-[#966314]"
/>
<span>{label}</span>
</div>
<span className="text-gray-400">({count})</span>
</label>
);
}
function Divider() {
return <div className="border-t border-gray-200"></div>;
}

View File

@ -0,0 +1,52 @@
"use client";
import Image from "next/image";
import Link from "next/link";
export default function VideoCard() {
const slug = "bharatu-mardi-hadji-gugur-saat-bertugas";
return (
<Link href={`/details/${slug}?type=video`}>
<div className="group bg-white rounded-2xl border border-gray-100 shadow-sm hover:shadow-md transition duration-300 overflow-hidden cursor-pointer">
{/* IMAGE */}
<div className="relative h-[200px] w-full">
<Image
src="/image/bharatu.jpg"
alt="news"
fill
className="object-cover group-hover:scale-105 transition duration-300"
/>
</div>
{/* CONTENT */}
<div className="p-5 space-y-3">
{/* BADGE + TAG */}
<div className="flex items-center gap-2 text-xs">
<span className="bg-red-600 text-white px-2 py-[3px] rounded-md font-medium">
POLRI
</span>
<span className="text-gray-500 uppercase tracking-wide">
SEPUTAR PRESTASI
</span>
</div>
{/* DATE */}
<p className="text-xs text-gray-400">02 Februari 2024</p>
{/* TITLE */}
<h3 className="font-semibold text-[15px] leading-snug line-clamp-2 group-hover:text-[#966314] transition">
Bharatu Mardi Hadji Gugur Saat Bertugas, Diganjar Kenaikan Pangkat
Luar Biasa
</h3>
{/* EXCERPT */}
<p className="text-sm text-gray-500 line-clamp-2 leading-relaxed">
Jakarta - Kapolri Jenderal Polisi Drs. Listyo Sigit Prabowo
memberikan kenaikan pangkat luar biasa anumerta kepada...
</p>
</div>
</div>
</Link>
);
}

92
config/swal.ts Normal file
View File

@ -0,0 +1,92 @@
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
const MySwal = withReactContent(Swal);
const Toast = MySwal.mixin({
toast: true,
position: "top-end",
showConfirmButton: false,
timer: 3000,
timerProgressBar: true,
didOpen: (toast: any) => {
toast.addEventListener("mouseenter", Swal.stopTimer);
toast.addEventListener("mouseleave", Swal.resumeTimer);
},
});
export function loading(msg?: any) {
let timerInterval: any;
MySwal.fire({
title: msg || "Loading...",
allowOutsideClick: false,
timerProgressBar: true,
didOpen: () => {
MySwal.showLoading();
timerInterval = setInterval(() => {}, 100);
},
willClose: () => {
clearInterval(timerInterval);
},
});
}
export function error(msg?: any) {
MySwal.fire({
icon: "error",
title: "Failed...",
text: msg || "Unknown Error",
customClass: {
popup: "custom-popup",
confirmButton: "custom-button",
},
});
}
export function successRouter(redirect: string, router?: any) {
MySwal.fire({
title: "Success!",
icon: "success",
confirmButtonColor: "#6642f5",
confirmButtonText: "Ok",
allowOutsideClick: false,
}).then((result: any) => {
if (result.isConfirmed) {
router.push(redirect);
}
});
}
export function success(title: string) {
Swal.fire({
title: "Success!",
icon: "success",
confirmButtonColor: "#6642f5",
confirmButtonText: "OK",
});
}
export function close() {
MySwal.close();
}
export function warning(text: string, redirect: string, router?: any) {
MySwal.fire({
title: text,
icon: "warning",
confirmButtonColor: "#3085d6",
confirmButtonText: "OK",
}).then((result: any) => {
if (result.isConfirmed) {
router.push(redirect);
}
});
}
export function successToast(title: string, text: string) {
Toast.fire({
icon: "success",
title: title,
text: text,
});
}

18
eslint.config.mjs Normal file
View File

@ -0,0 +1,18 @@
import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";
const eslintConfig = defineConfig([
...nextVitals,
...nextTs,
// Override default ignores of eslint-config-next.
globalIgnores([
// Default ignores of eslint-config-next:
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
]),
]);
export default eslintConfig;

6
lib/utils.ts Normal file
View File

@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

7
next.config.ts Normal file
View File

@ -0,0 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
};
export default nextConfig;

7102
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

42
package.json Normal file
View File

@ -0,0 +1,42 @@
{
"name": "portal-qudoco",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint"
},
"dependencies": {
"@iconify/iconify": "^3.1.1",
"@iconify/react": "^6.0.2",
"@radix-ui/react-slot": "^1.2.4",
"@types/js-cookie": "^3.0.6",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"framer-motion": "^12.33.0",
"js-cookie": "^3.0.5",
"lucide-react": "^0.562.0",
"next": "16.1.1",
"radix-ui": "^1.4.3",
"react": "19.2.3",
"react-datepicker": "^9.1.0",
"react-day-picker": "^9.13.2",
"react-dom": "19.2.3",
"sweetalert2": "^11.26.18",
"sweetalert2-react-content": "^5.1.1",
"tailwind-merge": "^3.4.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.1.1",
"tailwindcss": "^4",
"tw-animate-css": "^1.4.0",
"typescript": "^5"
}
}

7
postcss.config.mjs Normal file
View File

@ -0,0 +1,7 @@
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;

1
public/file.svg Normal file
View File

@ -0,0 +1 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 391 B

1
public/globe.svg Normal file
View File

@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
public/image/audio.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
public/image/aws.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

BIN
public/image/bharatu.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 394 KiB

BIN
public/image/dell.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

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