import React, { useState, useEffect, useRef } from 'react'; import { Link as LinkIcon, ShieldCheck, Copy, CheckCircle2, LayoutDashboard, LogOut, Trash2, ExternalLink, Loader2, AlertCircle, Printer, TrendingUp, Globe } from 'lucide-react'; /* * KONFIGURASI UTAMA * Ganti URL di bawah ini dengan URL Web App Google Apps Script Anda setelah di-deploy! */ const GAS_WEBAPP_URL = 'YOUR_GAS_WEBAPP_URL_HERE'; const DISPLAY_DOMAIN = 'madrasahku.sch.id'; const ADMIN_PASSWORD = 'sarjanasoft'; const ParticleBackground = () => { const canvasRef = useRef(null); useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext('2d'); let animationFrameId; let particles = []; const mouse = { x: null, y: null }; const resize = () => { canvas.width = window.innerWidth; canvas.height = window.innerHeight; initParticles(); }; const initParticles = () => { particles = []; const numParticles = Math.floor((window.innerWidth * window.innerHeight) / 15000); for (let i = 0; i < numParticles; i++) { particles.push({ x: Math.random() * canvas.width, y: Math.random() * canvas.height, vx: (Math.random() - 0.5) * 1.2, vy: (Math.random() - 0.5) * 1.2, radius: Math.random() * 2 + 1 }); } }; const handleMouseMove = (e) => { mouse.x = e.clientX; mouse.y = e.clientY; }; const handleMouseLeave = () => { mouse.x = null; mouse.y = null; }; window.addEventListener('resize', resize); window.addEventListener('mousemove', handleMouseMove); window.addEventListener('mouseleave', handleMouseLeave); resize(); const draw = () => { ctx.clearRect(0, 0, canvas.width, canvas.height); for (let i = 0; i < particles.length; i++) { let p = particles[i]; p.x += p.vx; p.y += p.vy; if (p.x < 0 || p.x > canvas.width) p.vx *= -1; if (p.y < 0 || p.y > canvas.height) p.vy *= -1; ctx.beginPath(); ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2); ctx.fillStyle = 'rgba(16, 185, 129, 0.5)'; ctx.fill(); for (let j = i + 1; j < particles.length; j++) { let p2 = particles[j]; let dx = p.x - p2.x; let dy = p.y - p2.y; let dist = Math.sqrt(dx*dx + dy*dy); if (dist < 120) { ctx.beginPath(); ctx.moveTo(p.x, p.y); ctx.lineTo(p2.x, p2.y); ctx.strokeStyle = `rgba(16, 185, 129, ${0.2 - (dist/120)*0.2})`; ctx.lineWidth = 1; ctx.stroke(); } } if (mouse.x != null && mouse.y != null) { let dx = p.x - mouse.x; let dy = p.y - mouse.y; let dist = Math.sqrt(dx*dx + dy*dy); if (dist < 180) { ctx.beginPath(); ctx.moveTo(p.x, p.y); ctx.lineTo(mouse.x, mouse.y); ctx.strokeStyle = `rgba(52, 211, 153, ${0.5 - (dist/180)*0.5})`; ctx.lineWidth = 1.5; ctx.stroke(); } } } animationFrameId = requestAnimationFrame(draw); }; draw(); return () => { window.removeEventListener('resize', resize); window.removeEventListener('mousemove', handleMouseMove); window.removeEventListener('mouseleave', handleMouseLeave); cancelAnimationFrame(animationFrameId); }; }, []); return ; }; export default function App() { const [links, setLinks] = useState([]); const [view, setView] = useState('landing'); const [isAdminLoggedIn, setIsAdminLoggedIn] = useState(false); const [showLoginModal, setShowLoginModal] = useState(false); const [longUrl, setLongUrl] = useState(''); const [customAlias, setCustomAlias] = useState(''); const [passwordInput, setPasswordInput] = useState(''); const [shortenedLink, setShortenedLink] = useState(null); const [captcha, setCaptcha] = useState({ num1: 0, num2: 0 }); const [captchaInput, setCaptchaInput] = useState(''); const [isLoading, setIsLoading] = useState(true); const [isSubmitting, setIsSubmitting] = useState(false); const [toast, setToast] = useState({ show: false, message: '', type: 'success' }); const [redirectData, setRedirectData] = useState(null); const getBaseUrl = () => window.location.origin + window.location.pathname + '#/'; const showToast = (message, type = 'success') => { setToast({ show: true, message, type }); setTimeout(() => setToast({ show: false, message: '', type: 'success' }), 3000); }; // Fetch data links dari Google Apps Script const fetchLinksFromGAS = async () => { setIsLoading(true); try { if (GAS_WEBAPP_URL === 'YOUR_GAS_WEBAPP_URL_HERE') { // Fallback Simulasi jika URL GAS belum diisi pengguna console.warn("GAS URL belum diatur. Menggunakan mode simulasi memori."); setIsLoading(false); return; } const response = await fetch(`${GAS_WEBAPP_URL}?action=getLinks`); const result = await response.json(); if (result.status === 'success') { // Urutkan dari yang terbaru const sortedData = result.data.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); setLinks(sortedData); } } catch (error) { console.error("Gagal mengambil data:", error); showToast("Koneksi ke server gagal", "error"); } finally { setIsLoading(false); } }; const generateCaptcha = () => { setCaptcha({ num1: Math.floor(Math.random() * 15) + 1, // Angka acak 1-15 num2: Math.floor(Math.random() * 10) + 1 // Angka acak 1-10 }); setCaptchaInput(''); }; useEffect(() => { fetchLinksFromGAS(); generateCaptcha(); // Load soal pertama kali aplikasi dibuka }, []); useEffect(() => { const hash = window.location.hash; if (hash && hash.startsWith('#/') && hash.length > 2) { const code = hash.replace('#/', ''); setView('redirecting'); if (!isLoading) { const targetLink = links.find(l => l.shortUrl === code); if (targetLink) { setRedirectData(targetLink); processRedirect(targetLink); } else { setView('notfound'); } } } else if (view === 'redirecting' || view === 'notfound') { setView('landing'); } }, [window.location.hash, isLoading, links]); const processRedirect = async (targetLink) => { // Jalankan redirect visual setTimeout(() => { window.location.href = targetLink.originalUrl; }, 1500); // Kirim request background ke GAS untuk menambah statistik klik if (GAS_WEBAPP_URL !== 'YOUR_GAS_WEBAPP_URL_HERE') { try { await fetch(GAS_WEBAPP_URL, { method: 'POST', headers: { 'Content-Type': 'text/plain' }, // Hindari CORS Preflight body: JSON.stringify({ action: 'incrementClick', shortUrl: targetLink.shortUrl }) }); } catch (e) { console.error("Gagal mencatat klik", e); } } }; const handleShorten = async (e) => { e.preventDefault(); if (parseInt(captchaInput) !== captcha.num1 + captcha.num2) { showToast('Jawaban keamanan matematika salah!', 'error'); generateCaptcha(); // Reset soal agar bot tidak bisa mencoba (brute-force) jawaban berulang-ulang return; } if (!longUrl) return showToast('Masukkan URL terlebih dahulu', 'error'); let formattedUrl = longUrl; if (!formattedUrl.match(/^[a-zA-Z]+:\/\//)) formattedUrl = 'https://' + formattedUrl; try { new URL(formattedUrl); } catch (err) { return showToast('Format URL tidak valid', 'error'); } setIsSubmitting(true); let finalAlias = customAlias.trim() || Math.random().toString(36).substring(2, 8); if (!/^[a-zA-Z0-9-]+$/.test(finalAlias)) { setIsSubmitting(false); return showToast('Alias hanya boleh berisi huruf, angka, dan strip (-)', 'error'); } if (links.some(l => l.shortUrl.toLowerCase() === finalAlias.toLowerCase())) { setIsSubmitting(false); return showToast('Alias sudah digunakan, pilih yang lain', 'error'); } const newLinkData = { shortUrl: finalAlias, originalUrl: formattedUrl, clicks: 0, createdAt: new Date().toISOString() }; try { if (GAS_WEBAPP_URL !== 'YOUR_GAS_WEBAPP_URL_HERE') { // Kirim ke GAS (Untuk disave ke RTDB & Sheet) const response = await fetch(GAS_WEBAPP_URL, { method: 'POST', headers: { 'Content-Type': 'text/plain' }, body: JSON.stringify({ action: 'addLink', data: newLinkData }) }); const result = await response.json(); if (result.status !== 'success') throw new Error("Gagal menyimpan di server"); } // Update state lokal untuk responsibilitas UI setLinks(prev => [newLinkData, ...prev]); setShortenedLink({ ...newLinkData, fullUrl: `${getBaseUrl()}${finalAlias}`, displayUrl: `${DISPLAY_DOMAIN}/${finalAlias}` }); setLongUrl(''); setCustomAlias(''); generateCaptcha(); // Reset soal setelah berhasil membuat shortlink showToast('URL berhasil dipendekkan!'); } catch (error) { showToast('Terjadi kesalahan sistem', 'error'); } finally { setIsSubmitting(false); } }; const handleDeleteLink = async (shortUrl) => { if (!confirm('Anda yakin ingin menghapus tautan ini?')) return; try { if (GAS_WEBAPP_URL !== 'YOUR_GAS_WEBAPP_URL_HERE') { await fetch(GAS_WEBAPP_URL, { method: 'POST', headers: { 'Content-Type': 'text/plain' }, body: JSON.stringify({ action: 'deleteLink', shortUrl }) }); } setLinks(prev => prev.filter(l => l.shortUrl !== shortUrl)); showToast('Tautan berhasil dihapus'); } catch (err) { showToast('Gagal menghapus tautan', 'error'); } }; const copyToClipboard = (text) => { navigator.clipboard.writeText(text) .then(() => showToast('Tautan berhasil disalin!')) .catch(() => showToast('Gagal menyalin tautan', 'error')); }; if (view === 'redirecting') { return (

Mengalihkan...

Anda sedang diarahkan ke tautan tujuan.

{redirectData && (
Menuju: {redirectData.originalUrl}
)}
); } if (view === 'notfound') { return (

Tautan Tidak Ditemukan

Maaf, tautan yang Anda cari tidak ada atau telah dihapus oleh Admin.

); } return (
{toast.show && (
{toast.type === 'error' ? : } {toast.message}
)} {showLoginModal && (

Admin Login

Area khusus pengelola madrasah

{ e.preventDefault(); if (passwordInput === ADMIN_PASSWORD) { setIsAdminLoggedIn(true); setShowLoginModal(false); setView('admin'); setPasswordInput(''); showToast('Login berhasil!'); } else { showToast('Password salah!', 'error'); } }} className="p-6">
setPasswordInput(e.target.value)} className="w-full px-4 py-3 rounded-lg border border-slate-300 focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500 outline-none transition-all" placeholder="Masukkan password..." required />
)}
setView('landing')}>

ShortLink

MADRASAH HEBAT BERMARTABAT

{isLoading ? (
) : ( <> {view === 'landing' && (

Perpendek Tautan Anda

Buat tautan panjang menjadi ringkas, rapi, dan mudah diingat. Resmi untuk ekosistem {DISPLAY_DOMAIN}.

setLongUrl(e.target.value)} placeholder="https://docs.google.com/forms/..." className="w-full pl-11 pr-4 py-4 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-emerald-500 outline-none transition-all text-slate-700" />
{DISPLAY_DOMAIN}/ setCustomAlias(e.target.value.replace(/[^a-zA-Z0-9-]/g, ''))} placeholder="rapat-komite" className="flex-1 min-w-0 block w-full px-4 py-4 bg-transparent outline-none text-slate-700" />
{}
setCaptchaInput(e.target.value)} placeholder="Hasil" className="w-full px-4 py-4 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-emerald-500 outline-none transition-all text-slate-700 font-bold text-center text-xl shadow-inner" />
{shortenedLink && (

Tautan Berhasil Dibuat!

)}
{links.length}
Total Tautan
{links.reduce((acc, curr) => acc + (curr.clicks || 0), 0)}
Total Klik
)} {view === 'admin' && isAdminLoggedIn && (

Dashboard Admin

Kelola tautan dan integrasi database Anda.

Total Tautan

{links.length}

Total Klik Semua

{links.reduce((acc, curr) => acc + (curr.clicks || 0), 0)}

Status Server GAS

{GAS_WEBAPP_URL !== 'https://script.google.com/macros/s/AKfycbxA76x9Yb1uI_d146ojj8jpcunJiJxMsgc0CIJM5NGMz1z3DGjWUAM0z-WI_eB4MGWubg/exec' ? 'Terhubung (Online)' : 'Mode Simulasi Lokal'}

Daftar Tautan Tersimpan

{links.length === 0 ? ( ) : ( links.map((link) => ( )) )}
Tautan Pendek URL Tujuan Asli Klik Dibuat Aksi
Belum ada tautan.
{DISPLAY_DOMAIN}/{link.shortUrl}
{link.originalUrl}
{link.clicks || 0} {new Date(link.createdAt).toLocaleDateString('id-ID')}
)} )}

© {new Date().getFullYear()} ShortLink Madrasah. Dikembangkan oleh Web Builder Pro-Sarjanasoft.

); } Madrasah Shortlink Perpustakaan Digital

MEMUAT SISTEM...

Perpustakaan Digital

Mari budayakan literasi di madrasah.

Sambutan kepala madrasah akan tampil di sini.

Nama Kepala
Kepala Madrasah

0

Koleksi

0

Anggota

0

Dibaca
Koleksi Terbaru
Eksplorasi Kategori

Portal Masuk

Silakan masuk untuk mengakses fitur penuh perpustakaan.

MODE PREVIEW AKTIF
Akses Admin: admin / edudigital
Akses Peserta: peserta / edudigital
?+?=

Hubungi petugas Perpustakaan untuk pendaftaran akun.

Filter Periode Laporan

0

Koleksi Buku

0

Anggota Aktif

0

Buku Dibaca
Koleksi per Kategori
10 Buku Terfavorit

0

Koleksi Tersedia

0

Riwayat Bacamu
Rekomendasi Spesial
Jelajahi Koleksi
Jejak Literasi Anda
Kelola Koleksi Buku
Export Laporan

MADRASAH

PERPUSTAKAAN DIGITAL
ALAMAT
LAPORAN RIWAYAT PEMINJAMAN BUKU DIGITAL

NoWaktu AksesNama AnggotaKelasJudul Buku

PETUGAS

Petugas Perpustakaan

Konfigurasi Sistem
Sinkronisasi Database Drive
Tarik file PDF otomatis dari folder Drive.
Identitas Madrasah
Profil Pimpinan
ID Folder Google Drive

Pastikan folder di-set "Anyone with the link can view".

Kecerdasan Buatan (AI)
Sistem Perpustakaan

Indonesia

Sistem Informasi Perpustakaan Terintegrasi © 2026