import React, { useState, useEffect, useCallback, useMemo } from 'react';
import { initializeApp } from 'firebase/app';
import { getAuth, signInAnonymously, onAuthStateChanged, signInWithCustomToken } from 'firebase/auth';
import { getFirestore, doc, getDoc, setDoc, onSnapshot } from 'firebase/firestore';
// --- Konfigurasi Firebase ---
// Konfigurasi ini akan disediakan secara otomatis di lingkungan Canvas.
const firebaseConfig = typeof __firebase_config !== 'undefined'
? JSON.parse(__firebase_config)
: { apiKey: "YOUR_API_KEY", authDomain: "YOUR_AUTH_DOMAIN", projectId: "YOUR_PROJECT_ID" };
const app = initializeApp(firebaseConfig);
const auth = getAuth(app);
const db = getFirestore(app);
// --- Icon Components (SVG) ---
const HomeIcon = ({ className }) => (
<svg className={className} xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" /><polyline points="9 22 9 12 15 12 15 22" /></svg>
);
const BookOpenIcon = ({ className }) => (
<svg className={className} xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z" /><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z" /></svg>
);
const CheckSquareIcon = ({ className }) => (
<svg className={className} xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="9 11 12 14 22 4" /><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11" /></svg>
);
const SettingsIcon = ({ className }) => (
<svg className={className} xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 0 2l-.15.08a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l-.22-.38a2 2 0 0 0-.73-2.73l-.15-.1a2 2 0 0 1 0-2l.15-.08a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z" /><circle cx="12" cy="12" r="3" /></svg>
);
const ClockIcon = ({ className }) => (
<svg className={className} xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10" /><polyline points="12 6 12 12 16 14" /></svg>
);
const TrendingUpIcon = ({ className }) => (
<svg className={className} xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="23 6 13.5 15.5 8.5 10.5 1 18" /><polyline points="17 6 23 6 23 12" /></svg>
);
const ArrowLeftIcon = ({ className }) => (
<svg className={className} xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="19" y1="12" x2="5" y2="12" /><polyline points="12 19 5 12 12 5" /></svg>
);
const PlayCircleIcon = ({ className }) => (
<svg className={className} xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"/><polygon points="10 8 16 12 10 16 10 8"/></svg>
);
const PauseCircleIcon = ({ className }) => (
<svg className={className} xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"/><line x1="10" y1="15" x2="10" y2="9"/><line x1="14" y1="15" x2="14" y2="9"/></svg>
);
// --- Helper Functions & Data ---
const shortSurahs = [
{ no: 1, name: "Al-Fatihah", translation: "Pembukaan" },
{ no: 103, name: "Al-Asr", translation: "Masa" },
{ no: 108, name: "Al-Kawthar", translation: "Nikmat yang Banyak" },
{ no: 109, name: "Al-Kafirun", translation: "Orang-orang Kafir" },
{ no: 110, name: "An-Nasr", translation: "Pertolongan" },
{ no: 111, name: "Al-Masad", translation: "Gejolak Api" },
{ no: 112, name: "Al-Ikhlas", translation: "Keesaan Allah" },
{ no: 113, name: "Al-Falaq", translation: "Waktu Subuh" },
{ no: 114, name: "An-Nas", translation: "Manusia" },
];
const LoadingSpinner = () => (
<div className="flex justify-center items-center h-full">
<div className="animate-spin rounded-full h-16 w-16 border-b-2 border-teal-500"></div>
</div>
);
// --- Main Components ---
const Dashboard = ({ onNavigate }) => {
// Data dummy untuk tampilan
const today = new Date();
const hijriDate = new Intl.DateTimeFormat('en-u-ca-islamic', { day: 'numeric', month: 'long', year: 'numeric' }).format(today);
const gregorianDate = today.toLocaleDateString('id-ID', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
return (
<div className="p-4 space-y-6">
<div className="bg-teal-500 text-white p-4 rounded-lg shadow-md">
<p className="font-bold text-lg">Assalamu'alaikum</p>
<p className="text-sm">{gregorianDate}</p>
<p className="text-sm">{hijriDate}</p>
</div>
<div className="bg-white p-4 rounded-lg shadow-md text-center">
<p className="text-gray-500">Sholat Selanjutnya</p>
<p className="text-3xl font-bold text-teal-600 my-2">Subuh</p>
<p className="text-2xl font-semibold text-gray-800">04:41</p>
<p className="text-gray-500 text-sm">dalam 8 jam 37 menit</p>
</div>
<div className="bg-white p-4 rounded-lg shadow-md">
<div className="flex justify-between items-center mb-2">
<h3 className="font-bold text-gray-700">Progres Hari Ini</h3>
<TrendingUpIcon className="w-6 h-6 text-teal-500" />
</div>
<p className="text-sm text-gray-500">Sholat Wajib</p>
<div className="flex items-center space-x-4 mt-1">
<div className="w-full bg-gray-200 rounded-full h-2.5">
<div className="bg-teal-500 h-2.5 rounded-full" style={{ width: '100%' }}></div>
</div>
<span className="font-bold text-teal-500">100%</span>
</div>
<p className="text-right text-sm text-gray-600 mt-1">selesai</p>
</div>
<div className="bg-white p-4 rounded-lg shadow-md">
<h3 className="font-bold text-gray-700 mb-2">Renungan Hari Ini</h3>
<blockquote className="border-l-4 border-teal-500 pl-4 italic text-gray-600">
"Maka ingatlah kamu kepada-Ku niscaya Aku ingat (pula) kepadamu."
<footer className="mt-1 text-sm text-gray-500">- QS. Al-Baqarah: 152</footer>
</blockquote>
</div>
</div>
);
};
const SurahDetailView = ({ surahNumber, onBack, userId }) => {
const [surah, setSurah] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [progress, setProgress] = useState([]);
const [audio, setAudio] = useState(null);
const [isPlaying, setIsPlaying] = useState(false);
const appId = typeof __app_id !== 'undefined' ? __app_id : 'default-app-id';
// Mengambil data surah dari API
useEffect(() => {
setLoading(true);
fetch(`https://quran-api.santrikoding.com/api/surah/${surahNumber}`)
.then(res => {
if (!res.ok) {
throw new Error('Gagal mengambil data surah');
}
return res.json();
})
.then(data => {
setSurah(data);
setError(null);
})
.catch(err => {
setError(err.message);
setSurah(null);
})
.finally(() => setLoading(false));
}, [surahNumber]);
// Mengambil dan mendengarkan progress dari Firestore
useEffect(() => {
if (!userId || !surah) return;
const progressDocRef = doc(db, `artifacts/${appId}/users/${userId}/memorizationProgress`, `surah_${surahNumber}`);
const unsubscribe = onSnapshot(progressDocRef, (docSnap) => {
if (docSnap.exists()) {
setProgress(docSnap.data().memorizedVerses || []);
} else {
setProgress([]);
}
}, (err) => {
console.error("Error listening to progress:", err);
});
return () => unsubscribe();
}, [userId, surah, surahNumber, appId]);
// Setup audio
useEffect(() => {
if (surah && surah.audio) {
const audioInstance = new Audio(surah.audio);
audioInstance.onended = () => setIsPlaying(false);
setAudio(audioInstance);
}
return () => {
if (audio) {
audio.pause();
}
};
}, [surah]);
const handleToggleVerse = async (verseNumber) => {
if (!userId || !surah) return;
const newProgress = progress.includes(verseNumber)
? progress.filter(v => v !== verseNumber)
: [...progress, verseNumber];
const progressDocRef = doc(db, `artifacts/${appId}/users/${userId}/memorizationProgress`, `surah_${surahNumber}`);
try {
await setDoc(progressDocRef, {
surahNumber: surah.nomor,
surahName: surah.nama,
memorizedVerses: newProgress,
totalVerses: surah.jumlah_ayat
}, { merge: true });
} catch (err) {
console.error("Error saving progress: ", err);
}
};
const togglePlay = () => {
if (!audio) return;
if (isPlaying) {
audio.pause();
} else {
audio.play();
}
setIsPlaying(!isPlaying);
};
if (loading) return <div className="p-4"><LoadingSpinner /></div>;
if (error) return <div className="p-4 text-red-500">Error: {error}</div>;
if (!surah) return null;
const memorizedCount = progress.length;
const totalCount = surah.jumlah_ayat;
const progressPercentage = totalCount > 0 ? (memorizedCount / totalCount) * 100 : 0;
return (
<div className="p-4">
<header className="flex items-center mb-4">
<button onClick={onBack} className="p-2 mr-2 rounded-full hover:bg-gray-200">
<ArrowLeftIcon className="w-6 h-6 text-gray-700" />
</button>
<div>
<h2 className="text-2xl font-bold text-gray-800">{surah.nama_latin} ({surah.nama})</h2>
<p className="text-gray-500">{surah.arti}, {surah.jumlah_ayat} ayat</p>
</div>
</header>
<div className="bg-white p-4 rounded-lg shadow-md mb-4">
<div className="flex items-center justify-between">
<div>
<h3 className="font-bold text-gray-700">Progres Hafalan</h3>
<p className="text-sm text-gray-500">{memorizedCount} dari {totalCount} ayat dihafal</p>
</div>
<button onClick={togglePlay} className="text-teal-500 hover:text-teal-600">
{isPlaying ? <PauseCircleIcon className="w-10 h-10"/> : <PlayCircleIcon className="w-10 h-10"/>}
</button>
</div>
<div className="w-full bg-gray-200 rounded-full h-2.5 mt-2">
<div className="bg-teal-500 h-2.5 rounded-full" style={{ width: `${progressPercentage}%` }}></div>
</div>
</div>
<div className="space-y-4">
{surah.ayat.map(ayat => (
<div key={ayat.nomor} className="bg-white p-4 rounded-lg shadow-md">
<div className="flex justify-between items-start">
<span className="text-sm font-bold text-teal-500 bg-teal-100 rounded-full w-8 h-8 flex items-center justify-center">{ayat.nomor}</span>
<p className="text-right text-2xl font-serif text-gray-800 leading-loose flex-1 ml-4">{ayat.ar}</p>
</div>
<p className="mt-4 text-teal-700 text-sm italic">{ayat.tr}</p>
<p className="mt-2 text-gray-600 text-sm">"{ayat.idn}"</p>
<div className="mt-4 pt-4 border-t border-gray-100 flex items-center">
<input
type="checkbox"
id={`verse-${ayat.nomor}`}
className="h-5 w-5 rounded border-gray-300 text-teal-600 focus:ring-teal-500 cursor-pointer"
checked={progress.includes(ayat.nomor)}
onChange={() => handleToggleVerse(ayat.nomor)}
/>
<label htmlFor={`verse-${ayat.nomor}`} className="ml-2 text-sm text-gray-700 cursor-pointer">Tandai sudah hafal</label>
</div>
</div>
))}
</div>
</div>
);
};
const MemorizationScreen = ({ onSelectSurah, userId }) => {
const [progressData, setProgressData] = useState({});
const appId = typeof __app_id !== 'undefined' ? __app_id : 'default-app-id';
useEffect(() => {
if (!userId) return;
const unsubscribes = shortSurahs.map(surah => {
const progressDocRef = doc(db, `artifacts/${appId}/users/${userId}/memorizationProgress`, `surah_${surah.no}`);
return onSnapshot(progressDocRef, (docSnap) => {
if (docSnap.exists()) {
setProgressData(prev => ({ ...prev, [surah.no]: docSnap.data() }));
}
});
});
return () => unsubscribes.forEach(unsub => unsub());
}, [userId, appId]);
return (
<div className="p-4">
<h2 className="text-2xl font-bold text-gray-800 mb-4">Hafalan Surah Pendek</h2>
<div className="space-y-3">
{shortSurahs.map(surah => {
const progress = progressData[surah.no];
const memorizedCount = progress?.memorizedVerses?.length || 0;
const totalCount = progress?.totalVerses || 0;
const percentage = totalCount > 0 ? (memorizedCount / totalCount) * 100 : 0;
return (
<div key={surah.no} onClick={() => onSelectSurah(surah.no)} className="bg-white p-4 rounded-lg shadow-md cursor-pointer hover:bg-gray-50 transition-colors">
<div className="flex justify-between items-center">
<div>
<p className="font-bold text-gray-800">{surah.name}</p>
<p className="text-sm text-gray-500">{surah.translation}</p>
</div>
<div className="text-right">
{progress && (
<>
<p className="text-sm font-semibold text-teal-600">{Math.round(percentage)}%</p>
<p className="text-xs text-gray-400">{memorizedCount}/{totalCount} ayat</p>
</>
)}
</div>
</div>
{progress && (
<div className="w-full bg-gray-200 rounded-full h-1.5 mt-2">
<div className="bg-teal-500 h-1.5 rounded-full" style={{ width: `${percentage}%` }}></div>
</div>
)}
</div>
);
})}
</div>
</div>
);
};
const PlaceholderScreen = ({ title }) => (
<div className="flex items-center justify-center h-full p-4">
<div className="text-center text-gray-500">
<h2 className="text-2xl font-bold">{title}</h2>
<p>Halaman ini sedang dalam pengembangan.</p>
</div>
</div>
);
const BottomNav = ({ activePage, onNavigate }) => {
const navItems = [
{ id: 'dashboard', label: 'Dashboard', icon: HomeIcon },
{ id: 'memorize', label: 'Hafalan', icon: BookOpenIcon },
{ id: 'progress', label: 'Progres', icon: CheckSquareIcon },
{ id: 'settings', label: 'Pengaturan', icon: SettingsIcon },
];
return (
<nav className="fixed bottom-0 left-0 right-0 bg-white shadow-[0_-1px_3px_rgba(0,0,0,0.1)]">
<div className="flex justify-around max-w-md mx-auto">
{navItems.map(item => (
<button
key={item.id}
onClick={() => onNavigate(item.id)}
className={`flex flex-col items-center justify-center w-full pt-2 pb-1 text-sm transition-colors ${activePage === item.id ? 'text-teal-500' : 'text-gray-500 hover:text-teal-500'}`}
>
<item.icon className="w-6 h-6 mb-1" />
<span>{item.label}</span>
{activePage === item.id && <div className="w-8 h-1 bg-teal-500 rounded-full mt-1"></div>}
</button>
))}
</div>
</nav>
);
};
export default function App() {
const [page, setPage] = useState('dashboard');
const [selectedSurah, setSelectedSurah] = useState(null);
const [userId, setUserId] = useState(null);
const [isAuthReady, setIsAuthReady] = useState(false);
const appId = useMemo(() => typeof __app_id !== 'undefined' ? __app_id : 'default-app-id', []);
useEffect(() => {
const authAndListen = async () => {
try {
const token = typeof __initial_auth_token !== 'undefined' ? __initial_auth_token : null;
if (token) {
await signInWithCustomToken(auth, token);
} else {
await signInAnonymously(auth);
}
} catch (error) {
console.error("Authentication failed:", error);
// Fallback to anonymous if custom token fails
if (auth.currentUser === null) {
await signInAnonymously(auth);
}
}
};
const unsubscribe = onAuthStateChanged(auth, (user) => {
if (user) {
setUserId(user.uid);
} else {
setUserId(null);
}
setIsAuthReady(true);
});
authAndListen();
return () => unsubscribe();
}, []);
const handleNavigate = (newPage) => {
setSelectedSurah(null);
setPage(newPage);
};
const handleSelectSurah = (surahNumber) => {
setSelectedSurah(surahNumber);
setPage('memorize_detail');
};
const handleBackFromDetail = () => {
setSelectedSurah(null);
setPage('memorize');
};
const renderPage = () => {
if (!isAuthReady) {
return <LoadingSpinner />;
}
switch (page) {
case 'dashboard':
return <Dashboard onNavigate={handleNavigate} />;
case 'memorize':
return <MemorizationScreen onSelectSurah={handleSelectSurah} userId={userId} />;
case 'memorize_detail':
return <SurahDetailView surahNumber={selectedSurah} onBack={handleBackFromDetail} userId={userId} />;
case 'progress':
return <PlaceholderScreen title="Progres" />;
case 'settings':
return <PlaceholderScreen title="Pengaturan" />;
default:
return <Dashboard onNavigate={handleNavigate} />;
}
};
return (
<div className="bg-gray-100 font-sans min-h-screen pb-20">
<div className="max-w-md mx-auto bg-gray-100">
{renderPage()}
</div>
{page !== 'memorize_detail' && <BottomNav activePage={page} onNavigate={handleNavigate} />}
</div>
);
}