Initial commit

This commit is contained in:
Anang Yusman 2025-11-11 10:52:38 +08:00
commit 5e61adcd5a
70 changed files with 6978 additions and 0 deletions

41
.gitignore vendored Normal file
View File

@ -0,0 +1,41 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

36
README.md Normal file
View File

@ -0,0 +1,36 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.

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

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

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

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

View File

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

View File

@ -0,0 +1,13 @@
import AdminNavbar from "@/components/dashboard/admin-navbar";
import AdminTable from "@/components/table/admin-table";
export default function AdminPage() {
return (
<div className="min-h-screen bg-[#f8f9fa]">
<AdminNavbar />
<div className="p-6">
<AdminTable />
</div>
</div>
);
}

View File

@ -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 (
<div className="min-h-screen bg-[#f8f9fa]">
<ApproverNavbar />
<div className="p-6">
<ApproverDetail />
</div>
</div>
);
}

View File

@ -0,0 +1,13 @@
import ApproverNavbar from "@/components/dashboard/approver-navbar";
import ApproverTable from "@/components/table/approver-table";
export default function ApproverPage() {
return (
<div className="min-h-screen bg-[#f8f9fa]">
<ApproverNavbar />
<div className="p-6">
<ApproverTable />
</div>
</div>
);
}

View File

@ -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 (
<div className="min-h-screen bg-[#f8f9fa]">
<UserNavbar />
<div className="p-6">
<SupervisorData />
</div>
</div>
);
}

View File

@ -0,0 +1,13 @@
import UserNavbar from "@/components/dashboard/user-navbar";
import FormCampaign from "@/components/form/campaign-form";
export default function UserCreatePage() {
return (
<div className="min-h-screen bg-[#f8f9fa]">
<UserNavbar />
<div className="p-6">
<FormCampaign />
</div>
</div>
);
}

View File

@ -0,0 +1,13 @@
import UserNavbar from "@/components/dashboard/user-navbar";
import UserTable from "@/components/table/user-table";
export default function UserPage() {
return (
<div className="min-h-screen bg-[#f8f9fa]">
<UserNavbar />
<div className="p-6">
<UserTable />
</div>
</div>
);
}

BIN
app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

122
app/globals.css Normal file
View File

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

34
app/layout.tsx Normal file
View File

@ -0,0 +1,34 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
</body>
</html>
);
}

15
app/page.tsx Normal file
View File

@ -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 (
<div className="flex min-h-screen flex-col font-[family-name:var(--font-geist-sans)] bg-white ">
<div className="bg-[#f2f2f2]">
<Navbar />
<Header />
<Footer />
</div>
</div>
);
}

22
components.json Normal file
View File

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

View File

@ -0,0 +1,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 (
<div className="w-full bg-white shadow-sm flex justify-between items-center px-6 py-3 border-b border-gray-200">
{/* Logo */}
<div className="flex items-center gap-2">
<Link href={"/"}>
<Image src="/campaign.png" alt="Logo" width={60} height={60} />
</Link>
</div>
{/* Middle Tabs */}
<div className="flex items-center gap-8">
<button
onClick={() => setActiveTab("Manajemen User")}
className={`flex flex-col items-center text-sm font-medium px-4 py-2 rounded-md transition ${
activeTab === "Manajemen User"
? "bg-[#f7f7f7] border-b-2 border-[#C4A663] text-gray-900"
: "text-gray-500 hover:text-gray-800"
}`}
>
<span className="text-base">🖼</span>
<span>Manajemen User</span>
</button>
</div>
{/* Right Section */}
<div className="flex items-center gap-4">
{/* Notifikasi */}
<button className="relative text-gray-600 hover:text-gray-800">
<Bell className="w-5 h-5" />
<span className="absolute -top-1 -right-1 bg-blue-500 text-white text-[10px] w-4 h-4 flex items-center justify-center rounded-full">
6
</span>
</button>
{/* Avatar Admin */}
<div className="flex items-center gap-2">
<Image
src="/non-user.png"
alt="User"
width={36}
height={36}
className="rounded-full border"
/>
</div>
</div>
</div>
);
}

View File

@ -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 (
<div className="w-full bg-white border-b shadow-sm">
<div className="flex justify-between items-center px-6 py-3">
{/* Left: Logo */}
<div className="flex items-center">
<Link href={"/"}>
<Image src="/campaign.png" alt="Logo" width={60} height={60} />
</Link>
</div>
{/* Middle: Tabs */}
<div className="flex items-center gap-12">
{["Kurasi Konten", "Publish Konten"].map((tab) => (
<button
key={tab}
onClick={() => setActiveTab(tab)}
className={`flex flex-col items-center text-sm font-medium transition ${
activeTab === tab
? "text-black border-b-2 border-[#C4A663]"
: "text-gray-500 hover:text-gray-800"
}`}
>
{tab === "Kurasi Konten" ? (
<span className="text-xl mb-1">🖼</span>
) : (
<span className="text-xl mb-1">🔁</span>
)}
<span>{tab}</span>
</button>
))}
</div>
{/* Right: Notification + Avatar */}
<div className="flex items-center gap-4">
<button className="relative text-gray-600 hover:text-gray-800">
<Bell className="w-5 h-5" />
<span className="absolute -top-1 -right-1 bg-blue-500 text-white text-[10px] w-4 h-4 flex items-center justify-center rounded-full">
6
</span>
</button>
<Image
src="/non-user.png"
alt="User"
width={36}
height={36}
className="rounded-full border"
/>
</div>
</div>
</div>
);
}

View File

@ -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 (
<div className="w-full bg-white shadow-sm flex justify-between items-center px-6 py-3">
{/* Logo */}
<div className="flex items-center gap-2">
<Link href={"/"}>
<Image src="/campaign.png" alt="Logo" width={60} height={60} />
</Link>
</div>
{/* Right Section */}
<div className="flex items-center gap-4">
<button className="relative text-gray-600 hover:text-gray-800">
<Bell className="w-5 h-5" />
<span className="absolute top-0 right-0 bg-red-500 text-white rounded-full w-2 h-2"></span>
</button>
<div className="flex items-center gap-2">
<Image
src="/non-user.png"
alt="User"
width={32}
height={32}
className="rounded-full border"
/>
<span className="text-gray-700 text-sm font-medium">Admin</span>
</div>
</div>
</div>
);
}

View File

@ -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 (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle className="text-lg font-semibold">Detail</DialogTitle>
</DialogHeader>
{/* Status badge */}
<div className="mb-4">
<span
className={`px-3 py-1 rounded-full text-xs font-medium ${
user.status === "Approved"
? "bg-green-100 text-green-700"
: "bg-gray-200 text-gray-700"
}`}
>
{user.status}
</span>
</div>
<div className="space-y-4 text-sm">
<div>
<p className="text-gray-500">Nama Lengkap</p>
<p className="font-semibold text-gray-900">{user.fullName}</p>
</div>
<div>
<p className="text-gray-500">Email</p>
<p className="font-semibold text-gray-900">{user.email}</p>
</div>
<div>
<p className="text-gray-500">Tanggal Pendaftaran</p>
<p className="font-semibold text-gray-900">{user.createdAt}</p>
</div>
</div>
<DialogFooter>
<Button
variant="outline"
onClick={onClose}
className="w-full text-gray-800"
>
Close
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@ -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 (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle className="text-lg font-semibold">Detail</DialogTitle>
</DialogHeader>
<div className="space-y-4">
{/* Status */}
<div>
<span className="bg-green-100 text-green-700 border border-green-600 px-3 py-1 rounded-full text-xs font-medium">
{data.status}
</span>
</div>
{/* Detail Info */}
<div className="space-y-2 text-sm">
<p>
<span className="text-gray-500">Durasi</span>
<br />
<span className="font-semibold">{data.durasi}</span>
</p>
<p>
<span className="text-gray-500">Media</span>
<br />
<span className="font-semibold">{data.media}</span>
</p>
<p>
<span className="text-gray-500">Tujuan</span>
<br />
<span className="font-semibold">{data.tujuan}</span>
</p>
<p>
<span className="text-gray-500">Materi Promote</span>
</p>
{/* File Box */}
<div className="border rounded-lg p-3 flex items-center justify-between">
<div className="flex items-center gap-2">
<FileText className="text-gray-500" />
<div>
<p className="text-sm font-medium text-gray-800">
Report_name_T1.pdf
</p>
<p className="text-xs text-gray-500">23.5MB</p>
</div>
</div>
</div>
</div>
</div>
<DialogFooter className="flex justify-end gap-3 mt-4">
<Button variant="outline" onClick={onClose} className="w-28">
Cancel
</Button>
<Button className="bg-blue-600 hover:bg-blue-700 text-white w-28">
Next
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@ -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<string[]>([]);
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 (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="w-full max-w-3xl sm:max-h-[90vh] overflow-hidden flex flex-col">
<DialogHeader>
<DialogTitle>Pilih Media Online</DialogTitle>
</DialogHeader>
{/* 🔍 Input Cari */}
<div className="relative mb-4">
<input
type="text"
placeholder="Cari Media Online..."
value={searchTerm}
onChange={(e) => 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"
/>
<svg
xmlns="http://www.w3.org/2000/svg"
className="w-4 h-4 absolute left-3 top-3 text-gray-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M21 21l-4.35-4.35m0 0A7.5 7.5 0 104.5 4.5a7.5 7.5 0 0012.15 12.15z"
/>
</svg>
</div>
{/* ✅ Konten scrollable */}
<div className="flex-1 overflow-y-auto border rounded-md p-2 space-y-2">
<label className="flex items-center gap-2 p-2 rounded-md hover:bg-gray-50 cursor-pointer font-medium sticky top-0 bg-white z-10 border-b">
<input
type="checkbox"
checked={selectedMediaOnline.length === mediaOnlineList.length}
onChange={toggleSelectAll}
className="accent-blue-600"
/>
Pilih semua Media Online
</label>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
{filteredMedia.map((item) => (
<label
key={item}
className="flex items-center gap-2 p-2 rounded-md hover:bg-gray-50 cursor-pointer"
>
<input
type="checkbox"
checked={selectedMediaOnline.includes(item)}
onChange={() => toggleMediaOnline(item)}
className="accent-blue-600"
/>
<span>{item}</span>
</label>
))}
</div>
</div>
{/* 🏷️ Tag Media Terpilih (scrollable juga bila panjang) */}
{selectedMediaOnline.length > 0 && (
<div className="mt-3">
<p className="text-sm font-medium mb-2">
{selectedMediaOnline.length} Media Online dipilih
</p>
<div className="flex flex-wrap gap-2 max-h-[100px] overflow-y-auto border rounded-md p-2 bg-gray-50">
{selectedMediaOnline.map((m) => (
<div
key={m}
className="flex items-center gap-2 px-3 py-1 bg-white rounded-full text-sm border"
>
{m}
<button
onClick={() => toggleMediaOnline(m)}
className="text-gray-500 hover:text-gray-700"
>
</button>
</div>
))}
</div>
</div>
)}
{/* 🔘 Tombol Footer */}
<DialogFooter className="mt-4">
<Button variant="outline" onClick={onClose}>
Batal
</Button>
<Button onClick={onClose}>Pilih</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@ -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<string, string[]> = {
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<string[]>([]);
const [searchTerm, setSearchTerm] = useState("");
const [expanded, setExpanded] = useState<Record<string, boolean>>({});
const [selectedPlatforms, setSelectedPlatforms] = useState<string[]>([
"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 (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="w-full max-w-3xl sm:max-h-[90vh] overflow-hidden flex flex-col">
<DialogHeader>
<DialogTitle>Pilih Media Sosial</DialogTitle>
</DialogHeader>
{/* Input Pencarian */}
<div className="relative mb-4">
<input
type="text"
placeholder="Cari Media Sosial..."
value={searchTerm}
onChange={(e) => 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"
/>
<svg
xmlns="http://www.w3.org/2000/svg"
className="w-4 h-4 absolute left-3 top-3 text-gray-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M21 21l-4.35-4.35m0 0A7.5 7.5 0 104.5 4.5a7.5 7.5 0 0012.15 12.15z"
/>
</svg>
</div>
{/* Checkbox utama */}
<div className="flex flex-wrap gap-3 mb-3">
<label className="flex items-center gap-2 font-medium cursor-pointer">
<input
type="checkbox"
checked={selectedPlatforms.length === mediaPlatforms.length}
onChange={toggleSelectAll}
className="accent-blue-600"
/>
Semua
</label>
{filteredPlatforms.map((platform) => (
<label
key={platform}
className="flex items-center gap-2 cursor-pointer"
>
<input
type="checkbox"
checked={selectedPlatforms.includes(platform)}
onChange={() => togglePlatform(platform)}
className="accent-blue-600"
/>
{platform}
</label>
))}
</div>
{/* Expand per platform */}
<div className="flex-1 overflow-y-auto border rounded-md p-2 space-y-1">
{filteredPlatforms.map((platform) => (
<div key={platform}>
<button
onClick={() => toggleExpand(platform)}
className="flex justify-between items-center w-full p-2 font-medium hover:bg-gray-50 rounded-md"
>
<span>Pilih {platform}</span>
{expanded[platform] ? (
<ChevronUp className="w-4 h-4" />
) : (
<ChevronDown className="w-4 h-4" />
)}
</button>
{expanded[platform] && (
<div className="pl-4 grid grid-cols-1 sm:grid-cols-2 gap-2 mt-1">
{mediaPerPlatform[platform].map((media) => (
<label
key={media}
className="flex items-center gap-2 p-2 rounded-md hover:bg-gray-50 cursor-pointer"
>
<input
type="checkbox"
checked={selectedMediaSosial.includes(media)}
onChange={() => toggleMedia(media)}
className="accent-blue-600"
/>
{media}
</label>
))}
</div>
)}
</div>
))}
</div>
{/* Tag media sosial dipilih */}
{selectedMediaSosial.length > 0 && (
<div className="mt-3">
<p className="text-sm font-medium mb-2">
{selectedMediaSosial.length} Media Sosial dipilih
</p>
<div className="flex flex-wrap gap-2 max-h-[100px] overflow-y-auto border rounded-md p-2 bg-gray-50">
{selectedMediaSosial.map((m) => (
<div
key={m}
className="flex items-center gap-2 px-3 py-1 bg-white rounded-full text-sm border"
>
{m}
<button
onClick={() => toggleMedia(m)}
className="text-gray-500 hover:text-gray-700"
>
</button>
</div>
))}
</div>
</div>
)}
{/* Tombol Footer */}
<DialogFooter className="mt-4">
<Button variant="outline" onClick={onClose}>
Batal
</Button>
<Button onClick={onClose}>Pilih</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@ -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<string[]>([
"Mabes",
"Polda",
"PTIK",
"LOGistik",
]);
const [selectedPolda, setSelectedPolda] = useState<string[]>([]);
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 (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="w-full max-w-3xl sm:max-h-[90vh] overflow-hidden flex flex-col">
<DialogHeader>
<DialogTitle>Pilih Videotron</DialogTitle>
</DialogHeader>
{/* SEARCH */}
<div className="relative mb-4">
<input
type="text"
placeholder="Cari Videotron..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-10 pr-4 py-2 border rounded-md focus:ring-2 focus:ring-blue-500"
/>
<svg
xmlns="http://www.w3.org/2000/svg"
className="w-4 h-4 absolute left-3 top-3 text-gray-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M21 21l-4.35-4.35m0 0A7.5 7.5 0 104.5 4.5a7.5 7.5 0 0012.15 12.15z"
/>
</svg>
</div>
{/* KATEGORI */}
<p className="text-sm font-medium mb-1">Pilih Media Sosial</p>
<div className="flex flex-wrap gap-3 mb-3">
<label className="flex items-center gap-2 font-medium cursor-pointer">
<input
type="checkbox"
checked={selectedCategories.length === mainCategories.length}
onChange={toggleSelectAllCategories}
className="accent-blue-600"
/>
Semua
</label>
{mainCategories.map((cat) => (
<label key={cat} className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={selectedCategories.includes(cat)}
onChange={() => toggleCategory(cat)}
className="accent-blue-600"
/>
{cat}
</label>
))}
</div>
{/* PILIH POLDA */}
<div className="border rounded-md p-2 overflow-y-auto flex-1">
<button
onClick={toggleExpand}
className="flex justify-between items-center w-full p-2 font-medium hover:bg-gray-50 rounded-md"
>
<span>Pilih Polda</span>
{expanded ? (
<ChevronUp className="w-4 h-4" />
) : (
<ChevronDown className="w-4 h-4" />
)}
</button>
{expanded && (
<div className="mt-2 grid grid-cols-1 sm:grid-cols-2 gap-2">
{poldaList.map((polda) => (
<label
key={polda}
className="flex items-center gap-2 p-2 rounded-md hover:bg-gray-50 cursor-pointer"
>
<input
type="checkbox"
checked={selectedPolda.includes(polda)}
onChange={() => togglePolda(polda)}
className="accent-blue-600"
/>
{polda}
</label>
))}
</div>
)}
</div>
{/* TAG TERPILIH */}
{selectedCategories.length + selectedPolda.length > 0 && (
<div className="mt-3">
<p className="text-sm font-medium mb-2">
{selectedCategories.length + selectedPolda.length} Videotron
dipilih
</p>
<div className="flex flex-wrap gap-2 max-h-[100px] overflow-y-auto border rounded-md p-2 bg-gray-50">
{selectedCategories.map((item) => (
<div
key={item}
className="flex items-center gap-2 px-3 py-1 bg-white rounded-full text-sm border"
>
{item}
<button
onClick={() => toggleCategory(item)}
className="text-gray-500 hover:text-gray-700"
>
</button>
</div>
))}
{selectedPolda.map((polda) => (
<div
key={polda}
className="flex items-center gap-2 px-3 py-1 bg-white rounded-full text-sm border"
>
{polda}
<button
onClick={() => togglePolda(polda)}
className="text-gray-500 hover:text-gray-700"
>
</button>
</div>
))}
</div>
</div>
)}
<DialogFooter className="mt-4">
<Button variant="outline" onClick={onClose}>
Batal
</Button>
<Button onClick={onClose}>Pilih</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@ -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 (
<div className="max-w-full mx-auto bg-white rounded-lg shadow-sm p-6 space-y-6">
{/* Header Tabs */}
<div className="flex gap-2 border-b pb-3">
<button
onClick={() => {
setActiveTab("detail");
setStep("configuration");
}}
className={cn(
"px-4 py-2 text-sm font-medium rounded-md transition",
activeTab === "detail"
? "bg-black text-white font-semibold"
: "text-gray-600 hover:text-black"
)}
>
Detail Konten
</button>
<button
onClick={() => setActiveTab("buatKonten")}
className={cn(
"px-4 py-2 text-sm font-medium rounded-md transition",
activeTab === "buatKonten"
? "bg-black text-white font-semibold"
: "text-gray-600 hover:text-black"
)}
>
Buat Konten
</button>
</div>
{/* DETAIL KONTEN */}
{activeTab === "detail" ? (
<>
<div>
<h2 className="text-sm text-gray-600 mb-1">Status</h2>
<span className="bg-gray-200 text-gray-700 px-3 py-1 rounded-full text-xs font-medium">
Tertunda
</span>
</div>
<div>
<h2 className="text-sm text-gray-600 mb-1">Judul Campaign</h2>
<p className="text-sm font-medium text-gray-800">
Lorem ipsum dolor sit amet consectetur. In faucibus diam eu ut
quisque.
</p>
</div>
<div>
<h2 className="text-sm text-gray-600 mb-1">Durasi</h2>
<p className="text-sm font-medium text-gray-800">
22/08/2025 - 22/08/2026
</p>
</div>
<div>
<h2 className="text-sm text-gray-600 mb-1">Media</h2>
<p className="font-semibold text-sm text-gray-900 mb-1">
Media Online
</p>
<ul className="text-sm text-gray-700 list-disc ml-4 space-y-1">
<li>Tribrata News Mabes</li>
<li>Tribrata News Polda Aceh</li>
<li>Tribrata News Polda Jawa Timur</li>
<li>Tribrata News Polda Jawa Tengah</li>
<li>Tribrata News Polda Jawa Barat</li>
</ul>
</div>
<div>
<h2 className="text-sm text-gray-600 mb-1">Tujuan</h2>
<p className="text-sm font-semibold text-gray-800">Sosialisasi</p>
</div>
<div>
<h2 className="text-sm text-gray-600 mb-1">Materi Promote</h2>
<div className="flex items-center justify-between border rounded-lg px-3 py-2 bg-gray-50">
<div>
<p className="text-sm font-medium text-gray-800">
Report name_T1.pdf
</p>
<p className="text-xs text-gray-500">23.5MB</p>
</div>
<Paperclip className="w-4 h-4 text-gray-500" />
</div>
</div>
<div className="pt-2 border-t">
<h2 className="text-sm text-gray-600 mb-2">Komentar</h2>
<div className="flex items-start gap-3">
<img
src="https://ui-avatars.com/api/?name=Kurator+POLRI&background=16a34a&color=fff"
alt="Kurator POLRI"
className="w-10 h-10 rounded-full"
/>
<div className="flex-1 border rounded-lg px-3 py-2 bg-gray-50">
<textarea
placeholder="Tuliskan komentar Anda di sini.."
className="w-full text-sm bg-transparent focus:outline-none resize-none"
rows={2}
/>
<div className="flex justify-between items-center mt-2 text-gray-500">
<div className="flex gap-3">
<Mic className="w-4 h-4 cursor-pointer hover:text-black" />
<Paperclip className="w-4 h-4 cursor-pointer hover:text-black" />
<Smile className="w-4 h-4 cursor-pointer hover:text-black" />
</div>
<Button
size="sm"
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-1 text-sm"
>
Kirim
</Button>
</div>
</div>
</div>
</div>
</>
) : (
// ==============================
// BUAT KONTEN
// ==============================
<div>
{/* STEP PROGRESS */}
<div className="relative flex items-center justify-between mb-6">
<div className="flex-1 h-0.5 bg-blue-500 absolute top-1/2 left-0 right-0 z-0" />
<div className="relative z-10 flex justify-between w-full">
<div className="flex flex-col items-center">
<div className="w-6 h-6 rounded-full bg-blue-600 flex items-center justify-center text-white">
<CheckCircle2 className="w-4 h-4" />
</div>
<span className="text-blue-600 text-sm mt-1 font-semibold">
Configuration
</span>
</div>
<div className="flex flex-col items-center">
<div
className={cn(
"w-6 h-6 rounded-full border-2 flex items-center justify-center",
step === "publish"
? "bg-blue-600 border-blue-600 text-white"
: "border-blue-300 text-gray-400"
)}
>
{step === "publish" ? (
<CheckCircle2 className="w-4 h-4" />
) : (
<div className="w-2 h-2 bg-blue-300 rounded-full" />
)}
</div>
<span
className={cn(
"text-sm mt-1 font-semibold",
step === "publish" ? "text-blue-600" : "text-gray-400"
)}
>
Publish
</span>
</div>
</div>
</div>
{/* CONFIGURATION FORM */}
{step === "configuration" && (
<div>
<h2 className="text-lg font-semibold mb-4">Configuration</h2>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium mb-1">
Select AI Journalist
</label>
<select className="border w-full rounded-md px-3 py-2 text-sm">
<option>Select AI Journalist</option>
<option>AI Journalist 1</option>
<option>AI Journalist 2</option>
</select>
</div>
<div>
<label className="block text-sm font-medium mb-1">
Main Keywords
</label>
<Input placeholder="Enter your main keyword here..." />
</div>
<div>
<label className="block text-sm font-medium mb-1">
Title
</label>
<Input placeholder="Enter your title here..." />
</div>
<div>
<label className="block text-sm font-medium mb-1">
SEO Keywords
</label>
<Input placeholder="Enter SEO keywords..." />
</div>
<div>
<label className="block text-sm font-medium mb-1">
Advanced Configuration
</label>
<textarea
className="w-full border rounded-md px-3 py-2 text-sm"
rows={3}
placeholder="Advanced settings..."
/>
</div>
<Button
className="bg-blue-600 hover:bg-blue-700 text-white"
onClick={() => setStep("publish")}
>
Generate Article
</Button>
</div>
</div>
)}
{/* PUBLISH STEP */}
{step === "publish" && (
<div className="mt-4">
<h2 className="text-lg font-semibold text-blue-600 mb-3">
Publish
</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{/* LEFT CONTENT */}
<div className="md:col-span-2 space-y-4">
<Input
placeholder="Judul"
defaultValue="Lorem ipsum dolor sit amet consectetur. In faucibus diam eu ut quisque."
/>
<div>
<label className="text-sm font-medium text-gray-700 mb-1 block">
Deskripsi
</label>
<textarea
rows={8}
className="w-full border rounded-md px-3 py-2 text-sm focus:outline-none"
defaultValue={`Kapolri Jenderal Polisi Drs. Listyo Sigit Prabowo, M.Si. mendampingi Presiden Joko Widodo menghadiri upacara Peringatan Hari Kesaktian Pancasila di Monumen Pancasila Sakti, Lubang Buaya, Jakarta Timur, pada Selasa (1/10/2024)...`}
/>
</div>
<div>
<label className="text-sm font-medium text-gray-700 mb-1 block">
File Media
</label>
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
{/* Dummy image sementara, nanti diganti dari API */}
{["/p1.jpg", "/p2.png"].map((url, i) => (
<div
key={i}
className="relative w-full h-24 rounded-md overflow-hidden border"
>
<img
src={url}
alt={`Media ${i + 1}`}
className="object-cover w-full h-full"
/>
</div>
))}
</div>
</div>
</div>
{/* RIGHT SIDEBAR */}
<div className="space-y-4">
<div>
<label className="text-sm font-medium text-gray-700 mb-1 block">
Kreator
</label>
<Input defaultValue="Humas POLRI" />
</div>
<div>
<label className="text-sm font-medium text-gray-700 mb-1 block">
Thumbnail
</label>
<div className="border border-dashed rounded-md py-6 flex flex-col items-center justify-center text-gray-400 text-sm">
<Upload className="w-5 h-5 mb-1" />
Drag and drop
<p className="text-xs text-gray-500 mt-1">
.PNG, .JPG max 3MB
</p>
</div>
</div>
<div>
<label className="text-sm font-medium text-gray-700 mb-1 block">
Tag
</label>
<Input
placeholder="Masukkan tag..."
defaultValue="Humas POLRI"
/>
<div className="flex flex-wrap gap-2 mt-2">
<span className="px-2 py-1 text-xs bg-blue-100 text-blue-700 rounded-md">
Poldakaltim
</span>
</div>
</div>
<div>
<label className="text-sm font-medium text-gray-700 mb-2 block">
Target Publish
</label>
<div className="space-y-2">
{[
"Tribrata News Mabes",
"Tribrata News Polda Aceh",
"Tribrata News Polda Jawa Timur",
"Tribrata News Polda Jawa Tengah",
"Tribrata News Polda Jawa Barat",
].map((item) => (
<label
key={item}
className="flex items-center text-sm gap-2"
>
<input
type="checkbox"
className="rounded border-gray-300"
/>
{item}
</label>
))}
</div>
</div>
<Button className="w-full bg-blue-600 hover:bg-blue-700 text-white">
Publish Terjadwal
</Button>
</div>
</div>
</div>
)}
</div>
)}
</div>
);
}

View File

@ -0,0 +1,476 @@
"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Label } from "@/components/ui/label";
import { Calendar } from "@/components/ui/calendar";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { CalendarIcon, Plus, Trash2 } from "lucide-react";
import { format } from "date-fns";
import { id } from "date-fns/locale";
import { Progress } from "../ui/progress";
import DialogMediaOnline from "../dialog/media-online";
import DialogMediaSosial from "../dialog/media-sosial";
export default function FormCampaign() {
const [startDate, setStartDate] = useState<Date | undefined>(undefined);
const [endDate, setEndDate] = useState<Date | undefined>(undefined);
const [goal, setGoal] = useState("Publikasi");
const [available, setAvailable] = useState("Yes");
const [isUploadOpen, setIsUploadOpen] = useState(false);
const [media, setMedia] = useState("Media Online");
const [isDialogOpen, setIsDialogOpen] = useState(false);
// contoh data pilihan media online (bisa diganti sesuai kebutuhan)
const mediaOnlineList = [
"Tribrata News Mabes",
"Tribrata News Polda Aceh",
"Tribrata News Polda Jawa Timur",
"Tribrata News Polda Jawa Tengah",
"Tribrata News Polda Jawa Barat",
];
const [selectedMediaOnline, setSelectedMediaOnline] = useState<string[]>([]);
const toggleMediaOnline = (item: string) => {
setSelectedMediaOnline((prev) =>
prev.includes(item) ? prev.filter((m) => m !== item) : [...prev, item]
);
};
const [files, setFiles] = useState<
{ file: File; progress: number; uploaded: boolean }[]
>([]);
const [url, setUrl] = useState("");
// ✅ Upload dari file input
const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const selectedFiles = e.target.files;
if (!selectedFiles) return;
const newFiles = Array.from(selectedFiles).map((f) => ({
file: f,
progress: 0,
uploaded: false,
}));
setFiles((prev) => [...prev, ...newFiles]);
simulateUpload(newFiles);
};
// ✅ Simulasi upload progress
const simulateUpload = (
fileList: { file: File; progress: number; uploaded: boolean }[]
) => {
fileList.forEach((fileObj) => {
let progress = 0;
const interval = setInterval(() => {
progress += 10;
setFiles((prev) =>
prev.map((f) =>
f.file === fileObj.file
? { ...f, progress, uploaded: progress >= 100 }
: f
)
);
if (progress >= 100) clearInterval(interval);
}, 300);
});
};
// ✅ Upload dari URL
const handleUploadFromUrl = () => {
if (!url.trim()) return;
const fakeFile = {
file: new File([], url.split("/").pop() || "file_from_url.jpg"),
progress: 100,
uploaded: true,
};
setFiles((prev) => [...prev, fakeFile]);
setUrl("");
};
// ✅ Hapus file
const removeFile = (index: number) => {
setFiles((prev) => prev.filter((_, i) => i !== index));
};
return (
<div className="bg-white rounded-2xl shadow-sm p-6 space-y-8">
{/* Langkah 1 */}
<section className="border-b pb-6">
<h2 className="font-semibold mb-4">Langkah 1</h2>
<p className="text-sm font-medium mb-2">Pilih Durasi</p>
<div className="flex flex-wrap gap-4">
<div>
<Label className="text-sm">Dari Tanggal</Label>
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
className="w-[200px] justify-start text-left font-normal"
>
<CalendarIcon className="mr-2 h-4 w-4" />
{startDate
? format(startDate, "dd MMMM yyyy", { locale: id })
: "Pilih tanggal"}
</Button>
</PopoverTrigger>
<PopoverContent className="p-0" align="start">
<Calendar
mode="single"
selected={startDate}
onSelect={setStartDate}
locale={id}
/>
</PopoverContent>
</Popover>
</div>
<div>
<Label className="text-sm">Sampai Tanggal</Label>
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
className="w-[200px] justify-start text-left font-normal"
>
<CalendarIcon className="mr-2 h-4 w-4" />
{endDate
? format(endDate, "dd MMMM yyyy", { locale: id })
: "Pilih tanggal"}
</Button>
</PopoverTrigger>
<PopoverContent className="p-0" align="start">
<Calendar
mode="single"
selected={endDate}
onSelect={setEndDate}
locale={id}
/>
</PopoverContent>
</Popover>
</div>
</div>
</section>
<section className="border-b pb-6">
<h2 className="font-semibold mb-4">Langkah 2</h2>
<p className="text-sm font-medium mb-2">Pilih Media</p>
<RadioGroup
value={media}
onValueChange={setMedia}
className="flex flex-wrap gap-4"
>
{[
"Media Online",
"Media Sosial",
"Videotron",
"Radio Polri",
"TV Polri",
"Majalah Digital",
].map((m) => (
<div key={m} className="flex items-center space-x-2">
<RadioGroupItem value={m} id={m} />
<Label htmlFor={m}>{m}</Label>
</div>
))}
</RadioGroup>
{/* Tombol muncul sesuai media terpilih */}
{media === "Media Online" && (
<Button
variant="outline"
size="sm"
className="mt-4"
onClick={() => setIsDialogOpen(true)}
>
<Plus className="h-4 w-4 mr-2" />
Tambahkan Media Online
</Button>
)}
{media === "Media Sosial" && (
<Button
variant="outline"
size="sm"
className="mt-4"
onClick={() => setIsDialogOpen(true)}
>
<Plus className="h-4 w-4 mr-2" />
Tambahkan Media Sosial
</Button>
)}
{media === "Videotron" && (
<Button
variant="outline"
size="sm"
className="mt-4"
onClick={() => setIsDialogOpen(true)}
>
<Plus className="h-4 w-4 mr-2" />
Tambahkan Videotron
</Button>
)}
{/* 🧩 Komponen DialogMediaOnline dipanggil di sini */}
{media === "Media Online" && (
<DialogMediaOnline
isOpen={isDialogOpen}
onClose={() => setIsDialogOpen(false)}
/>
)}
{media === "Media Sosial" && (
<DialogMediaSosial
isOpen={isDialogOpen}
onClose={() => setIsDialogOpen(false)}
/>
)}
{media === "Videotron" && (
<DialogMediaSosial
isOpen={isDialogOpen}
onClose={() => setIsDialogOpen(false)}
/>
)}
{media === "Radio Polri" && (
<div className="mt-4 space-y-3">
{[
"Pagi pukul 06:00 07:00",
"Siang pukul 12:00 13:00",
"Sore pukul 16:00 17:00",
"Malam pukul 20:00 21:00",
].map((time) => (
<div key={time} className="flex items-center space-x-2">
<input
type="checkbox"
id={time}
value={time}
className="w-4 h-4 border border-purple-600 text-purple-600 focus:ring-2 focus:ring-purple-500 rounded"
/>
<Label htmlFor={time} className="text-sm">
{time}
</Label>
</div>
))}
</div>
)}
{media === "TV Polri" && (
<div className="mt-4 space-y-3">
{[
"Pagi pukul 06:00 07:00",
"Siang pukul 12:00 13:00",
"Sore pukul 16:00 17:00",
"Malam pukul 20:00 21:00",
].map((time) => (
<div key={time} className="flex items-center space-x-2">
<input
type="checkbox"
id={time}
value={time}
className="w-4 h-4 border border-purple-600 text-purple-600 focus:ring-2 focus:ring-purple-500 rounded"
/>
<Label htmlFor={time} className="text-sm">
{time}
</Label>
</div>
))}
</div>
)}
</section>
{/* Langkah 3 */}
<section className="border-b pb-6">
<h2 className="font-semibold mb-4">Langkah 3</h2>
<p className="text-sm font-medium mb-2">Tujuan</p>
<RadioGroup value={goal} onValueChange={setGoal} className="flex gap-4">
{["Publikasi", "Sosialisasi"].map((g) => (
<div key={g} className="flex items-center space-x-2">
<RadioGroupItem value={g} id={g} />
<Label htmlFor={g}>{g}</Label>
</div>
))}
</RadioGroup>
</section>
{/* Langkah 4 */}
<section>
<h2 className="font-semibold mb-4">Langkah 4</h2>
<p className="text-sm font-medium mb-2">Materi Promote Tersedia</p>
<RadioGroup
value={available}
onValueChange={setAvailable}
className="flex gap-4 mb-4"
>
{["Yes", "Tidak"].map((a) => (
<div key={a} className="flex items-center space-x-2">
<RadioGroupItem value={a} id={a} />
<Label htmlFor={a}>{a}</Label>
</div>
))}
</RadioGroup>
{available === "Yes" ? (
// ✅ Jika user pilih "Yes" → tampil upload file
<div className="space-y-2">
<Label className="text-sm font-medium">Upload File</Label>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => setIsUploadOpen(true)}
>
<Plus className="h-4 w-4 mr-2" />
Upload File
</Button>
</div>
</div>
) : (
// ✅ Jika user pilih "Tidak" → tampil textarea deskripsi
<div className="space-y-2">
<Label className="text-sm font-medium">Deskripsi Promote</Label>
<textarea
placeholder="Tulis deskripsi promote..."
className="w-full min-h-[100px] p-3 border rounded-md text-sm resize-none focus:outline-none focus:ring-2 focus:ring-primary"
/>
</div>
)}
</section>
<div className="pt-6">
<Button className="w-[120px]" size="lg">
Submit
</Button>
</div>
{/* Modal Upload */}
<Dialog open={isUploadOpen} onOpenChange={setIsUploadOpen}>
<DialogContent className="max-w-lg w-[90vw] sm:w-full">
<DialogHeader>
<DialogTitle>Unggah Berkas</DialogTitle>
<p className="text-sm text-muted-foreground">
Pilih berkas dan unggah dengan aman untuk melanjutkan.
</p>
</DialogHeader>
{/* === Upload Section === */}
<div className="border-2 border-dashed rounded-lg p-6 flex flex-col items-center justify-center text-center space-y-2 w-full">
<p className="text-sm text-muted-foreground">
Seret dan jatuhkan berkas Anda
</p>
<p className="text-xs text-muted-foreground">
Format .PNG, .JPG, dan .JPEG hingga 50MB
</p>
<label htmlFor="fileInput">
<Button
variant="outline"
size="sm"
className="mt-2 cursor-pointer"
>
Pilih Berkas
</Button>
<Input
id="fileInput"
type="file"
multiple
className="hidden"
onChange={handleFileUpload}
/>
</label>
</div>
{/* === Upload via URL === */}
<div className="flex flex-col sm:flex-row items-center gap-2 mt-4 w-full">
<Input
type="url"
placeholder="Tambahkan URL berkas"
value={url}
onChange={(e) => setUrl(e.target.value)}
className="flex-1"
/>
<Button onClick={handleUploadFromUrl} className="w-full sm:w-auto">
Unggah
</Button>
</div>
{/* === Uploaded Files === */}
{files.length > 0 && (
<div className="mt-5 space-y-3 max-h-[60vh] overflow-y-auto">
<h4 className="text-sm font-semibold">Uploaded Files</h4>
<div className="space-y-2">
{files.map((f, i) => (
<div
key={i}
className="border rounded-lg p-3 flex flex-wrap sm:flex-nowrap justify-between items-start sm:items-center gap-3"
>
<div className="flex flex-col min-w-0 flex-1">
<p className="text-sm font-medium truncate max-w-[250px]">
{f.file.name}
</p>
<p className="text-xs text-muted-foreground">
{(f.file.size / (1024 * 1024)).toFixed(1)}MB {" "}
{f.uploaded ? (
<span className="text-green-600 font-medium">
Uploaded Successfully
</span>
) : (
<span className="text-blue-600 font-medium">
{f.progress}% Uploading...
</span>
)}
</p>
{!f.uploaded && (
<Progress
value={f.progress}
className="h-1 mt-1 w-full bg-gray-200"
/>
)}
</div>
<Button
variant="ghost"
size="icon"
onClick={() => removeFile(i)}
className="shrink-0"
>
<Trash2 className="h-4 w-4 text-muted-foreground" />
</Button>
</div>
))}
</div>
</div>
)}
<DialogFooter className="mt-4 flex flex-col sm:flex-row justify-end gap-2 sm:gap-4">
<Button
variant="outline"
onClick={() => setIsUploadOpen(false)}
className="w-full sm:w-auto"
>
Batal
</Button>
<Button
onClick={() => setIsUploadOpen(false)}
className="w-full sm:w-auto"
>
Lampirkan Berkas
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}

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

@ -0,0 +1,169 @@
"use client";
import { useState } from "react";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Button } from "@/components/ui/button";
import { Eye, EyeOff } from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/navigation";
import Footer from "../landing-page/footer";
import Navbar from "../landing-page/navbar";
// ✅ Dummy user data
const users = [
{
nrp: "1001",
password: "admin123",
role: "admin",
},
{
nrp: "1002",
password: "user123",
role: "user",
},
{
nrp: "1003",
password: "super123",
role: "supervisor",
},
{
nrp: "1004",
password: "approve123",
role: "approver",
},
];
export default function Login() {
const [showPassword, setShowPassword] = useState(false);
const [nrp, setNrp] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const router = useRouter();
const handleLogin = (e: React.FormEvent) => {
e.preventDefault();
const foundUser = users.find(
(u) => u.nrp === nrp && u.password === password
);
if (!foundUser) {
setError("NRP atau kata sandi salah!");
return;
}
// ✅ Simpan data user ke localStorage agar bisa diakses di halaman lain
localStorage.setItem("userRole", foundUser.role);
// ✅ Redirect ke halaman sesuai role
switch (foundUser.role) {
case "admin":
router.push("/dashboard/admin");
break;
case "user":
router.push("/dashboard/user");
break;
case "supervisor":
router.push("/dashboard/supervisor");
break;
case "approver":
router.push("/dashboard/approver");
break;
default:
router.push("/");
}
};
return (
<div className="min-h-screen flex flex-col justify-between bg-white">
<Navbar />
<div className="flex flex-1 items-center justify-center px-6 md:px-20 py-10">
<div className="grid md:grid-cols-2 gap-10 items-center w-full max-w-5xl">
{/* LEFT: FORM */}
<div className="w-full max-w-sm mx-auto">
<h1 className="text-lg font-semibold text-gray-800 mb-6">
Selamat Datang
</h1>
<form className="space-y-4" onSubmit={handleLogin}>
<div>
<Label htmlFor="nrp" className="text-gray-700">
NRP
</Label>
<Input
id="nrp"
value={nrp}
onChange={(e) => setNrp(e.target.value)}
placeholder="Masukkan NRP"
className="mt-1 bg-white border-gray-300 focus:border-yellow-600 focus:ring-yellow-600"
required
/>
</div>
<div className="relative">
<Label htmlFor="password" className="text-gray-700">
Kata Sandi
</Label>
<div className="relative mt-1">
<Input
id="password"
type={showPassword ? "text" : "password"}
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Masukkan kata sandi"
className="pr-10 bg-white border-gray-300 focus:border-yellow-600 focus:ring-yellow-600"
required
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-500 hover:text-gray-700"
>
{showPassword ? (
<EyeOff className="h-4 w-4" />
) : (
<Eye className="h-4 w-4" />
)}
</button>
</div>
</div>
{error && (
<p className="text-red-600 text-sm font-medium">{error}</p>
)}
<Button
type="submit"
className="w-full bg-yellow-700 hover:bg-yellow-800 text-white font-semibold"
>
Login
</Button>
</form>
<p className="text-center text-sm text-gray-600 mt-4">
Belum punya akun?{" "}
<Link href="/register" className="text-blue-600 hover:underline">
Register
</Link>
</p>
</div>
{/* RIGHT: ILLUSTRATION */}
<div className="hidden md:flex justify-center">
<Image
src="/login.png"
alt="Ilustrasi Login"
width={400}
height={400}
className="object-contain"
/>
</div>
</div>
</div>
<Footer />
</div>
);
}

View File

@ -0,0 +1,108 @@
"use client";
import { useState } from "react";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Button } from "@/components/ui/button";
import { Eye, EyeOff } from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/navigation"; // ✅ Tambahkan ini
import Footer from "../landing-page/footer";
import Navbar from "../landing-page/navbar";
export default function Registration() {
const [showPassword, setShowPassword] = useState(false);
const router = useRouter(); // ✅ gunakan router
const handleLogin = (e: React.FormEvent) => {
e.preventDefault();
// (contoh: kamu bisa tambahkan validasi login di sini)
// Jika berhasil login:
router.push("/admin"); // ✅ redirect ke halaman admin
};
return (
<div className="min-h-screen flex flex-col justify-between bg-white">
<Navbar />
<div className="flex flex-1 items-center justify-center px-6 md:px-20 py-10">
<div className="grid md:grid-cols-2 gap-10 items-center w-full max-w-5xl">
{/* LEFT: FORM */}
<div className="w-full max-w-sm mx-auto">
<h1 className="text-lg font-semibold text-gray-800 mb-6">
Selamat Datang
</h1>
<form className="space-y-4" onSubmit={handleLogin}>
<div>
<Label htmlFor="nrp" className="text-gray-700">
NRP
</Label>
<Input
id="nrp"
placeholder="Masukkan NRP"
className="mt-1 bg-white border-gray-300 focus:border-yellow-600 focus:ring-yellow-600"
required
/>
</div>
<div className="relative">
<Label htmlFor="password" className="text-gray-700">
Kata Sandi
</Label>
<div className="relative mt-1">
<Input
id="password"
type={showPassword ? "text" : "password"}
placeholder="Masukkan kata sandi"
className="pr-10 bg-white border-gray-300 focus:border-yellow-600 focus:ring-yellow-600"
required
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-500 hover:text-gray-700"
>
{showPassword ? (
<EyeOff className="h-4 w-4" />
) : (
<Eye className="h-4 w-4" />
)}
</button>
</div>
</div>
<Button
type="submit"
className="w-full bg-yellow-700 hover:bg-yellow-800 text-white font-semibold"
>
Login
</Button>
</form>
<p className="text-center text-sm text-gray-600 mt-4">
Sudah punya akun?{" "}
<Link href="/auth" className="text-blue-600 hover:underline">
Login
</Link>
</p>
</div>
{/* RIGHT: ILLUSTRATION */}
<div className="hidden md:flex justify-center">
<Image
src="/login.png"
alt="Ilustrasi Login"
width={400}
height={400}
className="object-contain"
/>
</div>
</div>
</div>
<Footer />
</div>
);
}

View File

@ -0,0 +1,86 @@
// components/landing-page/Footer.tsx
import {
Globe,
Instagram,
Facebook,
Twitter,
Youtube,
Music2,
} from "lucide-react";
import Image from "next/image";
export default function Footer() {
return (
<footer className="bg-[#f9f9f9] text-black py-6 border-t border-gray-200">
<div className="container mx-auto px-6 flex flex-col md:flex-row items-center justify-between gap-6">
{/* Kiri */}
<div className="flex flex-col md:flex-col items-start gap-6 text-sm text-gray-700">
<div className="flex items-center gap-2">
<Globe className="w-4 h-4" />
<span>Indonesia</span>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={2}
stroke="currentColor"
className="w-4 h-4"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M19 9l-7 7-7-7"
/>
</svg>
</div>
<p className="text-center md:text-left text-gray-600">
© 2025 Qudo Buana Nawakara. Semua Hak Dilindungi.
</p>
</div>
{/* Tengah - Sosial Media */}
{/* Kanan */}
<div className="flex flex-col md:flex-row items-center gap-4">
<div className="flex items-center justify-center gap-4 text-black">
<Instagram className="w-5 h-5 hover:text-gray-500 cursor-pointer" />
<Facebook className="w-5 h-5 hover:text-gray-500 cursor-pointer" />
<Twitter className="w-5 h-5 hover:text-gray-500 cursor-pointer" />
<Youtube className="w-5 h-5 hover:text-gray-500 cursor-pointer" />
<Music2 className="w-5 h-5 hover:text-gray-500 cursor-pointer" />{" "}
{/* untuk TikTok */}
</div>
<div className="flex items-center gap-4">
<div className="flex flex-col">
<Image
src="/kan.png"
alt="KAN Logo"
width={280}
height={88}
className="object-contain"
/>
</div>
</div>
<div className="flex flex-col gap-2">
<Image
src="/appstore.png"
alt="App Store"
width={130}
height={40}
className="object-contain cursor-pointer"
/>
<Image
src="/google.png"
alt="Google Play"
width={130}
height={40}
className="object-contain cursor-pointer"
/>
</div>
</div>
</div>
</footer>
);
}

View File

@ -0,0 +1,119 @@
import Image from "next/image";
export default function Header() {
return (
<section className=" text-white">
{/* Bagian Atas */}
<div className="container bg-[#B87C2C] mx-auto flex flex-col md:flex-row items-center justify-between px-6 py-10 md:py-16">
<div className="md:w-1/2 space-y-4">
<h1 className="text-2xl md:text-3xl font-bold leading-snug">
Capai Audiens yang Lebih Luas dengan melakukan <br />
<span className="text-white">Promote di CampaignPOOL</span>
</h1>
<p className="text-base md:text-lg opacity-90">
Buat jangkauan lebih luas dan lebih dikenal.
</p>
</div>
<div className="md:w-1/2 mt-8 md:mt-0 flex justify-center">
<Image
src="/h1.png"
alt="Promote Illustration"
width={400}
height={300}
className="object-contain"
/>
</div>
</div>
{/* Bagian Langkah-langkah */}
<div className="container mx-auto text-center py-10 px-4">
<h2 className="text-2xl font-semibold text-black">
Cara Promote di CampaignPOOL
</h2>
<p className="text-gray-600 mt-2">Langkah mudah untuk memasang iklan</p>
{/* Langkah-langkah */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mt-10 place-items-center">
{/* Langkah 1 */}
<div className="p-6 w-full max-w-sm">
<div className="bg-gray-200 rounded-lg h-56 flex items-center justify-center">
<Image
src="/l1.png"
alt="Langkah 1"
width={150}
height={100}
className="object-contain"
/>
</div>
<h3 className="mt-4 font-semibold text-black text-center">
Langkah 1
</h3>
<p className="text-sm text-gray-600 mt-2 text-center">
Pilih tanggal tayang, tentukan target promote, dan unggah materi
iklan
</p>
</div>
{/* Langkah 2 */}
<div className="p-6 w-full max-w-sm">
<div className="bg-gray-200 rounded-lg h-56 flex items-center justify-center">
<Image
src="/l2.png"
alt="Langkah 2"
width={150}
height={100}
className="object-contain"
/>
</div>
<h3 className="mt-4 font-semibold text-black text-center">
Langkah 2
</h3>
<p className="text-sm text-gray-600 mt-2 text-center">
Pemrosesan Internal dan Persetujuan
</p>
</div>
{/* Langkah 3 */}
<div className="p-6 w-full max-w-sm">
<div className="bg-gray-200 rounded-lg h-56 flex items-center justify-center">
<Image
src="/l3.png"
alt="Langkah 3"
width={150}
height={100}
className="object-contain"
/>
</div>
<h3 className="mt-4 font-semibold text-black text-center">
Langkah 3
</h3>
<p className="text-sm text-gray-600 mt-2 text-center">
Selamat! Promote Anda tayang
</p>
</div>
{/* Langkah 4 (tengah bawah) */}
<div className="md:col-span-3 flex justify-center">
<div className="w-full max-w-sm">
<div className="bg-gray-200 rounded-lg h-56 flex items-center justify-center">
<Image
src="/l4.png"
alt="Langkah 4"
width={150}
height={100}
className="object-contain"
/>
</div>
<h3 className="mt-4 font-semibold text-black text-center">
Langkah 4
</h3>
<p className="text-sm text-gray-600 mt-2 text-center">
Pantau perkembangan Promote Anda
</p>
</div>
</div>
</div>
</div>
</section>
);
}

View File

@ -0,0 +1,64 @@
"use client";
import { Search } from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import { useEffect, useState } from "react";
import { Button } from "../ui/button";
import { usePathname } from "next/navigation";
export default function Navbar() {
const [date, setDate] = useState("");
const pathname = usePathname();
// cek apakah sedang di halaman auth
const isAuthPage = pathname.startsWith("/auth");
useEffect(() => {
const today = new Date();
const options: Intl.DateTimeFormatOptions = {
day: "numeric",
month: "long",
year: "numeric",
};
const formattedDate = today.toLocaleDateString("id-ID", options);
setDate(formattedDate);
}, []);
return (
<div className="w-full bg-white pt-5 mt-0 border-b-2 mb-2">
<div className="flex items-center max-w-screen-full mx-3 md:mx-auto mb-3 p-3">
<Link href={"/"}>
<div className="flex items-center mr-6 mx-1">
<Image
src="/campaign.png"
alt="Kritik Tajam Logo"
width={60}
height={70}
/>
</div>
</Link>
{/* Spacer to push search icon to the right */}
<div className="flex-grow" />
{/* Search Icon */}
{!isAuthPage && (
<div className="text-xl cursor-pointer mr-1 gap-3 flex">
<Link href={"/auth/registration"}>
<Button variant={"ghost"} className="text-[#A3712A]">
Register
</Button>
</Link>
<Link href={"/auth"}>
<Button
variant={"outline"}
className="border rounded-xl border-[#A3712A] text-[#A3712A]"
>
Login
</Button>
</Link>
</div>
)}
</div>
</div>
);
}

View File

@ -0,0 +1,127 @@
"use client";
import { useState } from "react";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import DialogUserDetail from "../dialog/admin-detail";
export default function AdminTable() {
const [selectedUser, setSelectedUser] = useState<any>(null);
const [isDialogOpen, setIsDialogOpen] = useState(false);
const data = [
{
createdAt: "14 Januari 2025 13:00",
fullName: "Novan Farhandi",
email: "novanfarhandi@example.com",
status: "Approved",
},
{
createdAt: "14 Januari 2025 13:00",
fullName: "Salma Husna",
email: "salmahusna@example.com",
status: "Tertunda",
},
];
const openDialog = (user: any) => {
setSelectedUser(user);
setIsDialogOpen(true);
};
const closeDialog = () => {
setIsDialogOpen(false);
setSelectedUser(null);
};
return (
<div className="bg-white shadow rounded-lg p-4">
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow className="bg-gray-100">
<TableHead className="text-gray-600">Nama Lengkap</TableHead>
<TableHead className="text-gray-600">Email</TableHead>
<TableHead className="text-gray-600">Tanggal Daftar</TableHead>
<TableHead className="text-gray-600">Status</TableHead>
<TableHead className="text-gray-600">Tindakan</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data.map((row, i) => (
<TableRow key={i} className="hover:bg-gray-50 transition-colors">
<TableCell>{row.fullName}</TableCell>
<TableCell className="font-medium text-gray-800">
{row.email}
</TableCell>
<TableCell className="text-gray-700">{row.createdAt}</TableCell>
<TableCell>
<span
className={`px-3 py-1 rounded-full text-xs font-medium ${
row.status === "Approved"
? "bg-green-100 text-green-700"
: "bg-gray-200 text-gray-700"
}`}
>
{row.status}
</span>
</TableCell>
<TableCell>
<div className="flex flex-wrap gap-2">
<button
className="text-blue-600 hover:underline"
onClick={() => openDialog(row)}
>
Lihat
</button>
<button className="text-green-600 hover:underline">
Approve
</button>
<button className="text-red-500 hover:underline">
Hapus
</button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
{/* Pagination */}
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-center mt-4 text-sm text-gray-500 gap-2">
<div className="flex items-center">
<span>Rows per page:</span>
<select className="ml-2 border rounded px-1 py-0.5">
<option>6</option>
<option>12</option>
</select>
</div>
<div className="flex items-center justify-between sm:justify-end gap-2">
<span>11 of 1</span>
<div className="flex gap-1">
<button className="text-gray-400 hover:text-gray-600"></button>
<button className="text-gray-400 hover:text-gray-600"></button>
</div>
</div>
</div>
{/* ✅ Dialog terpisah */}
<DialogUserDetail
isOpen={isDialogOpen}
onClose={closeDialog}
user={selectedUser}
/>
</div>
);
}

View File

@ -0,0 +1,197 @@
"use client";
import { useState, useMemo } from "react";
import { Eye } from "lucide-react";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { cn } from "@/lib/utils";
import Link from "next/link";
export default function ApproverTable() {
const [activeCategory, setActiveCategory] = useState("Semua");
const categories = [
"Semua",
"Online Media",
"Social Media",
"Videotron",
"Radio Polri",
"TV Polri",
"Majalah Digital",
];
const data = [
{
id: 1,
no: 1,
media: "Media Online",
title:
"Lorem ipsum dolor sit amet consectetur. Tempor mi scelerisque enim semper sed nibh. Eget sit molestie.",
status: "Tertunda",
},
{
id: 2,
no: 2,
media: "Media Sosial",
title:
"Lorem ipsum dolor sit amet consectetur. Ultricies pellentesque ullamcorper mattis pellentesque. Amet eu ut.",
status: "Tertunda",
},
{
id: 3,
no: 3,
media: "Videotron",
title:
"Lorem ipsum dolor sit amet consectetur. Nisl orci magna ridiculus egestas. Eu sit ut vitae interdum. Aenean.",
status: "Tertunda",
},
{
id: 4,
no: 4,
media: "Radio Polri",
title:
"Lorem ipsum dolor sit amet consectetur. Tincidunt sem tellus interdum pharetra pharetra arcu. Sapien varius.",
status: "Tertunda",
},
{
id: 5,
no: 5,
media: "TV Polri",
title:
"Lorem ipsum dolor sit amet consectetur. Ac purus diam eget nulla turpis elementum. Est vestibulum nibh.",
status: "Selesai",
},
{
id: 6,
no: 6,
media: "Majalah Digital",
title:
"Lorem ipsum dolor sit amet consectetur. Eget odio magna id velit lacus tellus. Fermentum molestie viverra.",
status: "Selesai",
},
];
// ✅ Filter data berdasarkan kategori aktif
const filteredData = useMemo(() => {
if (activeCategory === "Semua") return data;
return data.filter(
(item) => item.media.toLowerCase() === activeCategory.toLowerCase()
);
}, [activeCategory]);
return (
<div className="p-4 sm:p-6 space-y-4">
{/* ✅ Tabs Navigation */}
<Tabs
defaultValue={activeCategory}
onValueChange={setActiveCategory}
className="max-w-4xl"
>
<TabsList className="w-full justify-start bg-black rounded-lg overflow-x-auto flex-nowrap p-1">
{categories.map((cat) => (
<TabsTrigger
key={cat}
value={cat}
className={cn(
"data-[state=active]:bg-white data-[state=active]:text-black data-[state=active]:font-semibold text-gray-300 rounded-lg text-sm sm:text-base transition-all px-4 py-2 whitespace-nowrap"
)}
>
{cat}
</TabsTrigger>
))}
</TabsList>
</Tabs>
{/* ✅ Table from shadcn/ui */}
<div className="bg-white border rounded-xl shadow-sm overflow-hidden">
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow className="bg-[#f9f9f9] border-b">
<TableHead className="w-10 sm:w-12 text-left">No</TableHead>
<TableHead className="w-28 sm:w-40 text-left">Media</TableHead>
<TableHead className="min-w-[200px] text-left">
Judul Campaign
</TableHead>
<TableHead className="w-28 sm:w-32 text-left">Status</TableHead>
<TableHead className="w-20 sm:w-24 text-left">
Tindakan
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredData.length > 0 ? (
filteredData.map((item, index) => (
<TableRow
key={index}
className="hover:bg-gray-50 border-b last:border-none"
>
{/* ✅ Nomor selalu mulai dari 1 sesuai hasil filter */}
<TableCell className="p-3">{index + 1}</TableCell>
<TableCell className="p-3 font-semibold">
{item.media}
</TableCell>
<TableCell className="p-3">{item.title}</TableCell>
<TableCell className="p-3">
<span
className={cn(
"px-2 sm:px-3 py-1 rounded-full text-[10px] sm:text-xs font-medium whitespace-nowrap",
item.status === "Tertunda"
? "bg-gray-200 text-gray-600"
: "bg-green-100 text-green-800"
)}
>
{item.status}
</span>
</TableCell>
<TableCell>
<Link href={`/dashboard/approver/detail/${item.id}`}>
<button className="text-blue-600 flex items-center gap-1 text-xs sm:text-sm hover:underline">
<Eye className="w-3 h-3 sm:w-4 sm:h-4" />
Lihat
</button>
</Link>
</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={5}
className="text-center py-4 text-gray-500"
>
Tidak ada data untuk kategori ini
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
{/* ✅ Pagination */}
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center px-4 py-3 text-xs sm:text-sm text-gray-500 gap-2 sm:gap-0">
<span>Rows per page: {filteredData.length}</span>
<div className="flex items-center justify-between w-full sm:w-auto">
<span>
{filteredData.length > 0
? `1${filteredData.length} of ${filteredData.length}`
: "0 of 0"}
</span>
<div className="flex gap-2 ml-4">
<button className="text-gray-400 cursor-not-allowed"></button>
<button className="text-gray-400 cursor-not-allowed"></button>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,198 @@
"use client";
import { useState } from "react";
import dynamic from "next/dynamic";
import { ApexOptions } from "apexcharts";
import { Card, CardContent } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
const Chart = dynamic(() => import("react-apexcharts"), { ssr: false });
export default function SupervisorData() {
const [startDate, setStartDate] = useState("2025-10-09");
const [endDate, setEndDate] = useState("2025-10-09");
// ✅ Chart config with proper ApexOptions typing
const chartOptions: ApexOptions = {
chart: {
type: "bar",
toolbar: { show: false },
},
plotOptions: {
bar: {
borderRadius: 4,
horizontal: false,
columnWidth: "45%",
},
},
dataLabels: { enabled: true },
xaxis: {
categories: [
"ITWASUM POLRI",
"BAINTELKAM POLRI",
"BAHARKAM POLRI",
"BARESKRIM POLRI",
"LEMDIKLAT POLRI",
"KORBRIMOB POLRI",
"STAMAOPS POLRI",
],
labels: {
rotate: -45,
style: { fontSize: "11px" },
},
},
yaxis: {
title: { text: "Total Konten" },
},
grid: { strokeDashArray: 4 },
responsive: [
{
breakpoint: 768,
options: {
plotOptions: {
bar: {
columnWidth: "60%",
},
},
xaxis: {
labels: { rotate: -30, style: { fontSize: "9px" } },
},
dataLabels: { enabled: false },
},
},
],
};
const chartDataSatker = [
{
name: "Total Konten",
data: [9, 8, 7, 6, 5, 4, 3],
},
];
const chartDataMedia = [
{
name: "Total Konten",
data: [9, 8, 7, 6, 5, 4],
},
];
const chartOptionsMedia: ApexOptions = {
...chartOptions,
xaxis: {
categories: [
"Media Online",
"Media Sosial",
"Videotron",
"Radio Polri",
"TV Polri",
"Majalah Digital",
],
labels: {
rotate: -45,
style: { fontSize: "11px" },
},
},
};
return (
<div className="space-y-6">
{/* === Satker Terbanyak Membuat Konten === */}
<Card className="shadow-sm">
<CardContent className="p-4 sm:p-6">
<h3 className="text-base sm:text-lg font-semibold mb-4">
Satker Terbanyak Membuat Konten
</h3>
{/* ✅ Responsive date inputs */}
<div className="flex flex-col sm:flex-row gap-4 mb-4">
<div className="flex flex-col w-full sm:w-auto">
<Label htmlFor="startDate" className="text-sm font-medium mb-1">
Start Date
</Label>
<Input
id="startDate"
type="date"
value={startDate}
onChange={(e) => setStartDate(e.target.value)}
className="w-full sm:w-[180px]"
/>
</div>
<div className="flex flex-col w-full sm:w-auto">
<Label htmlFor="endDate" className="text-sm font-medium mb-1">
End Date
</Label>
<Input
id="endDate"
type="date"
value={endDate}
onChange={(e) => setEndDate(e.target.value)}
className="w-full sm:w-[180px]"
/>
</div>
</div>
{/* ✅ Chart container with responsive width */}
<div className="w-full overflow-x-auto">
<div className="min-w-[350px]">
<Chart
options={chartOptions}
series={chartDataSatker}
type="bar"
height={250}
/>
</div>
</div>
</CardContent>
</Card>
{/* === Jenis Media Terbanyak === */}
<Card className="shadow-sm">
<CardContent className="p-4 sm:p-6">
<h3 className="text-base sm:text-lg font-semibold mb-4">
Jenis Media Terbanyak
</h3>
<div className="flex flex-col sm:flex-row gap-4 mb-4">
<div className="flex flex-col w-full sm:w-auto">
<Label htmlFor="startDate2" className="text-sm font-medium mb-1">
Start Date
</Label>
<Input
id="startDate2"
type="date"
value={startDate}
onChange={(e) => setStartDate(e.target.value)}
className="w-full sm:w-[180px]"
/>
</div>
<div className="flex flex-col w-full sm:w-auto">
<Label htmlFor="endDate2" className="text-sm font-medium mb-1">
End Date
</Label>
<Input
id="endDate2"
type="date"
value={endDate}
onChange={(e) => setEndDate(e.target.value)}
className="w-full sm:w-[180px]"
/>
</div>
</div>
<div className="w-full overflow-x-auto">
<div className="min-w-[350px]">
<Chart
options={chartOptionsMedia}
series={chartDataMedia}
type="bar"
height={250}
/>
</div>
</div>
</CardContent>
</Card>
</div>
);
}

View File

@ -0,0 +1,131 @@
"use client";
import Link from "next/link";
import { useState } from "react";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Button } from "@/components/ui/button";
import { Eye } from "lucide-react";
import DialogCampaignDetail from "../dialog/campaign-detail";
export default function UserTable() {
const data = [
{
durasi: "22/08/2025 - 22/08/2026",
media: "Media Online",
tujuan: "Sosialisasi",
materi: "Tersedia",
deskripsi:
"Lorem ipsum dolor sit amet consectetur. Tempor mi scelerisque enim semper sed nibh.",
status: "Selesai",
},
{
durasi: "22/08/2025 - 22/08/2026",
media: "Media Sosial",
tujuan: "Sosialisasi",
materi: "Tersedia",
deskripsi:
"Ultricies pellentesque ullamcorper mattis pellentesque. Amet eu ut.",
status: "Selesai",
},
];
const [selectedRow, setSelectedRow] = useState<any>(null);
const [isDialogOpen, setIsDialogOpen] = useState(false);
const openDetail = (row: any) => {
setSelectedRow(row);
setIsDialogOpen(true);
};
return (
<>
<div className="flex justify-between items-center mb-5 flex-wrap gap-3">
<h2 className="text-lg font-semibold text-gray-800">Daftar Campaign</h2>
<Link href={"/dashboard/user/create"}>
<Button className="bg-blue-600 hover:bg-blue-700 text-white">
Add New
</Button>
</Link>
</div>
<div className="bg-white shadow rounded-lg p-4">
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead>Durasi</TableHead>
<TableHead>Media</TableHead>
<TableHead>Tujuan</TableHead>
<TableHead>Materi</TableHead>
<TableHead>Deskripsi Promote</TableHead>
<TableHead>Status</TableHead>
<TableHead>Tindakan</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data.map((row, i) => (
<TableRow key={i}>
<TableCell>{row.durasi}</TableCell>
<TableCell className="font-medium">{row.media}</TableCell>
<TableCell>{row.tujuan}</TableCell>
<TableCell>{row.materi}</TableCell>
<TableCell className="min-w-[200px] text-gray-600">
{row.deskripsi}
</TableCell>
<TableCell>
<span className="bg-green-100 text-green-700 px-3 py-1 rounded-full text-xs font-medium">
{row.status}
</span>
</TableCell>
<TableCell>
<Button
variant="link"
className="text-blue-600 p-0 h-auto font-medium flex items-center gap-1"
onClick={() => openDetail(row)}
>
<Eye className="h-4 w-4" />
Lihat
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
{/* Pagination */}
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-center mt-4 text-sm text-gray-500 gap-2">
<div className="flex items-center">
<span>Rows per page:</span>
<select className="ml-2 border rounded px-1 py-0.5">
<option>6</option>
<option>12</option>
</select>
</div>
<div className="flex items-center justify-between sm:justify-end gap-2">
<span>11 of 1</span>
<div className="flex gap-1">
<button className="text-gray-400 hover:text-gray-600"></button>
<button className="text-gray-400 hover:text-gray-600"></button>
</div>
</div>
</div>
</div>
<DialogCampaignDetail
isOpen={isDialogOpen}
onClose={() => setIsDialogOpen(false)}
data={selectedRow}
/>
</>
);
}

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

@ -0,0 +1,60 @@
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 hover:bg-primary/90",
destructive:
"bg-destructive text-white 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 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",
"icon-sm": "size-8",
"icon-lg": "size-10",
},
},
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 }

216
components/ui/calendar.tsx Normal file
View File

@ -0,0 +1,216 @@
"use client"
import * as React from "react"
import {
ChevronDownIcon,
ChevronLeftIcon,
ChevronRightIcon,
} from "lucide-react"
import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker"
import { cn } from "@/lib/utils"
import { Button, buttonVariants } from "@/components/ui/button"
function Calendar({
className,
classNames,
showOutsideDays = true,
captionLayout = "label",
buttonVariant = "ghost",
formatters,
components,
...props
}: React.ComponentProps<typeof DayPicker> & {
buttonVariant?: React.ComponentProps<typeof Button>["variant"]
}) {
const defaultClassNames = getDefaultClassNames()
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn(
"bg-background group/calendar p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
className
)}
captionLayout={captionLayout}
formatters={{
formatMonthDropdown: (date) =>
date.toLocaleString("default", { month: "short" }),
...formatters,
}}
classNames={{
root: cn("w-fit", defaultClassNames.root),
months: cn(
"flex gap-4 flex-col md:flex-row relative",
defaultClassNames.months
),
month: cn("flex flex-col w-full gap-4", defaultClassNames.month),
nav: cn(
"flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between",
defaultClassNames.nav
),
button_previous: cn(
buttonVariants({ variant: buttonVariant }),
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
defaultClassNames.button_previous
),
button_next: cn(
buttonVariants({ variant: buttonVariant }),
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
defaultClassNames.button_next
),
month_caption: cn(
"flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)",
defaultClassNames.month_caption
),
dropdowns: cn(
"w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5",
defaultClassNames.dropdowns
),
dropdown_root: cn(
"relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md",
defaultClassNames.dropdown_root
),
dropdown: cn(
"absolute bg-popover inset-0 opacity-0",
defaultClassNames.dropdown
),
caption_label: cn(
"select-none font-medium",
captionLayout === "label"
? "text-sm"
: "rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5",
defaultClassNames.caption_label
),
table: "w-full border-collapse",
weekdays: cn("flex", defaultClassNames.weekdays),
weekday: cn(
"text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none",
defaultClassNames.weekday
),
week: cn("flex w-full mt-2", defaultClassNames.week),
week_number_header: cn(
"select-none w-(--cell-size)",
defaultClassNames.week_number_header
),
week_number: cn(
"text-[0.8rem] select-none text-muted-foreground",
defaultClassNames.week_number
),
day: cn(
"relative w-full h-full p-0 text-center [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none",
props.showWeekNumber
? "[&:nth-child(2)[data-selected=true]_button]:rounded-l-md"
: "[&:first-child[data-selected=true]_button]:rounded-l-md",
defaultClassNames.day
),
range_start: cn(
"rounded-l-md bg-accent",
defaultClassNames.range_start
),
range_middle: cn("rounded-none", defaultClassNames.range_middle),
range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end),
today: cn(
"bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
defaultClassNames.today
),
outside: cn(
"text-muted-foreground aria-selected:text-muted-foreground",
defaultClassNames.outside
),
disabled: cn(
"text-muted-foreground opacity-50",
defaultClassNames.disabled
),
hidden: cn("invisible", defaultClassNames.hidden),
...classNames,
}}
components={{
Root: ({ className, rootRef, ...props }) => {
return (
<div
data-slot="calendar"
ref={rootRef}
className={cn(className)}
{...props}
/>
)
},
Chevron: ({ className, orientation, ...props }) => {
if (orientation === "left") {
return (
<ChevronLeftIcon className={cn("size-4", className)} {...props} />
)
}
if (orientation === "right") {
return (
<ChevronRightIcon
className={cn("size-4", className)}
{...props}
/>
)
}
return (
<ChevronDownIcon className={cn("size-4", className)} {...props} />
)
},
DayButton: CalendarDayButton,
WeekNumber: ({ children, ...props }) => {
return (
<td {...props}>
<div className="flex size-(--cell-size) items-center justify-center text-center">
{children}
</div>
</td>
)
},
...components,
}}
{...props}
/>
)
}
function CalendarDayButton({
className,
day,
modifiers,
...props
}: React.ComponentProps<typeof DayButton>) {
const defaultClassNames = getDefaultClassNames()
const ref = React.useRef<HTMLButtonElement>(null)
React.useEffect(() => {
if (modifiers.focused) ref.current?.focus()
}, [modifiers.focused])
return (
<Button
ref={ref}
variant="ghost"
size="icon"
data-day={day.date.toLocaleDateString()}
data-selected-single={
modifiers.selected &&
!modifiers.range_start &&
!modifiers.range_end &&
!modifiers.range_middle
}
data-range-start={modifiers.range_start}
data-range-end={modifiers.range_end}
data-range-middle={modifiers.range_middle}
className={cn(
"data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 dark:hover:text-accent-foreground flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md [&>span]:text-xs [&>span]:opacity-70",
defaultClassNames.day,
className
)}
{...props}
/>
)
}
export { Calendar, CalendarDayButton }

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

@ -0,0 +1,92 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card"
className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
className
)}
{...props}
/>
)
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 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,
}

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

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

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

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

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

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

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

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

View File

@ -0,0 +1,31 @@
"use client"
import * as React from "react"
import * as ProgressPrimitive from "@radix-ui/react-progress"
import { cn } from "@/lib/utils"
function Progress({
className,
value,
...props
}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
return (
<ProgressPrimitive.Root
data-slot="progress"
className={cn(
"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
className
)}
{...props}
>
<ProgressPrimitive.Indicator
data-slot="progress-indicator"
className="bg-primary h-full w-full flex-1 transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
)
}
export { Progress }

View File

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

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

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

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

@ -0,0 +1,66 @@
"use client"
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "@/lib/utils"
function Tabs({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
return (
<TabsPrimitive.Root
data-slot="tabs"
className={cn("flex flex-col gap-2", className)}
{...props}
/>
)
}
function TabsList({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.List>) {
return (
<TabsPrimitive.List
data-slot="tabs-list"
className={cn(
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
className
)}
{...props}
/>
)
}
function TabsTrigger({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
return (
<TabsPrimitive.Trigger
data-slot="tabs-trigger"
className={cn(
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground 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-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
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 }

6
lib/utils.ts Normal file
View File

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

7
next.config.ts Normal file
View File

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

2641
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

38
package.json Normal file
View File

@ -0,0 +1,38 @@
{
"name": "campaign-pool",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
},
"dependencies": {
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-progress": "^1.1.8",
"@radix-ui/react-radio-group": "^1.3.8",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-tabs": "^1.1.13",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"lucide-react": "^0.552.0",
"next": "16.0.1",
"react": "19.2.0",
"react-apexcharts": "^1.8.0",
"react-day-picker": "^9.11.1",
"react-dom": "19.2.0",
"tailwind-merge": "^3.3.1"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"tailwindcss": "^4",
"tw-animate-css": "^1.4.0",
"typescript": "^5"
}
}

7
postcss.config.mjs Normal file
View File

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

BIN
public/appstore.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

BIN
public/campaign.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

1
public/file.svg Normal file
View File

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

After

Width:  |  Height:  |  Size: 391 B

1
public/globe.svg Normal file
View File

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

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
public/google.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

BIN
public/h1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

BIN
public/kan.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 KiB

BIN
public/l1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
public/l2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
public/l3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
public/l4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
public/login.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

1
public/next.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
public/non-user.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 354 KiB

BIN
public/p1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 MiB

BIN
public/p2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 MiB

1
public/vercel.svg Normal file
View File

@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 128 B

1
public/window.svg Normal file
View File

@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

After

Width:  |  Height:  |  Size: 385 B

34
tsconfig.json Normal file
View File

@ -0,0 +1,34 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts",
"**/*.mts"
],
"exclude": ["node_modules"]
}