642 lines
47 KiB
TypeScript
642 lines
47 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: 'user.restore', u: false, a: true, s: true },
|
||
{ perm: 'user.force-delete', u: false, a: false, s: true },
|
||
{ perm: 'user.export', u: false, a: true, s: true },
|
||
{ perm: 'user.import', u: false, a: true, s: true },
|
||
{ perm: 'role.view', u: false, a: true, s: true },
|
||
{ perm: 'role.create', u: false, a: true, s: true },
|
||
{ perm: 'role.delete', u: false, a: true, s: true },
|
||
{ perm: 'role.manage', u: false, a: true, s: true },
|
||
{ perm: 'notifications.view', u: false, a: true, s: true },
|
||
{ perm: 'notifications.send', u: false, a: true, s: true },
|
||
{ perm: 'activity-logs.view', u: false, a: true, s: true },
|
||
{ perm: 'activity-logs.delete', u: false, a: false, s: true },
|
||
{ perm: 'settings.view', u: false, a: false, s: true },
|
||
{ perm: 'settings.edit', u: false, a: false, s: true },
|
||
{ perm: 'settings.test-email', u: false, a: false, s: true },
|
||
{ perm: 'documentation.view', u: false, a: false, 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 + EMAIL" 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-6">
|
||
<p className="text-sm text-gray-500 dark:text-gray-400 font-medium leading-relaxed">
|
||
Sistem mendukung dual-modality 2FA: <strong className="text-[#3D4E4B] dark:text-white">TOTP (Google Authenticator)</strong> dan <strong className="text-[#3D4E4B] dark:text-white">Email Verification OTP</strong>. Integrasi 2FA dirancang dengan prioritas keamanan ketat dan kontrol administrasi global.
|
||
</p>
|
||
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
<div className="p-5 bg-gray-50 dark:bg-white/5 rounded-xl border border-gray-100 dark:border-white/10">
|
||
<div className="text-xs font-black text-[#D4A017] uppercase tracking-widest mb-3">📱 Google Authenticator / TOTP</div>
|
||
<ul className="space-y-2 text-xs font-medium text-gray-500 dark:text-gray-400 list-disc list-inside">
|
||
<li>Protokol standar industri RFC 6238</li>
|
||
<li>Dilengkapi 8 Recovery Codes cadangan sekali pakai</li>
|
||
<li>Dapat di-scan instan via QR Code</li>
|
||
</ul>
|
||
</div>
|
||
<div className="p-5 bg-gray-50 dark:bg-white/5 rounded-xl border border-gray-100 dark:border-white/10">
|
||
<div className="text-xs font-black text-[#D4A017] uppercase tracking-widest mb-3">✉️ Email OTP 2FA</div>
|
||
<ul className="space-y-2 text-xs font-medium text-gray-500 dark:text-gray-400 list-disc list-inside">
|
||
<li>Pengiriman kode 6-digit dinamis langsung ke email user</li>
|
||
<li>Masa berlaku kode terbatas (TTL 10 menit)</li>
|
||
<li>Mencegah aktivasi jika SMTP Mailer belum tervalidasi</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<h3 className="text-xs font-black text-[#3D4E4B] dark:text-white uppercase tracking-widest mb-3">Alur Aktivasi Keamanan 2FA</h3>
|
||
<div className="space-y-3">
|
||
{[
|
||
{ step: '1', title: 'Verifikasi SMTP / Mail Delivery', desc: 'Sebelum mengaktifkan Email 2FA, pastikan SMTP Mailer di System Settings terkonfigurasi dengan benar dan di-tes sukses handshake-nya.' },
|
||
{ step: '2', title: 'Pilih Metode di Pengaturan Pengguna', desc: 'Akses halaman Account Settings (/settings) → tab "Two-Factor Auth" untuk memilih metode yang diinginkan.' },
|
||
{ step: '3', title: 'Konfirmasi Kode OTP / Autentikator', desc: 'Masukkan kode validasi perdana untuk memastikan setup sinkron dengan sistem sebelum metode di-lock.' },
|
||
{ step: '4', title: 'Global Admin Override (Force Disable)', desc: 'Apabila administrator menonaktifkan metode 2FA secara global di System Settings, bypass otomatis akan di-enforce di login screen.' },
|
||
].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>
|
||
</div>
|
||
</section>
|
||
|
||
{/* ── SYSTEM SETTINGS ── */}
|
||
<section>
|
||
<SectionHeader id="settings" title="System Settings" badge="Admin / 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">Dapat diakses oleh pengguna dengan role <Badge color="red">super-admin</Badge> atau memiliki permission <Badge color="blue">settings.view</Badge>. Perubahan konfigurasi membutuhkan permission <Badge color="blue">settings.edit</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', 'Global TOTP 2FA toggle (Aktif/Nonaktif)', 'Global Email 2FA toggle (Aktif/Nonaktif)'] },
|
||
{ 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 handshake real-time (butuh settings.test-email)'] },
|
||
{ 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>
|
||
);
|
||
}
|