Initial commit
|
|
@ -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
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>;
|
||||||
|
}
|
||||||
|
|
@ -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 >
|
||||||
|
<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 ></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
export default function AuthLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return <> {children}</>;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
import Login from "@/components/form/login";
|
||||||
|
|
||||||
|
export default function AuthPage() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Login />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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 >
|
||||||
|
<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 ></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 25 KiB |
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 >
|
||||||
|
<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 ></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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 >
|
||||||
|
<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 ></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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": {}
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>;
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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} | {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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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} | {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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>;
|
||||||
|
}
|
||||||
|
|
@ -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">
|
||||||
|
Don’t 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
|
@ -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>;
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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);
|
||||||
|
};
|
||||||
|
|
@ -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;
|
||||||
|
};
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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 />}</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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 }
|
||||||
|
|
@ -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 };
|
||||||
|
|
@ -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,
|
||||||
|
}
|
||||||
|
|
@ -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 }
|
||||||
|
|
@ -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 };
|
||||||
|
|
@ -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,
|
||||||
|
}
|
||||||
|
|
@ -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 }
|
||||||
|
|
@ -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>;
|
||||||
|
};
|
||||||
|
|
@ -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,
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
}
|
||||||
|
|
@ -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 }
|
||||||
|
|
@ -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 }
|
||||||
|
|
@ -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,
|
||||||
|
}
|
||||||
|
|
@ -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 }
|
||||||
|
|
@ -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 }
|
||||||
|
|
@ -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 }
|
||||||
|
|
@ -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,
|
||||||
|
}
|
||||||
|
|
@ -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 }
|
||||||
|
|
@ -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,
|
||||||
|
}
|
||||||
|
|
@ -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 }
|
||||||
|
|
@ -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 }
|
||||||
|
|
@ -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 }
|
||||||
|
|
@ -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>;
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { clsx, type ClassValue } from "clsx"
|
||||||
|
import { twMerge } from "tailwind-merge"
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs))
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
|
const nextConfig: NextConfig = {
|
||||||
|
/* config options here */
|
||||||
|
};
|
||||||
|
|
||||||
|
export default nextConfig;
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
const config = {
|
||||||
|
plugins: {
|
||||||
|
"@tailwindcss/postcss": {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
|
@ -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 |
|
|
@ -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 |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 394 KiB |
|
After Width: | Height: | Size: 35 KiB |