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
)}
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}.
{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
| Tautan Pendek |
URL Tujuan Asli |
Klik |
Dibuat |
Aksi |
{links.length === 0 ? (
| Belum ada tautan. |
) : (
links.map((link) => (
| {DISPLAY_DOMAIN}/{link.shortUrl} |
{link.originalUrl} |
{link.clicks || 0} |
{new Date(link.createdAt).toLocaleDateString('id-ID')} |
|
))
)}
)}
>
)}
);
}
Madrasah Shortlink
Mengalihkan...
Menuju halaman tujuan Anda
URL Pendek, Akses Lebih Cepat
Solusi rapi bagikan dokumen dan pengumuman Madrasah
Dashboard Link
Laporan Data Shortlink Madrasah
| No |
URL Asli |
Short Code |
Klik |
Tanggal Dibuat |
Perpustakaan Digital
Sambutan kepala madrasah akan tampil di sini.
Nama Kepala
Kepala Madrasah
Koleksi Terbaru
Eksplorasi Kategori
Portal Masuk
Silakan masuk untuk mengakses fitur penuh perpustakaan.
MODE PREVIEW AKTIF
Akses Admin: admin / edudigital
Akses Peserta: peserta / edudigital
Kelola Koleksi Buku
Export Laporan
MADRASAH
PERPUSTAKAAN DIGITAL
ALAMAT
LAPORAN RIWAYAT PEMINJAMAN BUKU DIGITAL
| No | Waktu Akses | Nama Anggota | Kelas | Judul Buku |
PETUGAS
Petugas Perpustakaan
Konfigurasi Sistem
Sinkronisasi Database Drive
Tarik file PDF otomatis dari folder Drive.