610 lines
27 KiB
TypeScript
Executable File
610 lines
27 KiB
TypeScript
Executable File
|
|
import React, { useState, useEffect } from 'react';
|
|
import { LayoutDashboard, PlusCircle, Settings, Menu, Wifi, Printer, Save, Upload, Image as ImageIcon, ClipboardList, LogOut, UserCircle, Edit } from 'lucide-react';
|
|
import Dashboard from './Dashboard';
|
|
import JournalForm from './JournalForm';
|
|
import ClassJournalForm from './ClassJournalForm';
|
|
import RecapView from './RecapView';
|
|
import LoginView from './LoginView';
|
|
import { fetchData, calculateStats, getApiUrl, setApiUrl, saveSettings } from '../services/apiService';
|
|
import { ViewState, DashboardStats, AppData, SchoolSettings, AuthUser } from '../types';
|
|
import { DEFAULT_LOGO, DEFAULT_KOP } from '../assets/defaultImages';
|
|
|
|
// Utility to format Google Drive links for display
|
|
const formatGoogleDriveImageUrl = (url: string) => {
|
|
if (!url) return '';
|
|
if (url.startsWith('data:image')) return url;
|
|
try {
|
|
const idMatch = url.match(/(?:id=|\/d\/)([\w-]+)/);
|
|
if (idMatch && idMatch[1]) {
|
|
return `https://lh3.googleusercontent.com/d/${idMatch[1]}`;
|
|
}
|
|
return url;
|
|
} catch (e) {
|
|
return url;
|
|
}
|
|
};
|
|
|
|
const App: React.FC = () => {
|
|
const [view, setView] = useState<ViewState>(ViewState.DASHBOARD);
|
|
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
|
const [data, setData] = useState<AppData>({ entries: [], classEntries: [], students: [], teachers: [], subjects: [], settings: { semester: '', academicYear: '', headmasterName: '', headmasterNip: '', logoUrl: '', kopUrl: '' } });
|
|
const [stats, setStats] = useState<DashboardStats>({ totalEntries: 0, totalStudents: 0, totalTeachers: 0, weeklyActivity: [], classPerformance: [] });
|
|
const [loading, setLoading] = useState(true);
|
|
const [sheetUrl, setSheetUrl] = useState(getApiUrl());
|
|
|
|
// Auth State
|
|
const [currentUser, setCurrentUser] = useState<AuthUser | null>(null);
|
|
|
|
// Settings Form State
|
|
const [formSettings, setFormSettings] = useState<SchoolSettings>({
|
|
semester: '', academicYear: '', headmasterName: '', headmasterNip: '', logoUrl: '', kopUrl: ''
|
|
});
|
|
const [isSavingSettings, setIsSavingSettings] = useState(false);
|
|
|
|
// Edit Mode States for Settings
|
|
const [editLogo, setEditLogo] = useState(false);
|
|
const [editKop, setEditKop] = useState(false);
|
|
|
|
// Initial Load
|
|
useEffect(() => {
|
|
loadData();
|
|
}, []);
|
|
|
|
// Sync form settings when data loads
|
|
useEffect(() => {
|
|
if (data.settings) {
|
|
setFormSettings({
|
|
...data.settings,
|
|
logoUrl: data.settings.logoUrl || DEFAULT_LOGO,
|
|
kopUrl: data.settings.kopUrl || DEFAULT_KOP
|
|
});
|
|
}
|
|
}, [data.settings]);
|
|
|
|
const loadData = async () => {
|
|
setLoading(true);
|
|
const fetchedData = await fetchData();
|
|
|
|
// Ensure defaults if empty
|
|
if (!fetchedData.settings.logoUrl) fetchedData.settings.logoUrl = DEFAULT_LOGO;
|
|
if (!fetchedData.settings.kopUrl) fetchedData.settings.kopUrl = DEFAULT_KOP;
|
|
|
|
setData(fetchedData);
|
|
setStats(calculateStats(fetchedData.entries, fetchedData.students, fetchedData.teachers));
|
|
setLoading(false);
|
|
};
|
|
|
|
const handleLogin = (user: AuthUser) => {
|
|
setCurrentUser(user);
|
|
// Set default view based on role
|
|
if (user.role === 'SEKRETARIS') {
|
|
setView(ViewState.CLASS_JOURNAL_FORM);
|
|
} else {
|
|
setView(ViewState.DASHBOARD);
|
|
}
|
|
};
|
|
|
|
const handleLogout = () => {
|
|
if (window.confirm('Apakah Anda yakin ingin keluar dari aplikasi?')) {
|
|
// Force reset state
|
|
setCurrentUser(null);
|
|
setIsSidebarOpen(false);
|
|
setView(ViewState.DASHBOARD);
|
|
|
|
// Hard reload to ensure clean state and return to login
|
|
window.location.reload();
|
|
}
|
|
};
|
|
|
|
const handleConnectApi = (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setApiUrl(sheetUrl);
|
|
alert('URL API disimpan. Memuat ulang data...');
|
|
loadData();
|
|
};
|
|
|
|
const handleSaveSchoolSettings = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setIsSavingSettings(true);
|
|
const success = await saveSettings(formSettings);
|
|
if (success) {
|
|
alert('Pengaturan berhasil disimpan! Perubahan akan terlihat setelah refresh data.');
|
|
setData(prev => ({ ...prev, settings: formSettings }));
|
|
// Reset edit modes
|
|
setEditLogo(false);
|
|
setEditKop(false);
|
|
} else {
|
|
alert('Gagal menyimpan pengaturan. Pastikan internet lancar dan Script Google Sheet mendukung penyimpanan.');
|
|
}
|
|
setIsSavingSettings(false);
|
|
};
|
|
|
|
const handleSettingChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
|
setFormSettings({ ...formSettings, [e.target.name]: e.target.value });
|
|
};
|
|
|
|
// Helper function to compress image
|
|
const compressImage = (file: File, maxWidth: number): Promise<string> => {
|
|
return new Promise((resolve) => {
|
|
const reader = new FileReader();
|
|
reader.readAsDataURL(file);
|
|
reader.onload = (event) => {
|
|
const img = new Image();
|
|
img.src = event.target?.result as string;
|
|
img.onload = () => {
|
|
const canvas = document.createElement('canvas');
|
|
let width = img.width;
|
|
let height = img.height;
|
|
|
|
if (width > maxWidth) {
|
|
height = (height * maxWidth) / width;
|
|
width = maxWidth;
|
|
}
|
|
|
|
canvas.width = width;
|
|
canvas.height = height;
|
|
const ctx = canvas.getContext('2d');
|
|
ctx?.drawImage(img, 0, 0, width, height);
|
|
|
|
// Compress to JPEG with 0.7 quality to save space
|
|
const dataUrl = canvas.toDataURL('image/jpeg', 0.7);
|
|
resolve(dataUrl);
|
|
};
|
|
};
|
|
});
|
|
};
|
|
|
|
const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>, field: 'logoUrl' | 'kopUrl') => {
|
|
const file = e.target.files?.[0];
|
|
if (file) {
|
|
// Validasi awal ukuran file (sebelum kompresi)
|
|
if (file.size > 2 * 1024 * 1024) { // 2MB limit for raw file
|
|
alert('File terlalu besar! Mohon pilih gambar di bawah 2MB.');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Determine max width based on type
|
|
// Logo smaller (200px), Kop wider (800px)
|
|
const maxWidth = field === 'logoUrl' ? 200 : 800;
|
|
|
|
const compressedBase64 = await compressImage(file, maxWidth);
|
|
|
|
setFormSettings(prev => ({ ...prev, [field]: compressedBase64 }));
|
|
} catch (error) {
|
|
console.error("Error compressing image", error);
|
|
alert("Gagal memproses gambar.");
|
|
}
|
|
}
|
|
};
|
|
|
|
// Filter Menus based on Role
|
|
const getNavItems = () => {
|
|
if (!currentUser) return [];
|
|
|
|
const allItems = [
|
|
{ id: ViewState.DASHBOARD, icon: LayoutDashboard, label: "Dashboard", roles: ['ADMIN', 'GURU'] },
|
|
{ id: ViewState.JOURNAL_FORM, icon: PlusCircle, label: "Input Jurnal Guru", roles: ['ADMIN', 'GURU'] },
|
|
{ id: ViewState.CLASS_JOURNAL_FORM, icon: ClipboardList, label: "Input Jurnal Kelas", roles: ['ADMIN', 'SEKRETARIS'] },
|
|
{ id: ViewState.RECAP, icon: Printer, label: "Rekapitulasi Laporan", roles: ['ADMIN', 'GURU', 'SEKRETARIS'] },
|
|
{ id: ViewState.SETTINGS, icon: Settings, label: "Pengaturan", roles: ['ADMIN'] },
|
|
];
|
|
|
|
return allItems.filter(item => item.roles.includes(currentUser.role));
|
|
};
|
|
|
|
// Show Loading State
|
|
if (loading) {
|
|
return (
|
|
<div className="min-h-screen bg-slate-50 flex flex-col items-center justify-center text-slate-400 gap-4">
|
|
<div className="animate-spin h-12 w-12 border-4 border-school-900 border-t-accent-yellow rounded-full"></div>
|
|
<p className="font-medium text-school-900 animate-pulse">Menyiapkan Aplikasi...</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Show Login View if not authenticated
|
|
if (!currentUser) {
|
|
return (
|
|
<LoginView
|
|
teachers={data.teachers}
|
|
students={data.students}
|
|
settings={data.settings}
|
|
onLogin={handleLogin}
|
|
/>
|
|
);
|
|
}
|
|
|
|
const navItems = getNavItems();
|
|
|
|
return (
|
|
<div className="min-h-screen bg-slate-50 flex font-sans text-slate-800">
|
|
|
|
{/* Mobile Overlay */}
|
|
{isSidebarOpen && (
|
|
<div
|
|
className="fixed inset-0 bg-black/50 z-40 md:hidden"
|
|
onClick={() => setIsSidebarOpen(false)}
|
|
/>
|
|
)}
|
|
|
|
{/* Sidebar */}
|
|
<aside
|
|
className={`fixed md:static inset-y-0 left-0 z-50 w-72 bg-school-900 text-white shadow-2xl transform transition-transform duration-300 ease-in-out flex flex-col ${isSidebarOpen ? 'translate-x-0' : '-translate-x-full md:translate-x-0'
|
|
}`}
|
|
>
|
|
<div className="p-6 flex flex-col items-center justify-center gap-4 border-b border-school-800 text-center">
|
|
<img
|
|
src={formatGoogleDriveImageUrl(data.settings.logoUrl || DEFAULT_LOGO)}
|
|
alt="Logo Sekolah"
|
|
className="h-24 w-24 object-contain bg-white rounded-2xl p-2 shadow-lg"
|
|
onError={(e) => e.currentTarget.style.display = 'none'}
|
|
/>
|
|
<div className="w-full">
|
|
<h1 className="font-heading font-bold text-lg tracking-tight leading-tight mb-1">Aplikasi Jurnal Kelas &<br />Mengajar</h1>
|
|
<p className="text-xs text-blue-200 font-medium">SMA Negeri 1 Abiansemal</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* User Profile Snippet */}
|
|
<div className="p-6 bg-school-800/50 flex items-center gap-3">
|
|
<div className="h-10 w-10 rounded-full bg-white/10 flex items-center justify-center">
|
|
<UserCircle className="text-accent-yellow" size={24} />
|
|
</div>
|
|
<div className="overflow-hidden">
|
|
<p className="text-sm font-bold text-white truncate">{currentUser.name}</p>
|
|
<p className="text-xs text-blue-200 uppercase font-semibold tracking-wider">{currentUser.role}</p>
|
|
{currentUser.className && <p className="text-[10px] text-accent-yellow">{currentUser.className}</p>}
|
|
</div>
|
|
</div>
|
|
|
|
<nav className="mt-2 space-y-1 flex-1 overflow-y-auto">
|
|
{navItems.map(item => {
|
|
const Icon = item.icon;
|
|
return (
|
|
<button
|
|
key={item.id}
|
|
type="button"
|
|
onClick={() => {
|
|
setView(item.id);
|
|
setIsSidebarOpen(false);
|
|
}}
|
|
className={`w-full flex items-center gap-3 px-6 py-4 transition-all ${view === item.id
|
|
? 'bg-school-800 text-accent-yellow border-r-4 border-accent-yellow'
|
|
: 'text-blue-100 hover:bg-school-800 hover:text-white'
|
|
}`}
|
|
>
|
|
<Icon size={20} />
|
|
<span className="font-medium">{item.label}</span>
|
|
</button>
|
|
)
|
|
})}
|
|
</nav>
|
|
|
|
<div className="p-4 border-t border-school-800">
|
|
<button
|
|
type="button"
|
|
onClick={handleLogout}
|
|
className="w-full flex items-center gap-3 px-4 py-3 rounded-xl text-red-300 hover:bg-red-600 hover:text-white transition-all font-bold group focus:outline-none focus:ring-2 focus:ring-red-500"
|
|
>
|
|
<LogOut size={20} className="group-hover:scale-110 transition-transform" />
|
|
<span>Logout</span>
|
|
</button>
|
|
</div>
|
|
|
|
<div className="p-6 bg-school-950">
|
|
<div className="bg-school-800 rounded-xl p-4 border border-school-700">
|
|
<p className="text-xs text-blue-200 mb-2 font-semibold uppercase tracking-wider">Koneksi Database</p>
|
|
<div className="flex items-center gap-2">
|
|
<div className="h-2 w-2 rounded-full bg-green-400 animate-pulse"></div>
|
|
<span className="text-xs font-bold text-white flex items-center gap-1">
|
|
<Wifi size={12} /> Terhubung ke Database
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
|
|
{/* Main Content */}
|
|
<main className="flex-1 flex flex-col h-screen overflow-hidden">
|
|
{/* Topbar Mobile */}
|
|
<header className="bg-white shadow-sm p-4 flex items-center justify-between md:hidden z-30">
|
|
<div className="flex items-center gap-2">
|
|
<img
|
|
src={formatGoogleDriveImageUrl(data.settings.logoUrl || DEFAULT_LOGO)}
|
|
className="h-8 w-8 object-contain"
|
|
alt="Logo"
|
|
/>
|
|
<div className="flex flex-col">
|
|
<span className="font-bold text-school-900 text-sm leading-tight">Aplikasi Jurnal</span>
|
|
<span className="text-xs text-slate-500">{currentUser.role} Mode</span>
|
|
</div>
|
|
</div>
|
|
<button onClick={() => setIsSidebarOpen(true)} className="text-slate-600">
|
|
<Menu size={24} />
|
|
</button>
|
|
</header>
|
|
|
|
{/* Content Area */}
|
|
<div className="flex-1 overflow-auto p-4 md:p-8 scroll-smooth">
|
|
<>
|
|
{view === ViewState.DASHBOARD && currentUser.role !== 'SEKRETARIS' && (
|
|
<Dashboard stats={stats} recentEntries={data.entries} />
|
|
)}
|
|
|
|
{view === ViewState.JOURNAL_FORM && currentUser.role !== 'SEKRETARIS' && (
|
|
<JournalForm
|
|
onSuccess={() => {
|
|
loadData();
|
|
setView(ViewState.DASHBOARD);
|
|
}}
|
|
students={data.students}
|
|
teachers={data.teachers}
|
|
subjects={data.subjects}
|
|
currentUser={currentUser}
|
|
/>
|
|
)}
|
|
|
|
{view === ViewState.CLASS_JOURNAL_FORM && (
|
|
<ClassJournalForm
|
|
currentUser={currentUser}
|
|
onSuccess={() => {
|
|
loadData();
|
|
// If Secretary, stay or maybe show recap? Just reload data for now.
|
|
}}
|
|
students={data.students}
|
|
teachers={data.teachers}
|
|
subjects={data.subjects}
|
|
/>
|
|
)}
|
|
|
|
{view === ViewState.RECAP && (
|
|
<RecapView
|
|
currentUser={currentUser}
|
|
entries={data.entries}
|
|
classEntries={data.classEntries}
|
|
teachers={data.teachers}
|
|
settings={data.settings}
|
|
onRefresh={loadData}
|
|
/>
|
|
)}
|
|
|
|
{view === ViewState.SETTINGS && currentUser.role === 'ADMIN' && (
|
|
<div className="max-w-4xl mx-auto space-y-8 pb-20">
|
|
|
|
{/* School Configuration Card */}
|
|
<div className="bg-white rounded-2xl shadow-lg border border-slate-100 overflow-hidden">
|
|
<div className="bg-school-900 p-6 text-white flex items-center gap-3">
|
|
<Settings size={24} className="text-accent-yellow" />
|
|
<div>
|
|
<h2 className="font-heading text-xl font-bold">Pengaturan Data Sekolah</h2>
|
|
<p className="text-blue-200 text-sm">Upload Logo dan Kop Surat untuk Laporan PDF</p>
|
|
</div>
|
|
</div>
|
|
|
|
<form onSubmit={handleSaveSchoolSettings} className="p-6 md:p-8 space-y-6">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-semibold text-slate-600">Semester</label>
|
|
<select
|
|
name="semester"
|
|
value={formSettings.semester}
|
|
onChange={handleSettingChange}
|
|
className="w-full px-4 py-3 rounded-xl border border-slate-200 focus:border-school-800 outline-none bg-white"
|
|
>
|
|
<option value="Ganjil">Ganjil</option>
|
|
<option value="Genap">Genap</option>
|
|
</select>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-semibold text-slate-600">Tahun Pelajaran</label>
|
|
<input
|
|
type="text"
|
|
name="academicYear"
|
|
value={formSettings.academicYear}
|
|
onChange={handleSettingChange}
|
|
placeholder="2024/2025"
|
|
className="w-full px-4 py-3 rounded-xl border border-slate-200 focus:border-school-800 outline-none"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-semibold text-slate-600">Nama Kepala Sekolah</label>
|
|
<input
|
|
type="text"
|
|
name="headmasterName"
|
|
value={formSettings.headmasterName}
|
|
onChange={handleSettingChange}
|
|
className="w-full px-4 py-3 rounded-xl border border-slate-200 focus:border-school-800 outline-none"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-semibold text-slate-600">NIP Kepala Sekolah</label>
|
|
<input
|
|
type="text"
|
|
name="headmasterNip"
|
|
value={formSettings.headmasterNip}
|
|
onChange={handleSettingChange}
|
|
className="w-full px-4 py-3 rounded-xl border border-slate-200 focus:border-school-800 outline-none"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 pt-4">
|
|
{/* Logo Upload Section */}
|
|
<div className="space-y-3 bg-slate-50 p-4 rounded-xl border border-slate-200">
|
|
<label className="text-sm font-bold text-slate-700 flex items-center gap-2">
|
|
<ImageIcon size={18} className="text-school-900" /> Logo Sekolah
|
|
</label>
|
|
|
|
<div className="flex flex-col items-center justify-center gap-4">
|
|
<div className="h-32 w-32 bg-white rounded-lg border-2 border-dashed border-slate-300 flex items-center justify-center overflow-hidden relative p-2">
|
|
{formSettings.logoUrl ? (
|
|
<img src={formatGoogleDriveImageUrl(formSettings.logoUrl)} alt="Logo Preview" className="w-full h-full object-contain" />
|
|
) : (
|
|
<span className="text-xs text-slate-400">No Logo</span>
|
|
)}
|
|
</div>
|
|
|
|
<div className="w-full flex flex-col items-center">
|
|
{!formSettings.logoUrl || editLogo ? (
|
|
<div className="w-full animate-fade-in">
|
|
<label htmlFor="logo-upload" className="cursor-pointer flex items-center justify-center gap-2 w-full py-2 bg-white border border-slate-300 rounded-lg text-sm font-bold text-slate-600 hover:bg-slate-50 transition shadow-sm">
|
|
<Upload size={14} /> Pilih Logo
|
|
</label>
|
|
<input
|
|
id="logo-upload"
|
|
type="file"
|
|
accept="image/*"
|
|
onChange={(e) => handleFileUpload(e, 'logoUrl')}
|
|
className="hidden"
|
|
/>
|
|
{/* Fallback Text Input */}
|
|
<input
|
|
type="text"
|
|
name="logoUrl"
|
|
value={formSettings.logoUrl}
|
|
onChange={handleSettingChange}
|
|
placeholder="Atau paste URL Google Drive..."
|
|
className="mt-2 w-full text-xs p-2 border border-slate-200 rounded focus:border-school-800 outline-none"
|
|
/>
|
|
{formSettings.logoUrl && (
|
|
<button
|
|
type="button"
|
|
onClick={() => setEditLogo(false)}
|
|
className="mt-2 text-xs text-red-500 hover:underline w-full text-center"
|
|
>
|
|
Batal Ubah
|
|
</button>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<button
|
|
type="button"
|
|
onClick={() => setEditLogo(true)}
|
|
className="flex items-center justify-center gap-2 w-full py-2 bg-white border border-slate-300 rounded-lg text-sm font-bold text-school-900 hover:bg-slate-50 transition shadow-sm"
|
|
>
|
|
<Edit size={14} /> Ubah Logo
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Kop Upload Section */}
|
|
<div className="space-y-3 bg-slate-50 p-4 rounded-xl border border-slate-200">
|
|
<label className="text-sm font-bold text-slate-700 flex items-center gap-2">
|
|
<ImageIcon size={18} className="text-school-900" /> Kop Surat (Header Laporan)
|
|
</label>
|
|
|
|
<div className="flex flex-col items-center justify-center gap-4">
|
|
<div className="h-32 w-full bg-white rounded-lg border-2 border-dashed border-slate-300 flex items-center justify-center overflow-hidden relative p-2">
|
|
{formSettings.kopUrl ? (
|
|
<img src={formatGoogleDriveImageUrl(formSettings.kopUrl)} alt="Kop Preview" className="w-full h-full object-contain" />
|
|
) : (
|
|
<span className="text-xs text-slate-400">No Header</span>
|
|
)}
|
|
</div>
|
|
|
|
<div className="w-full flex flex-col items-center">
|
|
{!formSettings.kopUrl || editKop ? (
|
|
<div className="w-full animate-fade-in">
|
|
<label htmlFor="kop-upload" className="cursor-pointer flex items-center justify-center gap-2 w-full py-2 bg-white border border-slate-300 rounded-lg text-sm font-bold text-slate-600 hover:bg-slate-50 transition shadow-sm">
|
|
<Upload size={14} /> Pilih Kop
|
|
</label>
|
|
<input
|
|
id="kop-upload"
|
|
type="file"
|
|
accept="image/*"
|
|
onChange={(e) => handleFileUpload(e, 'kopUrl')}
|
|
className="hidden"
|
|
/>
|
|
{/* Fallback Text Input */}
|
|
<input
|
|
type="text"
|
|
name="kopUrl"
|
|
value={formSettings.kopUrl}
|
|
onChange={handleSettingChange}
|
|
placeholder="Atau paste URL Google Drive..."
|
|
className="mt-2 w-full text-xs p-2 border border-slate-200 rounded focus:border-school-800 outline-none"
|
|
/>
|
|
{formSettings.kopUrl && (
|
|
<button
|
|
type="button"
|
|
onClick={() => setEditKop(false)}
|
|
className="mt-2 text-xs text-red-500 hover:underline w-full text-center"
|
|
>
|
|
Batal Ubah
|
|
</button>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<button
|
|
type="button"
|
|
onClick={() => setEditKop(true)}
|
|
className="flex items-center justify-center gap-2 w-full py-2 bg-white border border-slate-300 rounded-lg text-sm font-bold text-school-900 hover:bg-slate-50 transition shadow-sm"
|
|
>
|
|
<Edit size={14} /> Ubah Kop
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="pt-4 border-t border-slate-100">
|
|
<button
|
|
type="submit"
|
|
disabled={isSavingSettings}
|
|
className={`flex items-center justify-center gap-2 w-full md:w-auto px-8 py-3 rounded-xl font-bold text-school-900 bg-accent-yellow hover:bg-yellow-400 transition shadow-md ${isSavingSettings ? 'opacity-75 cursor-not-allowed' : ''}`}
|
|
>
|
|
<Save size={20} />
|
|
{isSavingSettings ? 'Menyimpan Perubahan...' : 'Simpan Pengaturan & Aset'}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
{/* API Configuration Card */}
|
|
<div className="bg-white rounded-2xl shadow-lg border border-slate-100 overflow-hidden">
|
|
<div className="p-6 md:p-8">
|
|
<h3 className="font-heading font-bold text-lg text-slate-800 mb-4 flex items-center gap-2">
|
|
<Wifi size={20} className="text-school-800" /> Konfigurasi API Endpoint
|
|
</h3>
|
|
<div className="bg-blue-50 border-l-4 border-school-900 p-4 mb-6 text-sm text-blue-900">
|
|
<p className="font-bold mb-1">Google Apps Script URL</p>
|
|
<p>Pastikan script Anda mendukung parameter <code>?op=getData</code> dan aksi <code>simpanJurnal</code>, <code>simpanJurnalKelas</code>, & <code>simpanSettings</code>.</p>
|
|
</div>
|
|
|
|
<form onSubmit={handleConnectApi} className="flex flex-col md:flex-row gap-4">
|
|
<input
|
|
type="url"
|
|
className="flex-1 px-4 py-3 rounded-xl border border-slate-300 focus:border-school-900 outline-none"
|
|
placeholder="https://script.google.com/macros/s/..."
|
|
value={sheetUrl}
|
|
onChange={(e) => setSheetUrl(e.target.value)}
|
|
/>
|
|
<button
|
|
type="submit"
|
|
className="bg-school-900 text-white px-6 py-3 rounded-xl font-bold hover:bg-school-800 transition shadow-lg"
|
|
>
|
|
Hubungkan
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
)}
|
|
</>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default App;
|