commit 5e61adcd5adb9c9a3aaa6c54a06061ef92d16455 Author: Anang Yusman Date: Tue Nov 11 10:52:38 2025 +0800 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5ef6a52 --- /dev/null +++ b/.gitignore @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/README.md b/README.md new file mode 100644 index 0000000..e215bc4 --- /dev/null +++ b/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/app/auth/layout.tsx b/app/auth/layout.tsx new file mode 100644 index 0000000..c9e99f9 --- /dev/null +++ b/app/auth/layout.tsx @@ -0,0 +1,7 @@ +export default function AuthLayout({ + children, +}: { + children: React.ReactNode; +}) { + return <> {children}; +} diff --git a/app/auth/page.tsx b/app/auth/page.tsx new file mode 100644 index 0000000..f21582a --- /dev/null +++ b/app/auth/page.tsx @@ -0,0 +1,10 @@ +import Login from "@/components/form/login"; +import React from "react"; + +export default function AuthPage() { + return ( + <> + + + ); +} diff --git a/app/auth/registration/page.tsx b/app/auth/registration/page.tsx new file mode 100644 index 0000000..bbb79e1 --- /dev/null +++ b/app/auth/registration/page.tsx @@ -0,0 +1,9 @@ +import Registration from "@/components/form/registration"; + +export default function AuthPage() { + return ( + <> + + + ); +} diff --git a/app/dashboard/admin/page.tsx b/app/dashboard/admin/page.tsx new file mode 100644 index 0000000..e5bb6a7 --- /dev/null +++ b/app/dashboard/admin/page.tsx @@ -0,0 +1,13 @@ +import AdminNavbar from "@/components/dashboard/admin-navbar"; +import AdminTable from "@/components/table/admin-table"; + +export default function AdminPage() { + return ( +
+ +
+ +
+
+ ); +} diff --git a/app/dashboard/approver/detail/[id]/page.tsx b/app/dashboard/approver/detail/[id]/page.tsx new file mode 100644 index 0000000..c9b25bf --- /dev/null +++ b/app/dashboard/approver/detail/[id]/page.tsx @@ -0,0 +1,14 @@ +import ApproverNavbar from "@/components/dashboard/approver-navbar"; +import ApproverDetail from "@/components/form/approver-detail"; +import ApproverTable from "@/components/table/approver-table"; + +export default function ApproverDetailPage() { + return ( +
+ +
+ +
+
+ ); +} diff --git a/app/dashboard/approver/page.tsx b/app/dashboard/approver/page.tsx new file mode 100644 index 0000000..cfb5175 --- /dev/null +++ b/app/dashboard/approver/page.tsx @@ -0,0 +1,13 @@ +import ApproverNavbar from "@/components/dashboard/approver-navbar"; +import ApproverTable from "@/components/table/approver-table"; + +export default function ApproverPage() { + return ( +
+ +
+ +
+
+ ); +} diff --git a/app/dashboard/supervisor/page.tsx b/app/dashboard/supervisor/page.tsx new file mode 100644 index 0000000..d659df5 --- /dev/null +++ b/app/dashboard/supervisor/page.tsx @@ -0,0 +1,14 @@ +import UserNavbar from "@/components/dashboard/user-navbar"; +import SupervisorData from "@/components/table/supervisor-data"; +import UserTable from "@/components/table/user-table"; + +export default function SupervisorPage() { + return ( +
+ +
+ +
+
+ ); +} diff --git a/app/dashboard/user/create/page.tsx b/app/dashboard/user/create/page.tsx new file mode 100644 index 0000000..04018ef --- /dev/null +++ b/app/dashboard/user/create/page.tsx @@ -0,0 +1,13 @@ +import UserNavbar from "@/components/dashboard/user-navbar"; +import FormCampaign from "@/components/form/campaign-form"; + +export default function UserCreatePage() { + return ( +
+ +
+ +
+
+ ); +} diff --git a/app/dashboard/user/page.tsx b/app/dashboard/user/page.tsx new file mode 100644 index 0000000..543e0d0 --- /dev/null +++ b/app/dashboard/user/page.tsx @@ -0,0 +1,13 @@ +import UserNavbar from "@/components/dashboard/user-navbar"; +import UserTable from "@/components/table/user-table"; + +export default function UserPage() { + return ( +
+ +
+ +
+
+ ); +} diff --git a/app/favicon.ico b/app/favicon.ico new file mode 100644 index 0000000..718d6fe Binary files /dev/null and b/app/favicon.ico differ diff --git a/app/globals.css b/app/globals.css new file mode 100644 index 0000000..dc98be7 --- /dev/null +++ b/app/globals.css @@ -0,0 +1,122 @@ +@import "tailwindcss"; +@import "tw-animate-css"; + +@custom-variant dark (&:is(.dark *)); + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --font-sans: var(--font-geist-sans); + --font-mono: var(--font-geist-mono); + --color-sidebar-ring: var(--sidebar-ring); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar: var(--sidebar); + --color-chart-5: var(--chart-5); + --color-chart-4: var(--chart-4); + --color-chart-3: var(--chart-3); + --color-chart-2: var(--chart-2); + --color-chart-1: var(--chart-1); + --color-ring: var(--ring); + --color-input: var(--input); + --color-border: var(--border); + --color-destructive: var(--destructive); + --color-accent-foreground: var(--accent-foreground); + --color-accent: var(--accent); + --color-muted-foreground: var(--muted-foreground); + --color-muted: var(--muted); + --color-secondary-foreground: var(--secondary-foreground); + --color-secondary: var(--secondary); + --color-primary-foreground: var(--primary-foreground); + --color-primary: var(--primary); + --color-popover-foreground: var(--popover-foreground); + --color-popover: var(--popover); + --color-card-foreground: var(--card-foreground); + --color-card: var(--card); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); +} + +:root { + --radius: 0.625rem; + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); +} + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 0000000..f7fa87e --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,34 @@ +import type { Metadata } from "next"; +import { Geist, Geist_Mono } from "next/font/google"; +import "./globals.css"; + +const geistSans = Geist({ + variable: "--font-geist-sans", + subsets: ["latin"], +}); + +const geistMono = Geist_Mono({ + variable: "--font-geist-mono", + subsets: ["latin"], +}); + +export const metadata: Metadata = { + title: "Create Next App", + description: "Generated by create next app", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + {children} + + + ); +} diff --git a/app/page.tsx b/app/page.tsx new file mode 100644 index 0000000..59962cd --- /dev/null +++ b/app/page.tsx @@ -0,0 +1,15 @@ +import Footer from "@/components/landing-page/footer"; +import Header from "@/components/landing-page/header"; +import Navbar from "@/components/landing-page/navbar"; + +export default function Home() { + return ( +
+
+ +
+
+
+
+ ); +} diff --git a/components.json b/components.json new file mode 100644 index 0000000..b7b9791 --- /dev/null +++ b/components.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "registries": {} +} diff --git a/components/dashboard/admin-navbar.tsx b/components/dashboard/admin-navbar.tsx new file mode 100644 index 0000000..351b190 --- /dev/null +++ b/components/dashboard/admin-navbar.tsx @@ -0,0 +1,57 @@ +"use client"; +import Image from "next/image"; +import { Bell } from "lucide-react"; +import { useState } from "react"; +import Link from "next/link"; + +export default function AdminNavbar() { + const [activeTab, setActiveTab] = useState("Manajemen User"); + + return ( +
+ {/* Logo */} +
+ + Logo + +
+ + {/* Middle Tabs */} +
+ +
+ + {/* Right Section */} +
+ {/* Notifikasi */} + + + {/* Avatar Admin */} +
+ User +
+
+
+ ); +} diff --git a/components/dashboard/approver-navbar.tsx b/components/dashboard/approver-navbar.tsx new file mode 100644 index 0000000..4b86822 --- /dev/null +++ b/components/dashboard/approver-navbar.tsx @@ -0,0 +1,62 @@ +"use client"; + +import Image from "next/image"; +import { Bell } from "lucide-react"; +import { useState } from "react"; +import Link from "next/link"; + +export default function ApproverNavbar() { + const [activeTab, setActiveTab] = useState("Kurasi Konten"); + + return ( +
+
+ {/* Left: Logo */} +
+ + Logo + +
+ + {/* Middle: Tabs */} +
+ {["Kurasi Konten", "Publish Konten"].map((tab) => ( + + ))} +
+ + {/* Right: Notification + Avatar */} +
+ + User +
+
+
+ ); +} diff --git a/components/dashboard/user-navbar.tsx b/components/dashboard/user-navbar.tsx new file mode 100644 index 0000000..730e54c --- /dev/null +++ b/components/dashboard/user-navbar.tsx @@ -0,0 +1,36 @@ +"use client"; +import Image from "next/image"; +import { Bell, User } from "lucide-react"; +import Link from "next/link"; + +export default function UserNavbar() { + return ( +
+ {/* Logo */} +
+ + Logo + +
+ + {/* Right Section */} +
+ + +
+ User + Admin +
+
+
+ ); +} diff --git a/components/dialog/admin-detail.tsx b/components/dialog/admin-detail.tsx new file mode 100644 index 0000000..ce7ca96 --- /dev/null +++ b/components/dialog/admin-detail.tsx @@ -0,0 +1,77 @@ +"use client"; + +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; + +export default function DialogUserDetail({ + isOpen, + onClose, + user, +}: { + isOpen: boolean; + onClose: () => void; + user: { + fullName: string; + email: string; + createdAt: string; + status: string; + } | null; +}) { + if (!user) return null; + + return ( + + + + Detail + + + {/* Status badge */} +
+ + {user.status} + +
+ +
+
+

Nama Lengkap

+

{user.fullName}

+
+ +
+

Email

+

{user.email}

+
+ +
+

Tanggal Pendaftaran

+

{user.createdAt}

+
+
+ + + + +
+
+ ); +} diff --git a/components/dialog/campaign-detail.tsx b/components/dialog/campaign-detail.tsx new file mode 100644 index 0000000..c39fda0 --- /dev/null +++ b/components/dialog/campaign-detail.tsx @@ -0,0 +1,97 @@ +"use client"; + +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { FileText } from "lucide-react"; + +interface DialogCampaignDetailProps { + isOpen: boolean; + onClose: () => void; + data?: { + durasi: string; + media: string; + tujuan: string; + materi: string; + status: string; + }; +} + +export default function DialogCampaignDetail({ + isOpen, + onClose, + data, +}: DialogCampaignDetailProps) { + if (!data) return null; + + return ( + + + + Detail + + +
+ {/* Status */} +
+ + {data.status} + +
+ + {/* Detail Info */} +
+

+ Durasi +
+ {data.durasi} +

+ +

+ Media +
+ {data.media} +

+ +

+ Tujuan +
+ {data.tujuan} +

+ +

+ Materi Promote +

+ + {/* File Box */} +
+
+ +
+

+ Report_name_T1.pdf +

+

23.5MB

+
+
+
+
+
+ + + + + +
+
+ ); +} diff --git a/components/dialog/media-online.tsx b/components/dialog/media-online.tsx new file mode 100644 index 0000000..5eb1de5 --- /dev/null +++ b/components/dialog/media-online.tsx @@ -0,0 +1,177 @@ +import { useState, useMemo } from "react"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; + +export default function DialogMediaOnline({ + isOpen, + onClose, +}: { + isOpen: boolean; + onClose: () => void; +}) { + const mediaOnlineList = [ + "Tribrata News Mabes", + "Tribrata News Polda Aceh", + "Tribrata News Polda Sumatera Utara", + "Tribrata News Polda Sumatera Barat", + "Tribrata News Polda Riau", + "Tribrata News Polda Kep. Riau", + "Tribrata News Polda Jambi", + "Tribrata News Polda Bengkulu", + "Tribrata News Polda Lampung", + "Tribrata News Polda Banten", + "Tribrata News Polda Metro Jaya", + "Tribrata News Polda Jawa Barat", + "Tribrata News Polda Jawa Tengah", + "Tribrata News Polda D.I Yogyakarta", + "Tribrata News Polda Jawa Timur", + "Tribrata News Polda Bali", + "Tribrata News Polda Nusa Tenggara Barat", + "Tribrata News Polda Nusa Tenggara Timur", + "Tribrata News Polda Kalimantan Barat", + "Tribrata News Polda Kalimantan Tengah", + "Tribrata News Polda Kalimantan Selatan", + "Tribrata News Polda Kalimantan Timur", + "Tribrata News Polda Kalimantan Utara", + "Tribrata News Polda Sulawesi Utara", + "Tribrata News Polda Gorontalo", + "Tribrata News Polda Sulawesi Tengah", + "Tribrata News Polda Sulawesi Barat", + "Tribrata News Polda Sulawesi Selatan", + "Tribrata News Polda Sulawesi Tenggara", + "Tribrata News Polda Maluku", + "Tribrata News Polda Maluku Utara", + "Tribrata News Polda Papua", + "Tribrata News Polda Papua Barat", + ]; + + const [selectedMediaOnline, setSelectedMediaOnline] = useState([]); + const [searchTerm, setSearchTerm] = useState(""); + + // 🔍 Filter media berdasarkan pencarian + const filteredMedia = useMemo(() => { + return mediaOnlineList.filter((item) => + item.toLowerCase().includes(searchTerm.toLowerCase()) + ); + }, [searchTerm]); + + // ✅ Toggle media + const toggleMediaOnline = (item: string) => { + setSelectedMediaOnline((prev) => + prev.includes(item) ? prev.filter((m) => m !== item) : [...prev, item] + ); + }; + + // ✅ Pilih semua + const toggleSelectAll = () => { + if (selectedMediaOnline.length === mediaOnlineList.length) { + setSelectedMediaOnline([]); + } else { + setSelectedMediaOnline(mediaOnlineList); + } + }; + + return ( + + + + Pilih Media Online + + + {/* 🔍 Input Cari */} +
+ setSearchTerm(e.target.value)} + className="w-full pl-10 pr-4 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" + /> + + + +
+ + {/* ✅ Konten scrollable */} +
+ + +
+ {filteredMedia.map((item) => ( + + ))} +
+
+ + {/* 🏷️ Tag Media Terpilih (scrollable juga bila panjang) */} + {selectedMediaOnline.length > 0 && ( +
+

+ {selectedMediaOnline.length} Media Online dipilih +

+
+ {selectedMediaOnline.map((m) => ( +
+ {m} + +
+ ))} +
+
+ )} + + {/* 🔘 Tombol Footer */} + + + + +
+
+ ); +} diff --git a/components/dialog/media-sosial.tsx b/components/dialog/media-sosial.tsx new file mode 100644 index 0000000..e006ee3 --- /dev/null +++ b/components/dialog/media-sosial.tsx @@ -0,0 +1,236 @@ +import { useState, useMemo } from "react"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { ChevronDown, ChevronUp } from "lucide-react"; + +export default function DialogMediaSosial({ + isOpen, + onClose, +}: { + isOpen: boolean; + onClose: () => void; +}) { + const mediaPlatforms = ["X", "Instagram", "Facebook", "Youtube", "TikTok"]; + + const mediaPerPlatform: Record = { + X: ["X Mabes", "X Polda Jawa Timur", "X Polda Jawa Barat"], + Instagram: [ + "Instagram Mabes", + "Instagram Polda Jawa Timur", + "Instagram Polda Jawa Barat", + ], + Facebook: [ + "Facebook Mabes", + "Facebook Polda Jawa Timur", + "Facebook Polda Jawa Barat", + ], + Youtube: [ + "Youtube Mabes", + "Youtube Polda Jawa Timur", + "Youtube Polda Jawa Barat", + ], + TikTok: [ + "TikTok Mabes", + "TikTok Polda Jawa Timur", + "TikTok Polda Jawa Barat", + ], + }; + + const [selectedMediaSosial, setSelectedMediaSosial] = useState([]); + const [searchTerm, setSearchTerm] = useState(""); + const [expanded, setExpanded] = useState>({}); + const [selectedPlatforms, setSelectedPlatforms] = useState([ + "X", + "Instagram", + "Facebook", + "Youtube", + "TikTok", + ]); + + // Filter berdasarkan pencarian + const filteredPlatforms = useMemo(() => { + return mediaPlatforms.filter((p) => + p.toLowerCase().includes(searchTerm.toLowerCase()) + ); + }, [searchTerm]); + + // Toggle platform (misalnya centang Instagram) + const togglePlatform = (platform: string) => { + if (selectedPlatforms.includes(platform)) { + setSelectedPlatforms(selectedPlatforms.filter((p) => p !== platform)); + // hapus semua media sosial di dalam platform tsb + setSelectedMediaSosial((prev) => + prev.filter((m) => !mediaPerPlatform[platform].includes(m)) + ); + } else { + setSelectedPlatforms([...selectedPlatforms, platform]); + setSelectedMediaSosial((prev) => [ + ...prev, + ...mediaPerPlatform[platform], + ]); + } + }; + + // Toggle semua platform + const toggleSelectAll = () => { + if (selectedPlatforms.length === mediaPlatforms.length) { + setSelectedPlatforms([]); + setSelectedMediaSosial([]); + } else { + setSelectedPlatforms([...mediaPlatforms]); + setSelectedMediaSosial(Object.values(mediaPerPlatform).flat()); + } + }; + + // Expand/collapse per platform + const toggleExpand = (platform: string) => { + setExpanded((prev) => ({ ...prev, [platform]: !prev[platform] })); + }; + + // Toggle media per item + const toggleMedia = (media: string) => { + setSelectedMediaSosial((prev) => + prev.includes(media) ? prev.filter((m) => m !== media) : [...prev, media] + ); + }; + + return ( + + + + Pilih Media Sosial + + + {/* Input Pencarian */} +
+ setSearchTerm(e.target.value)} + className="w-full pl-10 pr-4 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" + /> + + + +
+ + {/* Checkbox utama */} +
+ + + {filteredPlatforms.map((platform) => ( + + ))} +
+ + {/* Expand per platform */} +
+ {filteredPlatforms.map((platform) => ( +
+ + + {expanded[platform] && ( +
+ {mediaPerPlatform[platform].map((media) => ( + + ))} +
+ )} +
+ ))} +
+ + {/* Tag media sosial dipilih */} + {selectedMediaSosial.length > 0 && ( +
+

+ {selectedMediaSosial.length} Media Sosial dipilih +

+
+ {selectedMediaSosial.map((m) => ( +
+ {m} + +
+ ))} +
+
+ )} + + {/* Tombol Footer */} + + + + +
+
+ ); +} diff --git a/components/dialog/videotron.tsx b/components/dialog/videotron.tsx new file mode 100644 index 0000000..db98b42 --- /dev/null +++ b/components/dialog/videotron.tsx @@ -0,0 +1,218 @@ +import { useState } from "react"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { ChevronDown, ChevronUp } from "lucide-react"; + +// daftar kategori utama +const mainCategories = ["Mabes", "Polda", "PTIK", "LOGistik"]; + +// daftar Polda +const poldaList = [ + "Polda Aceh", + "Polda Jawa Timur", + "Polda Papua", + "Polda Sumatra Utara", + "Polda Sumatra Barat", + "Polda Riau", + "Polda Kep. Riau", + "Polda Jambi", + "Polda Jawa Tengah", + "Polda Metro Jaya", + "Polda Jawa Barat", + "Polda Banten", + "Polda D.I Yogyakarta", + "Polda Sumatra Selatan", + "Polda Kep. Bangka Belitung", + "Polda Bengkulu", + "Polda Lampung", +]; + +export default function DialogVideotron({ + isOpen, + onClose, +}: { + isOpen: boolean; + onClose: () => void; +}) { + const [searchTerm, setSearchTerm] = useState(""); + const [expanded, setExpanded] = useState(true); + const [selectedCategories, setSelectedCategories] = useState([ + "Mabes", + "Polda", + "PTIK", + "LOGistik", + ]); + const [selectedPolda, setSelectedPolda] = useState([]); + + const toggleExpand = () => setExpanded(!expanded); + + const toggleCategory = (cat: string) => { + setSelectedCategories((prev) => + prev.includes(cat) ? prev.filter((c) => c !== cat) : [...prev, cat] + ); + }; + + const toggleSelectAllCategories = () => { + if (selectedCategories.length === mainCategories.length) { + setSelectedCategories([]); + } else { + setSelectedCategories([...mainCategories]); + } + }; + + const togglePolda = (polda: string) => { + setSelectedPolda((prev) => + prev.includes(polda) ? prev.filter((p) => p !== polda) : [...prev, polda] + ); + }; + + return ( + + + + Pilih Videotron + + + {/* SEARCH */} +
+ setSearchTerm(e.target.value)} + className="w-full pl-10 pr-4 py-2 border rounded-md focus:ring-2 focus:ring-blue-500" + /> + + + +
+ + {/* KATEGORI */} +

Pilih Media Sosial

+
+ + + {mainCategories.map((cat) => ( + + ))} +
+ + {/* PILIH POLDA */} +
+ + + {expanded && ( +
+ {poldaList.map((polda) => ( + + ))} +
+ )} +
+ + {/* TAG TERPILIH */} + {selectedCategories.length + selectedPolda.length > 0 && ( +
+

+ {selectedCategories.length + selectedPolda.length} Videotron + dipilih +

+ +
+ {selectedCategories.map((item) => ( +
+ {item} + +
+ ))} + + {selectedPolda.map((polda) => ( +
+ {polda} + +
+ ))} +
+
+ )} + + + + + +
+
+ ); +} diff --git a/components/form/approver-detail.tsx b/components/form/approver-detail.tsx new file mode 100644 index 0000000..661668f --- /dev/null +++ b/components/form/approver-detail.tsx @@ -0,0 +1,367 @@ +"use client"; + +import { useState } from "react"; +import { + Paperclip, + Send, + Smile, + Mic, + CheckCircle2, + Upload, +} from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { cn } from "@/lib/utils"; + +export default function ApproverDetail() { + const [activeTab, setActiveTab] = useState<"detail" | "buatKonten">("detail"); + const [step, setStep] = useState<"configuration" | "publish">( + "configuration" + ); + + return ( +
+ {/* Header Tabs */} +
+ + +
+ + {/* DETAIL KONTEN */} + {activeTab === "detail" ? ( + <> +
+

Status

+ + Tertunda + +
+ +
+

Judul Campaign

+

+ Lorem ipsum dolor sit amet consectetur. In faucibus diam eu ut + quisque. +

+
+ +
+

Durasi

+

+ 22/08/2025 - 22/08/2026 +

+
+ +
+

Media

+

+ Media Online +

+
    +
  • Tribrata News Mabes
  • +
  • Tribrata News Polda Aceh
  • +
  • Tribrata News Polda Jawa Timur
  • +
  • Tribrata News Polda Jawa Tengah
  • +
  • Tribrata News Polda Jawa Barat
  • +
+
+ +
+

Tujuan

+

Sosialisasi

+
+ +
+

Materi Promote

+
+
+

+ Report name_T1.pdf +

+

23.5MB

+
+ +
+
+ +
+

Komentar

+ +
+ Kurator POLRI +
+