Files
biiproject-kit-v2/resources/js/Pages/Docs/Index.tsx
T

611 lines
44 KiB
TypeScript

import React, { useState, useEffect, useRef } from 'react';
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import { Head } from '@inertiajs/react';
const NAV = [
{ id: 'intro', label: 'Pendahuluan', icon: <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}><path strokeLinecap="round" strokeLinejoin="round" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg> },
{ id: 'quickstart', label: 'Quick Start', icon: <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}><path strokeLinecap="round" strokeLinejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z" /></svg> },
{ id: 'stack', label: 'Tech Stack', icon: <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}><path strokeLinecap="round" strokeLinejoin="round" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" /></svg> },
{ id: 'auth', label: 'Autentikasi', icon: <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}><path strokeLinecap="round" strokeLinejoin="round" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" /></svg> },
{ id: 'roles', label: 'Roles & Permission', icon: <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}><path strokeLinecap="round" strokeLinejoin="round" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" /></svg> },
{ id: 'features', label: 'Fitur Lengkap', icon: <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}><path strokeLinecap="round" strokeLinejoin="round" d="M4 6h16M4 10h16M4 14h16M4 18h7" /></svg> },
{ id: 'api', label: 'REST API', icon: <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}><path strokeLinecap="round" strokeLinejoin="round" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg> },
{ id: '2fa', label: 'Two-Factor Auth', icon: <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}><path strokeLinecap="round" strokeLinejoin="round" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" /></svg> },
{ id: 'settings', label: 'System Settings', icon: <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}><path strokeLinecap="round" strokeLinejoin="round" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" /><path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /></svg> },
{ id: 'structure', label: 'Struktur Folder', icon: <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}><path strokeLinecap="round" strokeLinejoin="round" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" /></svg> },
];
function Badge({ children, color = 'gray' }: { children: React.ReactNode; color?: string }) {
const colors: Record<string, string> = {
green: 'bg-emerald-50 text-emerald-700 border border-emerald-200',
blue: 'bg-blue-50 text-blue-700 border border-blue-200',
amber: 'bg-amber-50 text-amber-700 border border-amber-200',
red: 'bg-red-50 text-red-700 border border-red-200',
gray: 'bg-gray-50 text-gray-600 border border-gray-200',
purple: 'bg-purple-50 text-purple-700 border border-purple-200',
};
return (
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-lg text-[10px] font-bold uppercase tracking-widest ${colors[color]}`}>
{children}
</span>
);
}
function CodeBlock({ children, lang = 'bash' }: { children: string; lang?: string }) {
const [copied, setCopied] = useState(false);
const copy = () => {
navigator.clipboard.writeText(children);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
return (
<div className="relative group mt-3">
<div className="flex items-center justify-between bg-[#1E2A28] rounded-t-xl px-4 py-2 border-b border-white/5">
<span className="text-[10px] text-gray-400 font-mono font-bold uppercase tracking-widest">{lang}</span>
<button onClick={copy} className="text-[10px] text-gray-400 font-bold hover:text-white transition-colors">
{copied ? '✓ Copied' : 'Copy'}
</button>
</div>
<pre className="bg-[#152320] text-emerald-300 text-xs font-mono p-4 rounded-b-xl overflow-x-auto leading-relaxed">
<code>{children}</code>
</pre>
</div>
);
}
function SectionHeader({ id, title, badge, badgeColor }: { id: string; title: string; badge?: string; badgeColor?: string }) {
return (
<div id={id} className="flex items-center gap-3 mb-6 pt-2 scroll-mt-6">
<h2 className="text-base font-black text-[#3D4E4B] dark:text-white tracking-tight">{title}</h2>
{badge && <Badge color={badgeColor}>{badge}</Badge>}
<div className="flex-1 h-px bg-gray-100 dark:bg-white/10"></div>
</div>
);
}
function Endpoint({ method, path, desc, auth = true }: { method: string; path: string; desc: string; auth?: boolean }) {
const methodColor: Record<string, string> = {
GET: 'bg-blue-100 text-blue-700',
POST: 'bg-emerald-100 text-emerald-700',
PATCH: 'bg-amber-100 text-amber-700',
DELETE: 'bg-red-100 text-red-700',
PUT: 'bg-purple-100 text-purple-700',
};
return (
<div className="flex items-center gap-3 py-3 border-b border-gray-50 last:border-0">
<span className={`shrink-0 px-2.5 py-1 rounded-lg text-[10px] font-black tracking-widest uppercase ${methodColor[method] ?? 'bg-gray-100 text-gray-600'}`}>{method}</span>
<code className="flex-1 text-xs font-mono text-[#3D4E4B] font-bold">{path}</code>
<span className="text-xs text-gray-400 font-medium hidden md:block">{desc}</span>
{auth && <span className="shrink-0 text-[10px] text-amber-600 font-bold">🔒 Auth</span>}
</div>
);
}
export default function DocsIndex() {
const [activeSection, setActiveSection] = useState('intro');
const contentRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const ids = NAV.map(n => n.id);
const observer = new IntersectionObserver(
(entries) => {
const visible = entries
.filter(e => e.isIntersecting)
.sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top);
if (visible.length > 0) setActiveSection(visible[0].target.id);
},
{ rootMargin: '-20% 0px -70% 0px', threshold: 0 }
);
ids.forEach(id => { const el = document.getElementById(id); if (el) observer.observe(el); });
return () => observer.disconnect();
}, []);
const scrollTo = (id: string) => {
document.getElementById(id)?.scrollIntoView({ behavior: 'smooth', block: 'start' });
};
return (
<AuthenticatedLayout>
<Head title="Dokumentasi biiproject kit v2" />
<div className="flex items-center justify-between mb-8 anim-down">
<div>
<h1 className="text-xl font-bold text-[#3D4E4B] dark:text-white tracking-tight leading-none">Dokumentasi</h1>
<p className="text-sm font-semibold text-gray-400 tracking-tight mt-2">Panduan lengkap biiproject kit v2</p>
</div>
<Badge color="green">v2.0</Badge>
</div>
<div className="flex gap-8 anim-up">
{/* Sticky Sidebar Nav */}
<aside className="hidden lg:block w-52 shrink-0">
<div className="sticky top-6 bg-white dark:bg-[#1A2120] rounded-2xl border border-gray-100 dark:border-white/10 shadow-sm p-3 space-y-0.5">
{NAV.map(s => (
<button key={s.id} onClick={() => scrollTo(s.id)}
className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-xl text-xs font-bold text-left transition-all ${activeSection === s.id ? 'bg-[#3D4E4B] text-white' : 'text-gray-500 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-white/5 hover:text-[#3D4E4B] dark:hover:text-white'}`}>
<span className={`shrink-0 ${activeSection === s.id ? 'text-[#D4A017]' : ''}`}>{s.icon}</span>
{s.label}
</button>
))}
</div>
</aside>
{/* Content */}
<div ref={contentRef} className="flex-1 min-w-0 space-y-12 pb-20">
{/* ── PENDAHULUAN ── */}
<section>
<SectionHeader id="intro" title="Pendahuluan" badge="Starter Kit" badgeColor="blue" />
<div className="bg-white dark:bg-[#1A2120] rounded-2xl border border-gray-100 dark:border-white/10 shadow-sm p-8 space-y-4">
<p className="text-sm text-gray-600 dark:text-gray-300 font-medium leading-relaxed">
<strong className="text-[#3D4E4B] dark:text-white">biiproject kit v2</strong> adalah starter kit enterprise berbasis <strong>Laravel 13 + React (Inertia.js)</strong> yang dirancang untuk mempercepat pembangunan aplikasi web dengan fitur manajemen pengguna, hak akses berbasis peran, monitoring aktivitas, notifikasi, dan konfigurasi sistem yang lengkap.
</p>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 pt-2">
{[
{ label: 'Users & Roles', icon: '👥' },
{ label: 'Activity Logs', icon: '📋' },
{ label: 'System Settings', icon: '⚙️' },
{ label: 'Two-Factor Auth', icon: '🔒' },
{ label: 'REST API v1', icon: '🔌' },
{ label: 'Dark Mode', icon: '🌙' },
{ label: 'Notifikasi', icon: '🔔' },
{ label: 'Mobile Ready', icon: '📱' },
].map(f => (
<div key={f.label} className="p-4 bg-gray-50 dark:bg-white/5 rounded-xl text-center">
<div className="text-2xl mb-1">{f.icon}</div>
<div className="text-[11px] font-bold text-[#3D4E4B] dark:text-white">{f.label}</div>
</div>
))}
</div>
</div>
</section>
{/* ── QUICK START ── */}
<section>
<SectionHeader id="quickstart" title="Quick Start" badge="Setup" badgeColor="green" />
<div className="bg-white dark:bg-[#1A2120] rounded-2xl border border-gray-100 dark:border-white/10 shadow-sm p-8 space-y-6">
<div>
<h3 className="text-xs font-black text-[#3D4E4B] dark:text-white uppercase tracking-widest mb-2">1. Clone & Install</h3>
<CodeBlock lang="bash">{`git clone https://github.com/your-org/biiskit.git
cd biiskit
composer install
npm install`}</CodeBlock>
</div>
<div>
<h3 className="text-xs font-black text-[#3D4E4B] dark:text-white uppercase tracking-widest mb-2">2. Konfigurasi Environment</h3>
<CodeBlock lang="bash">{`cp .env.example .env
php artisan key:generate
# Edit .env sesuai konfigurasi database
DB_CONNECTION=pgsql
DB_HOST=127.0.0.1
DB_PORT=5432
DB_DATABASE=biiskit
DB_USERNAME=your_user
DB_PASSWORD=your_password`}</CodeBlock>
</div>
<div>
<h3 className="text-xs font-black text-[#3D4E4B] dark:text-white uppercase tracking-widest mb-2">3. Migrasi & Seeder</h3>
<CodeBlock lang="bash">{`php artisan migrate --seed`}</CodeBlock>
<p className="text-xs text-gray-400 font-medium mt-2">Seeder akan membuat 3 akun default: <code className="bg-gray-100 dark:bg-white/10 px-1.5 py-0.5 rounded font-mono">superadmin</code>, <code className="bg-gray-100 dark:bg-white/10 px-1.5 py-0.5 rounded font-mono">admin</code>, dan <code className="bg-gray-100 dark:bg-white/10 px-1.5 py-0.5 rounded font-mono">user</code>.</p>
</div>
<div>
<h3 className="text-xs font-black text-[#3D4E4B] dark:text-white uppercase tracking-widest mb-2">4. Jalankan Server</h3>
<CodeBlock lang="bash">{`# Terminal 1 — Laravel
php artisan serve
# Terminal 2 — Vite dev server
npm run dev`}</CodeBlock>
</div>
<div className="p-4 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-700/40 rounded-xl">
<p className="text-xs font-bold text-amber-700 dark:text-amber-400">Akses aplikasi di <code className="font-mono">http://localhost:8000</code> — redirect otomatis ke halaman login.</p>
</div>
</div>
</section>
{/* ── TECH STACK ── */}
<section>
<SectionHeader id="stack" title="Tech Stack" />
<div className="bg-white dark:bg-[#1A2120] rounded-2xl border border-gray-100 dark:border-white/10 shadow-sm p-8">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{[
{ layer: 'Backend', items: ['Laravel 13', 'PHP 8.3', 'PostgreSQL', 'Laravel Sanctum', 'Spatie Permission', 'Spatie Activity Log'] },
{ layer: 'Frontend', items: ['React 18', 'Inertia.js v2', 'TypeScript', 'Tailwind CSS v4', 'Vite 6', 'Chart.js'] },
{ layer: 'Keamanan', items: ['RBAC (Role-Based Access Control)', 'Two-Factor Auth (TOTP)', 'Sanctum API Tokens', 'Gate::before super-admin bypass', 'Bcrypt Password Hashing'] },
{ layer: 'DevOps', items: ['PostgreSQL + Redis (Docker ready)', 'Laravel Queue (database)', 'Cache: database driver', 'Pest PHP testing suite'] },
].map(s => (
<div key={s.layer} className="p-5 bg-gray-50 dark:bg-white/5 rounded-xl">
<div className="text-xs font-black text-[#D4A017] uppercase tracking-widest mb-3">{s.layer}</div>
<div className="space-y-1.5">
{s.items.map(i => (
<div key={i} className="flex items-center gap-2 text-xs font-semibold text-gray-600 dark:text-gray-300">
<span className="w-1.5 h-1.5 rounded-full bg-[#3D4E4B] dark:bg-[#D4A017] shrink-0"></span>
{i}
</div>
))}
</div>
</div>
))}
</div>
</div>
</section>
{/* ── AUTENTIKASI ── */}
<section>
<SectionHeader id="auth" title="Autentikasi" badge="Web + API" badgeColor="purple" />
<div className="bg-white dark:bg-[#1A2120] rounded-2xl border border-gray-100 dark:border-white/10 shadow-sm p-8 space-y-6">
<div>
<h3 className="text-xs font-black text-[#3D4E4B] dark:text-white uppercase tracking-widest mb-4">Akun Bawaan (Seeder)</h3>
<div className="overflow-x-auto">
<table className="w-full text-xs">
<thead>
<tr className="border-b border-gray-100 dark:border-white/10">
<th className="text-left font-black text-gray-400 uppercase tracking-widest py-2 pr-6">Email</th>
<th className="text-left font-black text-gray-400 uppercase tracking-widest py-2 pr-6">Password</th>
<th className="text-left font-black text-gray-400 uppercase tracking-widest py-2">Role</th>
</tr>
</thead>
<tbody>
{[
{ email: 'superadmin@biiskit.com', pw: 'password', role: 'super-admin', color: 'red' },
{ email: 'admin@biiskit.com', pw: 'password', role: 'admin', color: 'amber' },
{ email: 'user@biiskit.com', pw: 'password', role: 'user', color: 'blue' },
].map(u => (
<tr key={u.email} className="border-b border-gray-50 dark:border-white/5 last:border-0">
<td className="py-3 pr-6 font-mono text-[#3D4E4B] dark:text-white font-bold">{u.email}</td>
<td className="py-3 pr-6 font-mono text-gray-400">{u.pw}</td>
<td className="py-3"><Badge color={u.color}>{u.role}</Badge></td>
</tr>
))}
</tbody>
</table>
</div>
</div>
<div>
<h3 className="text-xs font-black text-[#3D4E4B] dark:text-white uppercase tracking-widest mb-3">Alur Login Web</h3>
<div className="flex flex-wrap gap-2 items-center text-xs font-bold text-gray-500">
{['Form Login', '→', 'Auth Check', '→', '2FA Challenge?', '→', 'Email Verified?', '→', 'Dashboard'].map((s, i) => (
<span key={i} className={s === '→' ? 'text-gray-300' : 'px-3 py-1.5 bg-gray-50 dark:bg-white/5 rounded-lg text-[#3D4E4B] dark:text-white'}>{s}</span>
))}
</div>
<p className="text-xs text-gray-400 font-medium mt-2">2FA Challenge hanya muncul jika user telah mengaktifkan Two-Factor Auth di <code className="bg-gray-100 dark:bg-white/10 px-1 py-0.5 rounded font-mono">/settings#2fa</code>.</p>
</div>
<div>
<h3 className="text-xs font-black text-[#3D4E4B] dark:text-white uppercase tracking-widest mb-2">Login via API</h3>
<CodeBlock lang="json">{`POST /api/v1/login
Content-Type: application/json
{
"email": "admin@biiskit.com",
"password": "password"
}
// Response
{
"token": "1|abc123...",
"user": { "id": 1, "email": "admin@biiskit.com", ... }
}`}</CodeBlock>
</div>
</div>
</section>
{/* ── ROLES & PERMISSIONS ── */}
<section>
<SectionHeader id="roles" title="Roles & Permission" badge="Spatie" badgeColor="green" />
<div className="bg-white dark:bg-[#1A2120] rounded-2xl border border-gray-100 dark:border-white/10 shadow-sm p-8 space-y-6">
<p className="text-sm text-gray-500 dark:text-gray-400 font-medium leading-relaxed">
Menggunakan <strong className="text-[#3D4E4B] dark:text-white">spatie/laravel-permission</strong>. Role <Badge color="red">super-admin</Badge> mendapat akses penuh via <code className="text-[11px] bg-gray-100 dark:bg-white/10 px-1.5 py-0.5 rounded font-mono">Gate::before</code> bypass tidak perlu assign permission satu per satu.
</p>
<div className="overflow-x-auto">
<table className="w-full text-xs">
<thead>
<tr className="border-b border-gray-100 dark:border-white/10">
<th className="text-left font-black text-gray-400 uppercase tracking-widest py-2 pr-6">Permission</th>
<th className="text-center font-black text-gray-400 uppercase tracking-widest py-2 px-4">user</th>
<th className="text-center font-black text-gray-400 uppercase tracking-widest py-2 px-4">admin</th>
<th className="text-center font-black text-gray-400 uppercase tracking-widest py-2 px-4">super-admin</th>
</tr>
</thead>
<tbody>
{[
{ perm: 'user.view', u: true, a: true, s: true },
{ perm: 'user.create', u: false, a: true, s: true },
{ perm: 'user.edit', u: false, a: true, s: true },
{ perm: 'user.delete', u: false, a: true, s: true },
{ perm: 'role.view', u: false, a: true, s: true },
{ perm: 'role.manage', u: false, a: false, s: true },
{ perm: 'settings.manage',u: false, a: false, s: true },
{ perm: 'reports.view', u: false, a: true, s: true },
].map(row => (
<tr key={row.perm} className="border-b border-gray-50 dark:border-white/5 last:border-0">
<td className="py-3 pr-6 font-mono font-bold text-[#3D4E4B] dark:text-white">{row.perm}</td>
{[row.u, row.a, row.s].map((v, i) => (
<td key={i} className="py-3 text-center px-4">
{v ? <span className="text-emerald-500 font-black text-sm"></span> : <span className="text-gray-200 font-black text-sm"></span>}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
<div>
<h3 className="text-xs font-black text-[#3D4E4B] dark:text-white uppercase tracking-widest mb-2">Pengecekan Permission di Controller</h3>
<CodeBlock lang="php">{`// Via Policy (model instance)
$this->authorize('update', $user);
// Via Gate string (tanpa model)
$this->authorize('user.delete');
// Via Blade / React (shared props)
// auth.permissions = ['user.view', 'user.create', ...]`}</CodeBlock>
</div>
</div>
</section>
{/* ── FITUR LENGKAP ── */}
<section>
<SectionHeader id="features" title="Fitur Lengkap" />
<div className="bg-white dark:bg-[#1A2120] rounded-2xl border border-gray-100 dark:border-white/10 shadow-sm p-8 space-y-6">
{[
{
title: 'Manajemen Pengguna',
icon: '👥',
items: [
'CRUD lengkap (tambah, edit, hapus, restore)',
'Soft delete dengan arsip & purge permanen',
'Bulk archive / restore / force-delete',
'Filter by status, role, pencarian nama/email',
'Sorting multi-kolom + pagination',
'Export ke Excel (.xlsx)',
'Import user massal via Excel/CSV',
'Assign multi-role per user',
],
},
{
title: 'Roles & Permission Manager',
icon: '🛡️',
items: [
'Kelola role dari UI (tambah / hapus role)',
'Assign/revoke permission per role via toggle',
'Super-admin bypass via Gate::before',
'3 role default: super-admin, admin, user',
],
},
{
title: 'Notifikasi',
icon: '🔔',
items: [
'Kirim notifikasi (email / in-app)',
'Target: all users, role tertentu, atau user spesifik',
'Log pengiriman dengan status (sent/failed)',
'Badge counter di topbar (unread last 7 hari)',
'Pagination history notifikasi',
],
},
{
title: 'Activity Logs',
icon: '📋',
items: [
'Log otomatis via spatie/laravel-activitylog',
'Filter by user, event, tanggal',
'Bulk delete logs',
'Tampilan subject & properties berubah',
],
},
{
title: 'Dashboard',
icon: '📊',
items: [
'Statistik total users, admin, active, inactive',
'Chart pendaftaran user 30 hari terakhir (Chart.js)',
'Tabel aktivitas terbaru',
'Quick actions (tambah user, lihat logs)',
],
},
{
title: 'Account Settings',
icon: '👤',
items: [
'Tab Profile: nama, email, telepon, bio, avatar upload',
'Tab Security & Password: ganti password',
'Tab Two-Factor Auth: aktifkan/nonaktifkan TOTP 2FA',
'Tab Danger Zone: hapus akun permanen',
'Tab aktif persisten saat reload (via URL hash)',
],
},
{
title: 'UI/UX',
icon: '🎨',
items: [
'Dark mode toggle (persisted di localStorage)',
'Sidebar responsif + burger menu mobile',
'Breadcrumb dinamis di topbar',
'Flash messages (success / error)',
'Animasi masuk halaman (anim-down, anim-up, anim-left)',
'Tab state persisten via URL hash (#tab-name)',
'Custom scrollbar',
'Error pages: 403, 404, 500',
],
},
].map(feature => (
<div key={feature.title} className="p-5 bg-gray-50 dark:bg-white/5 rounded-xl">
<div className="flex items-center gap-3 mb-3">
<span className="text-xl">{feature.icon}</span>
<div className="text-sm font-black text-[#3D4E4B] dark:text-white">{feature.title}</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-1.5">
{feature.items.map(item => (
<div key={item} className="flex items-start gap-2 text-xs font-medium text-gray-500 dark:text-gray-400">
<span className="text-emerald-500 font-black mt-0.5 shrink-0"></span>
{item}
</div>
))}
</div>
</div>
))}
</div>
</section>
{/* ── REST API ── */}
<section>
<SectionHeader id="api" title="REST API" badge="v1" badgeColor="blue" />
<div className="bg-white dark:bg-[#1A2120] rounded-2xl border border-gray-100 dark:border-white/10 shadow-sm p-8 space-y-6">
<div>
<h3 className="text-xs font-black text-[#3D4E4B] dark:text-white uppercase tracking-widest mb-2">Base URL</h3>
<CodeBlock lang="http">{'http://your-domain.com/api/v1'}</CodeBlock>
</div>
<div>
<h3 className="text-xs font-black text-[#3D4E4B] dark:text-white uppercase tracking-widest mb-2">Authentication Header</h3>
<CodeBlock lang="http">{'Authorization: Bearer {your-sanctum-token}'}</CodeBlock>
</div>
<div>
<h3 className="text-xs font-black text-[#3D4E4B] dark:text-white uppercase tracking-widest mb-4">Endpoints</h3>
<div className="border border-gray-100 dark:border-white/10 rounded-xl overflow-hidden">
<div className="px-4 py-2.5 bg-gray-50 dark:bg-white/5 border-b border-gray-100 dark:border-white/10">
<span className="text-[10px] font-black text-gray-400 uppercase tracking-widest">Auth</span>
</div>
<div className="px-4">
<Endpoint method="POST" path="/api/v1/login" desc="Dapatkan Sanctum token" auth={false} />
<Endpoint method="GET" path="/api/v1/me" desc="Data user aktif" />
<Endpoint method="POST" path="/api/v1/logout" desc="Revoke token" />
</div>
<div className="px-4 py-2.5 bg-gray-50 dark:bg-white/5 border-y border-gray-100 dark:border-white/10">
<span className="text-[10px] font-black text-gray-400 uppercase tracking-widest">Users</span>
</div>
<div className="px-4">
<Endpoint method="GET" path="/api/v1/users" desc="List semua user" />
<Endpoint method="POST" path="/api/v1/users" desc="Buat user baru" />
<Endpoint method="GET" path="/api/v1/users/{id}" desc="Detail user" />
<Endpoint method="PATCH" path="/api/v1/users/{id}" desc="Update user" />
<Endpoint method="DELETE" path="/api/v1/users/{id}" desc="Hapus user" />
</div>
<div className="px-4 py-2.5 bg-gray-50 dark:bg-white/5 border-y border-gray-100 dark:border-white/10">
<span className="text-[10px] font-black text-gray-400 uppercase tracking-widest">App Config</span>
</div>
<div className="px-4">
<Endpoint method="GET" path="/api/v1/app-config" desc="Konfigurasi aplikasi publik" auth={false} />
</div>
</div>
</div>
</div>
</section>
{/* ── 2FA ── */}
<section>
<SectionHeader id="2fa" title="Two-Factor Authentication" badge="TOTP" badgeColor="amber" />
<div className="bg-white dark:bg-[#1A2120] rounded-2xl border border-gray-100 dark:border-white/10 shadow-sm p-8 space-y-4">
<p className="text-sm text-gray-500 dark:text-gray-400 font-medium leading-relaxed">
2FA menggunakan protokol <strong className="text-[#3D4E4B] dark:text-white">TOTP (Time-based One-Time Password)</strong> yang kompatibel dengan Google Authenticator, Authy, dan 1Password.
</p>
<div className="space-y-3">
{[
{ step: '1', title: 'Buka tab Two-Factor Auth', desc: 'Masuk ke Account Settings (/settings) → tab "Two-Factor Auth"' },
{ step: '2', title: 'Scan QR Code', desc: 'Gunakan aplikasi authenticator (Google Authenticator / Authy) untuk scan QR' },
{ step: '3', title: 'Masukkan kode verifikasi', desc: 'Ketik 6 digit dari aplikasi untuk mengaktifkan 2FA' },
{ step: '4', title: 'Simpan recovery codes', desc: '8 kode cadangan tersedia — simpan di tempat aman jika kehilangan akses ke authenticator' },
{ step: '5', title: 'Login berikutnya', desc: 'Setelah diaktifkan, setiap login akan redirect ke halaman 2FA Challenge sebelum masuk dashboard' },
].map(s => (
<div key={s.step} className="flex items-start gap-4 p-4 bg-gray-50 dark:bg-white/5 rounded-xl">
<div className="w-7 h-7 rounded-full bg-[#3D4E4B] text-white text-xs font-black flex items-center justify-center shrink-0">{s.step}</div>
<div>
<div className="text-sm font-bold text-[#3D4E4B] dark:text-white">{s.title}</div>
<div className="text-xs text-gray-400 font-medium mt-0.5">{s.desc}</div>
</div>
</div>
))}
</div>
<div className="p-4 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-700/40 rounded-xl">
<p className="text-xs font-bold text-blue-700 dark:text-blue-400">2FA bersifat opsional per user. Setelah diaktifkan, setiap login akan meminta kode 6 digit dari authenticator.</p>
</div>
</div>
</section>
{/* ── SYSTEM SETTINGS ── */}
<section>
<SectionHeader id="settings" title="System Settings" badge="Super Admin" badgeColor="red" />
<div className="bg-white dark:bg-[#1A2120] rounded-2xl border border-gray-100 dark:border-white/10 shadow-sm p-8 space-y-4">
<p className="text-sm text-gray-500 dark:text-gray-400 font-medium">Hanya bisa diakses oleh pengguna dengan role <Badge color="red">super-admin</Badge>. Tersedia di <code className="text-xs bg-gray-100 dark:bg-white/10 px-2 py-0.5 rounded font-mono">/system-settings</code>.</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{[
{ tab: 'General & Branding', items: ['Nama aplikasi', 'Logo upload', 'Teks logo fallback', 'Registrasi publik on/off', 'Verifikasi email on/off'] },
{ tab: 'Security & OAuth', items: ['Password minimum panjang', 'Wajib huruf besar/kecil/angka/simbol', 'Google OAuth (Client ID & Secret)', 'GitHub OAuth (Client ID & Secret)'] },
{ tab: 'Email / SMTP', items: ['Host & port SMTP', 'Enkripsi (TLS/SSL)', 'Username & password SMTP', 'From name & address', 'Test kirim email dari UI'] },
{ tab: 'Mobile App Control', items: ['Versi terbaru & minimum Android', 'URL Play Store', 'Mode maintenance mobile app', 'Pesan maintenance kustom'] },
].map(t => (
<div key={t.tab} className="p-5 bg-gray-50 dark:bg-white/5 rounded-xl">
<div className="text-xs font-black text-[#D4A017] uppercase tracking-widest mb-3">{t.tab}</div>
{t.items.map(i => (
<div key={i} className="flex items-center gap-2 text-xs font-semibold text-gray-500 dark:text-gray-400 mb-1.5">
<span className="w-1.5 h-1.5 rounded-full bg-gray-300 dark:bg-gray-600 shrink-0"></span>
{i}
</div>
))}
</div>
))}
</div>
</div>
</section>
{/* ── STRUKTUR FOLDER ── */}
<section>
<SectionHeader id="structure" title="Struktur Folder" />
<div className="bg-white dark:bg-[#1A2120] rounded-2xl border border-gray-100 dark:border-white/10 shadow-sm p-8 space-y-4">
<CodeBlock lang="text">{`biiskit/
├── app/
│ ├── Http/
│ │ ├── Controllers/ # Web + API controllers
│ │ │ └── Api/V1/ # REST API v1 controllers
│ │ ├── Middleware/
│ │ │ └── HandleInertiaRequests.php # Shared props (auth, settings)
│ │ └── Requests/ # Form request validation
│ ├── Models/
│ │ ├── User.php # SoftDeletes + HasRoles + HasPermissions
│ │ ├── Setting.php # System settings key-value store
│ │ └── NotificationLog.php # Notifikasi log
│ └── Policies/
│ └── UserPolicy.php # Gate policies untuk user CRUD
├── database/
│ ├── migrations/ # PostgreSQL migrations
│ └── seeders/
│ └── DatabaseSeeder.php # Roles, permissions, demo users
├── resources/js/
│ ├── Layouts/
│ │ ├── AuthenticatedLayout.tsx # Wrapper utama + mobile sidebar
│ │ └── components/
│ │ ├── Sidebar.tsx # Nav dengan permission check
│ │ └── Topbar.tsx # Breadcrumb + notif bell + dark mode
│ ├── Pages/
│ │ ├── Auth/ # Login, Register, Password reset
│ │ ├── Dashboard/ # Dashboard dengan chart
│ │ ├── Users/ # Index, Show (CRUD)
│ │ ├── Roles/ # Roles & permission manager
│ │ ├── Notifications/ # Kirim & history notifikasi
│ │ ├── ActivityLogs/ # Log aktivitas
│ │ ├── Settings/ # Account settings (profile, password, 2FA, danger zone)
│ │ ├── TwoFactor/ # 2FA challenge page (login flow, no auth)
│ │ ├── SystemSettings/ # Sistem config (super-admin)
│ │ ├── Docs/ # Halaman dokumentasi ini
│ │ └── Errors/ # 403, 404, 500 pages
│ └── Components/
│ ├── DataTable.tsx # Reusable sortable table
│ └── FlashMessage.tsx # Success/error flash
├── routes/
│ ├── web.php # Web routes (Inertia)
│ └── api.php # API routes (Sanctum)
└── tests/
├── Feature/ # Feature tests (Pest)
└── Unit/ # Unit tests`}</CodeBlock>
</div>
</section>
</div>
</div>
</AuthenticatedLayout>
);
}