Trending

Tanpa judul

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

Micro Tubs

Berbagi tips dan informasi seputar blogging,cara membuat blog, tips menulis konten,Adsense,Kisah Sukses Blogger

Posting Komentar

Lebih baru Lebih lama

Formulir Kontak