import React, { useState, useEffect } from 'react';
import {
LayoutDashboard,
Users,
PlusCircle,
BookOpen,
Trash2,
Camera,
Loader2,
ChevronRight,
Filter,
CheckCircle2,
XCircle
} from 'lucide-react';
// KONFIGURASI API - Masukkan URL Web App dari Apps Script Anda di sini
const API_URL = "MASUKKAN_URL_WEB_APP_ANDA_DI_SINI";
const App = () => {
const [currentView, setCurrentView] = useState('dashboard');
const [loading, setLoading] = useState(false);
const [pelatihan, setPelatihan] = useState([]);
const [peserta, setPeserta] = useState([]);
const [statusFilter, setStatusFilter] = useState('Semua');
// Fetch data awal
useEffect(() => {
fetchData();
}, []);
const fetchData = async () => {
setLoading(true);
try {
const resPelatihan = await fetch(`${API_URL}?action=getPelatihan`);
const dataPelatihan = await resPelatihan.json();
if(dataPelatihan.status === 'success') setPelatihan(dataPelatihan.data);
const resPeserta = await fetch(`${API_URL}?action=getPeserta`);
const dataPeserta = await resPeserta.json();
if(dataPeserta.status === 'success') setPeserta(dataPeserta.data);
} catch (err) {
console.error("Gagal mengambil data", err);
} finally {
setLoading(false);
}
};
// Komponen Navigasi
const NavItem = ({ id, icon: Icon, label }) => (
<button
onClick={() => setCurrentView(id)}
className={`flex items-center space-x-3 w-full p-3 rounded-lg transition-all ${
currentView === id ? 'bg-navy-700 text-white shadow-lg' : 'text-slate-400 hover:bg-slate-800'
}`}
>
<Icon size={20} />
<span className="font-medium">{label}</span>
</button>
);
return (
<div className="flex h-screen bg-slate-50 text-slate-900 font-sans">
{/* Sidebar */}
<aside className="w-64 bg-slate-900 text-white p-6 hidden md:block">
<div className="flex items-center space-x-3 mb-10 px-2">
<div className="bg-blue-500 p-2 rounded-lg">
<BookOpen size={24} className="text-white" />
</div>
<h1 className="text-xl font-bold tracking-tight">E-Latih</h1>
</div>
<nav className="space-y-2">
<NavItem id="dashboard" icon={LayoutDashboard} label="Dashboard" />
<NavItem id="pelatihan" icon={BookOpen} label="Daftar Pelatihan" />
<NavItem id="admin" icon={PlusCircle} label="Manajemen Admin" />
</nav>
</aside>
{/* Main Content */}
<main className="flex-1 overflow-y-auto">
<header className="bg-white border-b border-slate-200 p-4 sticky top-0 z-10 flex justify-between items-center">
<h2 className="text-lg font-semibold capitalize">{currentView.replace('-', ' ')}</h2>
<div className="flex items-center space-x-4">
{loading && <Loader2 className="animate-spin text-blue-600" size={20} />}
<div className="h-8 w-8 rounded-full bg-slate-200"></div>
</div>
</header>
<div className="p-6 max-w-6xl mx-auto">
{currentView === 'dashboard' && (
<DashboardStats pelatihan={pelatihan} peserta={peserta} />
)}
{currentView === 'pelatihan' && (
<PelatihanList
data={pelatihan}
filter={statusFilter}
setFilter={setStatusFilter}
/>
)}
{currentView === 'admin' && (
<AdminPanel
pelatihan={pelatihan}
peserta={peserta}
onRefresh={fetchData}
/>
)}
</div>
</main>
{/* Mobile Nav */}
<div className="md:hidden fixed bottom-0 left-0 right-0 bg-white border-t border-slate-200 flex justify-around p-3">
<button onClick={() => setCurrentView('dashboard')} className={currentView === 'dashboard' ? 'text-blue-600' : 'text-slate-400'}>
<LayoutDashboard size={24} />
</button>
<button onClick={() => setCurrentView('pelatihan')} className={currentView === 'pelatihan' ? 'text-blue-600' : 'text-slate-400'}>
<BookOpen size={24} />
</button>
<button onClick={() => setCurrentView('admin')} className={currentView === 'admin' ? 'text-blue-600' : 'text-slate-400'}>
<PlusCircle size={24} />
</button>
</div>
</div>
);
};
// --- SUB-KOMPONEN ---
const DashboardStats = ({ pelatihan, peserta }) => {
const activePelatihan = pelatihan.filter(p => p.Status === 'Aktif').length;
return (
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<StatCard title="Total Pelatihan" value={pelatihan.length} icon={BookOpen} color="blue" />
<StatCard title="Pelatihan Aktif" value={activePelatihan} icon={CheckCircle2} color="green" />
<StatCard title="Total Peserta" value={peserta.length} icon={Users} color="purple" />
<div className="md:col-span-3 bg-white p-6 rounded-xl border border-slate-200 shadow-sm">
<h3 className="text-lg font-bold mb-4">Pelatihan Terbaru</h3>
<div className="space-y-4">
{pelatihan.slice(0, 3).map((p, i) => (
<div key={i} className="flex justify-between items-center p-3 hover:bg-slate-50 rounded-lg border border-transparent hover:border-slate-100 transition-all">
<div>
<p className="font-semibold text-slate-800">{p['Nama Pelatihan']}</p>
<p className="text-sm text-slate-500">{p.Pemateri} • {p.Tanggal}</p>
</div>
<ChevronRight className="text-slate-300" />
</div>
))}
</div>
</div>
</div>
);
};
const StatCard = ({ title, value, icon: Icon, color }) => {
const colors = {
blue: 'bg-blue-50 text-blue-600',
green: 'bg-emerald-50 text-emerald-600',
purple: 'bg-violet-50 text-violet-600'
};
return (
<div className="bg-white p-6 rounded-xl border border-slate-200 shadow-sm">
<div className="flex justify-between items-start">
<div>
<p className="text-sm font-medium text-slate-500 uppercase tracking-wider">{title}</p>
<h4 className="text-3xl font-bold mt-2 text-slate-900">{value}</h4>
</div>
<div className={`p-3 rounded-xl ${colors[color]}`}>
<Icon size={24} />
</div>
</div>
</div>
);
};
const PelatihanList = ({ data, filter, setFilter }) => {
const filteredData = filter === 'Semua' ? data : data.filter(p => p.Status === filter);
return (
<div className="space-y-6">
<div className="flex items-center space-x-2 overflow-x-auto pb-2">
{['Semua', 'Aktif', 'Selesai', 'Mendatang'].map(f => (
<button
key={f}
onClick={() => setFilter(f)}
className={`px-4 py-2 rounded-full text-sm font-medium whitespace-nowrap transition-all ${
filter === f ? 'bg-slate-900 text-white' : 'bg-white border border-slate-200 text-slate-600 hover:border-slate-300'
}`}
>
{f}
</button>
))}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredData.map((p, i) => (
<div key={i} className="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden hover:shadow-md transition-all">
<div className={`h-2 ${p.Status === 'Aktif' ? 'bg-green-500' : 'bg-slate-400'}`}></div>
<div className="p-5">
<div className="flex justify-between items-start mb-3">
<span className={`text-[10px] uppercase font-bold px-2 py-1 rounded-md ${
p.Status === 'Aktif' ? 'bg-green-100 text-green-700' : 'bg-slate-100 text-slate-600'
}`}>
{p.Status}
</span>
<p className="text-xs text-slate-400">ID: {p.ID}</p>
</div>
<h4 className="text-lg font-bold text-slate-800 mb-1">{p['Nama Pelatihan']}</h4>
<p className="text-sm text-slate-500 mb-4">{p.Pemateri}</p>
<div className="grid grid-cols-2 gap-4 text-sm text-slate-600">
<div className="flex items-center space-x-2">
<LayoutDashboard size={14} className="text-slate-400" />
<span>{p.Tanggal}</span>
</div>
<div className="flex items-center space-x-2">
<Users size={14} className="text-slate-400" />
<span>{p.Kuota} Kuota</span>
</div>
</div>
</div>
</div>
))}
</div>
</div>
);
};
const AdminPanel = ({ pelatihan, peserta, onRefresh }) => {
const [isSubmitting, setIsSubmitting] = useState(false);
const [activeTab, setActiveTab] = useState('tambah'); // 'tambah' or 'daftar-peserta'
const [formData, setFormData] = useState({
nama: '', pemateri: '', tanggal: '', kuota: '', status: 'Aktif'
});
const handleSubmitPelatihan = async (e) => {
e.preventDefault();
setIsSubmitting(true);
try {
await fetch(API_URL, {
method: 'POST',
mode: 'no-cors',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'addPelatihan', ...formData })
});
setFormData({ nama: '', pemateri: '', tanggal: '', kuota: '', status: 'Aktif' });
// Karena no-cors, kita tidak bisa membaca response. Asumsikan sukses dan refresh
setTimeout(() => onRefresh(), 2000);
} catch (err) {
console.error(err);
} finally {
setIsSubmitting(false);
}
};
return (
<div className="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden">
<div className="flex border-b border-slate-200">
<button
onClick={() => setActiveTab('tambah')}
className={`flex-1 py-4 text-sm font-bold ${activeTab === 'tambah' ? 'text-blue-600 border-b-2 border-blue-600' : 'text-slate-500'}`}
>
TAMBAH PELATIHAN
</button>
<button
onClick={() => setActiveTab('peserta')}
className={`flex-1 py-4 text-sm font-bold ${activeTab === 'peserta' ? 'text-blue-600 border-b-2 border-blue-600' : 'text-slate-500'}`}
>
MANAJEMEN PESERTA
</button>
</div>
<div className="p-8">
{activeTab === 'tambah' ? (
<form onSubmit={handleSubmitPelatihan} className="space-y-6 max-w-2xl">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<label className="text-sm font-bold text-slate-700">Nama Pelatihan</label>
<input
required
type="text"
value={formData.nama}
onChange={(e) => setFormData({...formData, nama: e.target.value})}
className="w-full p-3 rounded-lg border border-slate-300 focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition-all"
placeholder="Contoh: React Masterclass"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-bold text-slate-700">Pemateri</label>
<input
required
type="text"
value={formData.pemateri}
onChange={(e) => setFormData({...formData, pemateri: e.target.value})}
className="w-full p-3 rounded-lg border border-slate-300 focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition-all"
placeholder="Nama Pemateri"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-bold text-slate-700">Tanggal</label>
<input
required
type="date"
value={formData.tanggal}
onChange={(e) => setFormData({...formData, tanggal: e.target.value})}
className="w-full p-3 rounded-lg border border-slate-300 focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition-all"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-bold text-slate-700">Kuota</label>
<input
required
type="number"
value={formData.kuota}
onChange={(e) => setFormData({...formData, kuota: e.target.value})}
className="w-full p-3 rounded-lg border border-slate-300 focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition-all"
placeholder="0"
/>
</div>
</div>
<button
disabled={isSubmitting}
type="submit"
className="w-full md:w-auto px-8 py-3 bg-blue-600 text-white rounded-lg font-bold hover:bg-blue-700 transition-all disabled:opacity-50 flex items-center justify-center space-x-2"
>
{isSubmitting ? <Loader2 className="animate-spin" /> : <PlusCircle size={20} />}
<span>SIMPAN PELATIHAN</span>
</button>
</form>
) : (
<PesertaManager pelatihan={pelatihan} peserta={peserta} onRefresh={onRefresh} />
)}
</div>
</div>
);
};
const PesertaManager = ({ pelatihan, peserta, onRefresh }) => {
const [selectedPelatihan, setSelectedPelatihan] = useState('');
const [formPeserta, setFormPeserta] = useState({ nama: '', email: '', institusi: '', foto: null });
const [isSubmitting, setIsSubmitting] = useState(false);
const handleFileChange = (e) => {
const file = e.target.files[0];
const reader = new FileReader();
reader.onloadend = () => {
setFormPeserta({ ...formPeserta, foto: reader.result, fotoName: file.name });
};
if (file) reader.readAsDataURL(file);
};
const handleAddPeserta = async (e) => {
e.preventDefault();
if (!selectedPelatihan) return;
setIsSubmitting(true);
try {
await fetch(API_URL, {
method: 'POST',
mode: 'no-cors',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'addPeserta',
idPelatihan: selectedPelatihan,
fotoBase64: formPeserta.foto,
fotoName: formPeserta.fotoName,
...formPeserta
})
});
setFormPeserta({ nama: '', email: '', institusi: '', foto: null });
setTimeout(() => onRefresh(), 2000);
} catch (err) {
console.error(err);
} finally {
setIsSubmitting(false);
}
};
const filteredPeserta = selectedPelatihan
? peserta.filter(p => p['ID Pelatihan'] === selectedPelatihan)
: [];
return (
<div className="space-y-8">
<div className="bg-slate-50 p-6 rounded-xl border border-slate-200">
<h4 className="font-bold mb-4 flex items-center space-x-2">
<PlusCircle size={18} className="text-blue-600" />
<span>Tambah Peserta Baru</span>
</h4>
<form onSubmit={handleAddPeserta} className="grid grid-cols-1 md:grid-cols-2 gap-4">
<select
required
className="p-3 rounded-lg border border-slate-300 outline-none"
value={selectedPelatihan}
onChange={(e) => setSelectedPelatihan(e.target.value)}
>
<option value="">Pilih Pelatihan</option>
{pelatihan.map(p => <option key={p.ID} value={p.ID}>{p['Nama Pelatihan']}</option>)}
</select>
<input required type="text" placeholder="Nama Peserta" value={formPeserta.nama} onChange={e => setFormPeserta({...formPeserta, nama: e.target.value})} className="p-3 rounded-lg border border-slate-300" />
<input required type="email" placeholder="Email" value={formPeserta.email} onChange={e => setFormPeserta({...formPeserta, email: e.target.value})} className="p-3 rounded-lg border border-slate-300" />
<input required type="text" placeholder="Institusi" value={formPeserta.institusi} onChange={e => setFormPeserta({...formPeserta, institusi: e.target.value})} className="p-3 rounded-lg border border-slate-300" />
<div className="md:col-span-2 flex items-center space-x-4">
<label className="flex-1 cursor-pointer bg-white border border-dashed border-slate-400 p-3 rounded-lg text-center hover:bg-slate-50">
<div className="flex items-center justify-center space-x-2 text-slate-500">
<Camera size={18} />
<span>{formPeserta.fotoName || 'Klik untuk Upload Foto'}</span>
</div>
<input type="file" className="hidden" accept="image/*" onChange={handleFileChange} />
</label>
<button disabled={isSubmitting} className="px-8 py-3 bg-blue-600 text-white rounded-lg font-bold disabled:opacity-50 min-w-[150px]">
{isSubmitting ? 'Mengirim...' : 'Tambah'}
</button>
</div>
</form>
</div>
<div>
<h4 className="font-bold mb-4">Daftar Peserta Pelatihan</h4>
{selectedPelatihan ? (
<div className="overflow-x-auto">
<table className="w-full text-left border-collapse">
<thead>
<tr className="bg-slate-50 text-slate-600 text-xs uppercase tracking-wider">
<th className="p-4 border-b">Foto</th>
<th className="p-4 border-b">Nama</th>
<th className="p-4 border-b">Email</th>
<th className="p-4 border-b">Institusi</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{filteredPeserta.length > 0 ? filteredPeserta.map((p, i) => (
<tr key={i} className="hover:bg-slate-50">
<td className="p-4">
{p.Foto ? (
<img src={p.Foto} alt="Avatar" className="w-10 h-10 rounded-full object-cover border border-slate-200" />
) : (
<div className="w-10 h-10 rounded-full bg-slate-200 flex items-center justify-center">
<Users size={16} className="text-slate-400" />
</div>
)}
</td>
<td className="p-4 font-medium text-slate-800">{p['Nama Peserta']}</td>
<td className="p-4 text-slate-600">{p.Email}</td>
<td className="p-4 text-slate-600">{p.Institusi}</td>
</tr>
)) : (
<tr><td colSpan="4" className="p-10 text-center text-slate-400">Belum ada peserta yang terdaftar</td></tr>
)}
</tbody>
</table>
</div>
) : (
<div className="p-10 border border-dashed border-slate-200 rounded-xl text-center text-slate-400">
Pilih pelatihan di atas untuk melihat daftar peserta
</div>
)}
</div>
</div>
);
};
export default App;
Tidak ada komentar:
Posting Komentar