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 (
);
}
if (view === 'notfound') {
return (
);
}
return (
{toast.show && (
)}
{isLoading ? (
) : (
<>
{view === 'landing' && (
)}
{view === 'admin' && isAdminLoggedIn && (
)}
>
)}
);
}
Mengalihkan...
Anda sedang diarahkan ke tautan tujuan.
{redirectData && (
Menuju: {redirectData.originalUrl}
)}
Tautan Tidak Ditemukan
Maaf, tautan yang Anda cari tidak ada atau telah dihapus oleh Admin.
{toast.type === 'error' ? : }
{toast.message}
)}
{showLoginModal && (
Admin Login
Area khusus pengelola madrasah
setView('landing')}>
ShortLink
MADRASAH HEBAT BERMARTABAT
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
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 |
|---|---|---|---|---|
| Belum ada tautan. | ||||
| {DISPLAY_DOMAIN}/{link.shortUrl} | {link.originalUrl} |
{link.clicks || 0} | {new Date(link.createdAt).toLocaleDateString('id-ID')} |
|