import React, { useState, useEffect, useMemo } from 'react';
import { initializeApp } from 'firebase/app';
import { getAuth, signInAnonymously, onAuthStateChanged, signInWithCustomToken } from 'firebase/auth';
import { getFirestore, doc, onSnapshot, setDoc } from 'firebase/firestore';
import { Home, Settings, Unlock, Lock, Trash, WifiOff, Upload, ArrowLeft } from 'lucide-react';
// --- PENGATURAN DASAR & STYLING ---
const FontStyles = () => (
);
// Firebase Initialization
const firebaseConfig = typeof __firebase_config !== 'undefined' && __firebase_config ? JSON.parse(__firebase_config) : {
apiKey: "mock-key", authDomain: "mock-domain", projectId: "mock-id"
};
const app = initializeApp(firebaseConfig);
const auth = getAuth(app);
const db = getFirestore(app);
const APP_ID = typeof __app_id !== 'undefined' ? __app_id : 'lks-portal-2026';
// --- DATA BAWAAN (DEFAULT) ---
const defaultData = {
docLink: '#',
spreadsheetLink: '#',
schedule: [
{ id: '1', activity: 'Loading in Peralatan & Pengecekan', time: '2026-04-05T09:00:00', endTime: '2026-04-05T15:00:00', duration: 360, pic: 'PANITIA' },
{ id: '2', activity: 'Registrasi, Persiapan (C1)', time: '2026-04-06T07:15:00', endTime: '2026-04-06T07:45:00', duration: 30, pic: 'PANITIA' },
{ id: '3', activity: 'Briefing & Tanya Jawab', time: '2026-04-06T07:45:00', endTime: '2026-04-06T08:15:00', duration: 30, pic: 'TIM JURI' },
{ id: '4', activity: 'Pengerjaan Modul 1', time: '2026-04-06T08:15:00', endTime: '2026-04-06T11:15:00', duration: 180, pic: '-' },
{ id: '5', activity: 'Pengumpulan Modul 1', time: '2026-04-06T11:15:00', endTime: '2026-04-06T11:30:00', duration: 15, pic: 'PANITIA' },
{ id: '6', activity: 'ISHOMA', time: '2026-04-06T11:30:00', endTime: '2026-04-06T12:30:00', duration: 60, pic: '-' },
{ id: '7', activity: 'Briefing & Tanya Jawab', time: '2026-04-06T12:30:00', endTime: '2026-04-06T13:00:00', duration: 30, pic: 'TIM JURI' },
{ id: '8', activity: 'Pengerjaan Modul 2', time: '2026-04-06T13:00:00', endTime: '2026-04-06T16:00:00', duration: 180, pic: '-' },
{ id: '9', activity: 'Pengumpulan Modul 2', time: '2026-04-06T16:00:00', endTime: '2026-04-06T16:15:00', duration: 15, pic: 'PANITIA' },
{ id: '10', activity: 'Peserta Pulang', time: '2026-04-06T16:15:00', endTime: '2026-04-06T16:45:00', duration: 30, pic: '-' },
{ id: '11', activity: 'Penilaian / Marking', time: '2026-04-06T16:45:00', endTime: '2026-04-06T19:45:00', duration: 180, pic: 'TIM JURI' },
{ id: '12', activity: 'Registrasi, Persiapan (C2)', time: '2026-04-07T07:15:00', endTime: '2026-04-07T08:15:00', duration: 60, pic: 'PANITIA' },
{ id: '13', activity: 'Briefing & Tanya Jawab', time: '2026-04-07T08:15:00', endTime: '2026-04-07T08:45:00', duration: 30, pic: 'TIM JURI' },
{ id: '14', activity: 'Pengerjaan Modul 3', time: '2026-04-07T08:30:00', endTime: '2026-04-07T11:30:00', duration: 180, pic: '-' },
{ id: '15', activity: 'Pengumpulan Modul 3', time: '2026-04-07T11:30:00', endTime: '2026-04-07T11:45:00', duration: 15, pic: 'PANITIA' },
{ id: '16', activity: 'ISHOMA', time: '2026-04-07T11:45:00', endTime: '2026-04-07T12:45:00', duration: 60, pic: '-' },
{ id: '17', activity: 'Briefing & Tanya Jawab', time: '2026-04-07T12:45:00', endTime: '2026-04-07T13:15:00', duration: 30, pic: 'TIM JURI' },
{ id: '18', activity: 'Pengerjaan Modul 4', time: '2026-04-07T13:15:00', endTime: '2026-04-07T16:15:00', duration: 180, pic: '-' },
{ id: '19', activity: 'Pengumpulan Modul 4', time: '2026-04-07T16:15:00', endTime: '2026-04-07T16:30:00', duration: 15, pic: 'PANITIA' },
{ id: '20', activity: 'Mengemas Alat & Pulang', time: '2026-04-07T16:30:00', endTime: '2026-04-07T17:00:00', duration: 30, pic: '-' },
{ id: '21', activity: 'Penilaian / Marking', time: '2026-04-07T17:00:00', endTime: '2026-04-07T19:30:00', duration: 150, pic: 'TIM JURI' },
{ id: '22', activity: 'Penilaian / Marking (C3)', time: '2026-04-08T08:00:00', endTime: '2026-04-08T18:00:00', duration: 600, pic: 'TIM JURI' },
{ id: '23', activity: 'Pengumuman (C4)', time: '2026-04-09T08:00:00', endTime: '2026-04-09T18:00:00', duration: 600, pic: 'PANITIA' }
],
modules: [{ id: 'm1', title: 'Modul LKS Desain Grafis', link: 'https://drive.google.com/drive/folders/1DDpTLmTJQOQDOAYyIuZ63KdaDFl_tTGD?usp=sharing', releaseTime: '2026-04-05T08:00:00' }],
submissions: [{ id: 's1', title: 'Folder Pengumpulan Tugas', link: '#', releaseTime: '2026-04-06T11:15:00' }]
};
export default function App() {
const [user, setUser] = useState(null);
const [appData, setAppData] = useState(defaultData);
const [currentTime, setCurrentTime] = useState(new Date());
const [currentView, setCurrentView] = useState('dashboard');
const [isAdminAuth, setIsAdminAuth] = useState(false);
const [dbError, setDbError] = useState(false);
const [adminTab, setAdminTab] = useState('jadwal'); // Default admin tab
// State untuk Modal Login
const [showLoginModal, setShowLoginModal] = useState(false);
const [passwordInput, setPasswordInput] = useState('');
const [loginError, setLoginError] = useState(false);
// --- DATABASE SYNC ---
useEffect(() => {
const initAuth = async () => {
try {
if (typeof __initial_auth_token !== 'undefined' && __initial_auth_token) {
await signInWithCustomToken(auth, __initial_auth_token);
} else {
await signInAnonymously(auth);
}
} catch (err) {
setDbError(true);
}
};
initAuth();
const unsubscribe = onAuthStateChanged(auth, setUser);
return () => unsubscribe();
}, []);
useEffect(() => {
if (!user) return;
const docRef = doc(db, 'artifacts', APP_ID, 'public', 'data', 'settings', 'config');
const unsubscribe = onSnapshot(docRef, (snap) => {
if (snap.exists()) {
const data = snap.data();
setAppData({
docLink: data.docLink || defaultData.docLink,
schedule: data.schedule || defaultData.schedule,
modules: data.modules || defaultData.modules,
submissions: data.submissions || defaultData.submissions
});
} else {
saveToCloud(defaultData);
}
}, () => setDbError(true));
return () => unsubscribe();
}, [user]);
useEffect(() => {
const timer = setInterval(() => setCurrentTime(new Date()), 1000);
return () => clearInterval(timer);
}, []);
const saveToCloud = async (newData) => {
if (!user) return;
try {
await setDoc(doc(db, 'artifacts', APP_ID, 'public', 'data', 'settings', 'config'), newData);
} catch (error) {
console.error("Error saving:", error);
}
};
// --- LOGIKA UTAMA VIEW ---
const agendaStatus = useMemo(() => {
const now = currentTime.getTime();
let ongoing = null;
let next = null;
const sortedSchedule = [...appData.schedule].sort((a, b) => new Date(a.time) - new Date(b.time));
for (let item of sortedSchedule) {
const start = new Date(item.time).getTime();
const end = new Date(item.endTime).getTime();
if (now >= start && now < end) {
ongoing = item;
break;
} else if (now < start && !next) {
next = item;
}
}
if (ongoing) return { type: 'ONGOING', title: ongoing.activity, target: new Date(ongoing.endTime) };
if (next) return { type: 'NEXT', title: next.activity, target: new Date(next.time) };
return { type: 'DONE', title: 'Kompetisi Selesai', target: null };
}, [appData.schedule, currentTime]);
const renderCountdown = (targetDate) => {
if (!targetDate) return { h: '00', m: '00', s: '00' };
const diff = targetDate.getTime() - currentTime.getTime();
if (diff <= 0) return { h: '00', m: '00', s: '00' };
const hours = Math.floor(diff / (1000 * 60 * 60));
const minutes = Math.floor((diff / (1000 * 60)) % 60);
const seconds = Math.floor((diff / 1000) % 60);
return {
h: String(hours).padStart(2, '0'),
m: String(minutes).padStart(2, '0'),
s: String(seconds).padStart(2, '0')
};
};
const timerVals = renderCountdown(agendaStatus.target);
const handleAdminLogin = () => {
if (isAdminAuth) {
setCurrentView('admin');
} else {
setShowLoginModal(true);
setPasswordInput('');
setLoginError(false);
}
};
const submitLogin = () => {
if (passwordInput === 'roomjuri') {
setIsAdminAuth(true);
setCurrentView('admin');
setShowLoginModal(false);
} else {
setLoginError(true);
}
};
// --- LOGIKA ADMIN PANEL ---
const handleUpdateSetting = (key, value) => {
const newData = { ...appData, [key]: value };
setAppData(newData);
saveToCloud(newData);
};
const handleAddListItem = (type) => {
const id = Date.now().toString();
const newItem = { id, title: 'Judul Baru', link: '', releaseTime: new Date().toISOString().slice(0,16) };
const newData = { ...appData, [type]: [...appData[type], newItem] };
setAppData(newData);
saveToCloud(newData);
};
const handleDeleteListItem = (type, id) => {
const newData = { ...appData, [type]: appData[type].filter(i => i.id !== id) };
setAppData(newData);
saveToCloud(newData);
};
const handleUpdateListItem = (type, id, field, value) => {
const newData = {
...appData,
[type]: appData[type].map(i => i.id === id ? { ...i, [field]: value } : i)
};
setAppData(newData);
saveToCloud(newData);
};
const handleScheduleChange = (id, field, value) => {
let updatedSchedule = [...appData.schedule];
const index = updatedSchedule.findIndex(s => s.id === id);
if (index === -1) return;
if (field === 'duration') {
updatedSchedule[index].duration = parseInt(value) || 0;
} else if (field === 'startTimeUpdate') {
const datePart = updatedSchedule[index].time.split('T')[0];
updatedSchedule[index].time = `${datePart}T${value}:00`;
} else {
updatedSchedule[index][field] = value;
}
const currentItem = updatedSchedule[index];
const itemDate = currentItem.time.split('T')[0];
let dayItems = updatedSchedule.filter(s => s.time.startsWith(itemDate)).sort((a,b) => new Date(a.time) - new Date(b.time));
let previousEndTime = null;
dayItems = dayItems.map((item, idx) => {
let startTimeObj;
if (idx > 0 && previousEndTime) {
startTimeObj = new Date(previousEndTime);
item.time = startTimeObj.toISOString().slice(0, 19);
} else {
startTimeObj = new Date(item.time);
}
const endTimeObj = new Date(startTimeObj.getTime() + item.duration * 60000);
item.endTime = endTimeObj.toISOString().slice(0, 19);
previousEndTime = item.endTime;
return item;
});
updatedSchedule = updatedSchedule.map(s => {
const dayItem = dayItems.find(d => d.id === s.id);
return dayItem ? dayItem : s;
});
const newData = { ...appData, schedule: updatedSchedule };
setAppData(newData);
saveToCloud(newData);
};
// --- RENDER UI ---
if (dbError) {
return (
Koneksi Database Terputus
);
}
return (
{/* HEADER: Dibuat menyatu dan tersembunyi, hanya menampilkan ikon navigasi */}
{/* Modal Login Juri */}
{showLoginModal && (
Room Juri
Otorisasi panitia diperlukan.
setPasswordInput(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && submitLogin()}
className="w-full p-4 border-2 border-white bg-black text-white rounded-none mb-2 outline-none font-mono focus:bg-gray-900"
/>
{loginError ? (
Akses ditolak.
) : (
)}
setShowLoginModal(false)} className="px-4 py-2 font-bold text-gray-400 hover:text-white transition">BATAL
MASUK
)}
{/* BODY CONTENT */}
{/* VIEW: DASHBOARD */}
{currentView === 'dashboard' && (
<>
{/* Modul Waktu Countdown - Grid System Kuat */}
{/* Indikator Status & Judul Kegiatan (Elegan di Bawah Countdown) */}
{agendaStatus.type === 'ONGOING' ? 'Sedang Berjalan:' : agendaStatus.type === 'NEXT' ? 'Menuju:' : ''} {agendaStatus.title}
{/* Tombol Link - Grid Kotak Putih */}
setCurrentView('modules')} className="block w-full bg-white text-black text-center py-6 font-extrabold text-sm md:text-lg rounded-none hover:bg-gray-300 transition-colors">
AKSES MODUL
setCurrentView('submissions')} className="block w-full bg-white text-black text-center py-6 font-extrabold text-sm md:text-lg rounded-none hover:bg-gray-300 transition-colors">
PENGUMPULAN
setCurrentView('schedule')} className="block w-full bg-white text-black text-center py-6 font-extrabold text-sm md:text-lg rounded-none hover:bg-gray-300 transition-colors">
JADWAL
DOKUMENTASI
>
)}
{/* VIEW: MODULES & SUBMISSIONS (TIME-LOCKED) */}
{(currentView === 'modules' || currentView === 'submissions') && (
{currentView === 'modules' ? 'MODUL LOMBA' : 'PENGUMPULAN TUGAS'}
{appData[currentView].map(item => {
const releaseDate = new Date(item.releaseTime);
const isLocked = currentTime < releaseDate;
return (
);
})}
)}
{/* VIEW: SCHEDULE */}
{currentView === 'schedule' && (
JADWAL LKS
{Object.entries(
appData.schedule.reduce((acc, curr) => {
const date = curr.time.split('T')[0];
if (!acc[date]) acc[date] = [];
acc[date].push(curr);
return acc;
}, {})
).map(([date, items]) => (
{new Date(date).toLocaleDateString('id-ID', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })}
{items.sort((a,b) => new Date(a.time) - new Date(b.time)).map(item => {
const isOngoing = currentTime >= new Date(item.time) && currentTime < new Date(item.endTime);
return (
{new Date(item.time).toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit' })}
{item.activity}
{item.pic}
);
})}
))}
)}
{/* VIEW: ADMIN PANEL */}
{currentView === 'admin' && isAdminAuth && (
{['jadwal', 'modul', 'pengumpulan', 'pengaturan'].map(tab => (
setAdminTab(tab)}
className={`px-8 py-5 font-extrabold text-sm uppercase tracking-widest whitespace-nowrap transition-colors ${adminTab === tab ? 'bg-white text-black' : 'text-gray-500 hover:text-white'}`}
>
{tab}
))}
{/* Tab: Pengaturan */}
{adminTab === 'pengaturan' && (
)}
{/* Tab: Jadwal (Auto Recalculate) */}
{adminTab === 'jadwal' && (
INSTRUKSI: Untuk menggeser jadwal (overtime), ubah Waktu Mulai pada sesi paling pertama di hari tersebut, atau ubah Durasi (dalam menit). Jadwal di bawahnya akan bergeser secara otomatis.
{Object.entries(
appData.schedule.reduce((acc, curr) => {
const date = curr.time.split('T')[0];
if (!acc[date]) acc[date] = [];
acc[date].push(curr);
return acc;
}, {})
).map(([date, items]) => {
const sortedItems = items.sort((a,b) => new Date(a.time) - new Date(b.time));
return (
{new Date(date).toLocaleDateString('id-ID', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })}
{sortedItems.map((item, index) => (
))}
)})}
)}
{/* Tab: Modul & Pengumpulan */}
{(adminTab === 'modul' || adminTab === 'pengumpulan') && (
handleAddListItem(adminTab === 'modul' ? 'modules' : 'submissions')} className="bg-white text-black px-6 py-4 font-extrabold text-sm uppercase tracking-widest hover:bg-gray-300 transition">
+ TAMBAH {adminTab === 'modul' ? 'MODUL' : 'LINK'}
{appData[adminTab === 'modul' ? 'modules' : 'submissions'].map(item => (
Judul
handleUpdateListItem(adminTab === 'modul' ? 'modules' : 'submissions', item.id, 'title', e.target.value)} className="w-full p-3 border-2 border-white bg-black text-white outline-none font-bold focus:bg-gray-900" />
Link (G-Drive dll)
handleUpdateListItem(adminTab === 'modul' ? 'modules' : 'submissions', item.id, 'link', e.target.value)} className="w-full p-3 border-2 border-white bg-black text-white outline-none font-mono text-sm focus:bg-gray-900" />
Waktu Rilis
handleUpdateListItem(adminTab === 'modul' ? 'modules' : 'submissions', item.id, 'releaseTime', e.target.value)} className="w-full p-3 border-2 border-white bg-black text-white outline-none font-mono text-sm focus:bg-gray-900" />
handleDeleteListItem(adminTab === 'modul' ? 'modules' : 'submissions', item.id)} className="p-4 text-white bg-red-600 hover:bg-red-700 transition flex-shrink-0 border-2 border-red-600">
))}
)}
)}
);
}