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')}
)} )}
); }