import React, { useState, useEffect, createContext, useContext } from 'react'; import { initializeApp } from 'firebase/app'; import { getAuth, signInAnonymously, signInWithCustomToken, onAuthStateChanged } from 'firebase/auth'; import { getFirestore, doc, getDoc, addDoc, setDoc, updateDoc, deleteDoc, onSnapshot, collection, query, orderBy, limit, getDocs, where } from 'firebase/firestore'; // Global variables provided by the Canvas environment const appId = typeof __app_id !== 'undefined' ? __app_id : 'default-novel-app-id'; const firebaseConfig = typeof __firebase_config !== 'undefined' ? JSON.parse(__firebase_config) : {}; const initialAuthToken = typeof __initial_auth_token !== 'undefined' ? __initial_auth_token : null; // Create a context for Firebase and user data const FirebaseContext = createContext(null); // Firebase Provider Component const FirebaseProvider = ({ children }) => { const [db, setDb] = useState(null); const [auth, setAuth] = useState(null); const [userId, setUserId] = useState(null); const [loadingAuth, setLoadingAuth] = useState(true); useEffect(() => { const app = initializeApp(firebaseConfig); const firestore = getFirestore(app); const firebaseAuth = getAuth(app); setDb(firestore); setAuth(firebaseAuth); // Sign in or listen to auth state changes const unsubscribe = onAuthStateChanged(firebaseAuth, async (user) => { if (user) { setUserId(user.uid); } else { // If no user, try to sign in with custom token or anonymously try { if (initialAuthToken) { await signInWithCustomToken(firebaseAuth, initialAuthToken); } else { await signInAnonymously(firebaseAuth); } } catch (error) { console.error("Firebase authentication failed:", error); // Fallback if anonymous sign-in also fails setUserId(crypto.randomUUID()); } } setLoadingAuth(false); }); return () => unsubscribe(); // Cleanup auth listener }, []); if (loadingAuth) { return (
Loading authentication...
); } return ( {children} ); }; // Custom hook to use Firebase context const useFirebase = () => { const context = useContext(FirebaseContext); if (!context) { throw new Error('useFirebase must be used within a FirebaseProvider'); } return context; }; // Helper for simple client-side routing const routes = { home: 'home', novelDetail: 'novelDetail', chapterRead: 'chapterRead', admin: 'admin', }; // Component for displaying messages (instead of alert) const MessageModal = ({ message, onClose }) => { if (!message) return null; return (

{message}

); }; // --- Components --- // Header Component const Header = ({ navigate, onSearch }) => { const [searchTerm, setSearchTerm] = useState(''); const handleSearch = (e) => { e.preventDefault(); onSearch(searchTerm); }; return (

navigate(routes.home)} > Freewilly Translation

setSearchTerm(e.target.value)} />
); }; // Novel Card Component const NovelCard = ({ novel, navigate }) => { const placeholderImage = `https://placehold.co/150x200/2d3748/cbd5e0?text=No+Cover`; // Tailwind gray-800 bg, gray-400 text return (
navigate(routes.novelDetail, novel.id)} > {novel.title} { e.target.onerror = null; e.target.src = placeholderImage; }} />

{novel.title}

{novel.author}

{novel.genres && novel.genres.map((genre, index) => ( {genre} ))}
); }; // Home Page Component const HomePage = ({ navigate, searchTerm: initialSearchTerm }) => { const { db } = useFirebase(); const [latestNovels, setLatestNovels] = useState([]); const [popularNovels, setPopularNovels] = useState([]); const [searchResults, setSearchResults] = useState([]); const [loading, setLoading] = useState(true); const [searchTerm, setSearchTerm] = useState(initialSearchTerm || ''); useEffect(() => { if (!db) return; setLoading(true); const fetchNovels = async () => { try { // Fetch Latest Updates (ordered by creation date descending) const latestQuery = query( collection(db, `artifacts/${appId}/public/data/novels`), orderBy('createdAt', 'desc'), limit(10) ); const latestSnapshot = await getDocs(latestQuery); const latest = latestSnapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })); setLatestNovels(latest); // Fetch Popular Novels (ordered by views descending - using dummy views for now) const popularQuery = query( collection(db, `artifacts/${appId}/public/data/novels`), orderBy('views', 'desc'), limit(10) ); const popularSnapshot = await getDocs(popularQuery); const popular = popularSnapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })); setPopularNovels(popular); } catch (error) { console.error("Error fetching novels:", error); } finally { setLoading(false); } }; fetchNovels(); // Real-time listener for latest novels (optional, for more dynamic updates) const unsubscribeLatest = onSnapshot( query(collection(db, `artifacts/${appId}/public/data/novels`), orderBy('createdAt', 'desc'), limit(10)), (snapshot) => { const novels = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })); setLatestNovels(novels); }, (error) => console.error("Error listening to latest novels:", error) ); // Real-time listener for popular novels (optional) const unsubscribePopular = onSnapshot( query(collection(db, `artifacts/${appId}/public/data/novels`), orderBy('views', 'desc'), limit(10)), (snapshot) => { const novels = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })); setPopularNovels(novels); }, (error) => console.error("Error listening to popular novels:", error) ); return () => { unsubscribeLatest(); unsubscribePopular(); }; }, [db]); useEffect(() => { const performSearch = async () => { if (!db || !searchTerm) { setSearchResults([]); return; } setLoading(true); try { const q = query( collection(db, `artifacts/${appId}/public/data/novels`), where('title', '>=', searchTerm), where('title', '<=', searchTerm + '\uf8ff') ); const snapshot = await getDocs(q); const results = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })); setSearchResults(results); } catch (error) { console.error("Error searching novels:", error); } finally { setLoading(false); } }; performSearch(); }, [db, searchTerm]); if (loading) { return (
Loading novels...
); } return (

{searchTerm ? `Search Results for "${searchTerm}"` : 'Latest Updates'}

{(searchTerm ? searchResults : latestNovels).map(novel => ( ))} {searchTerm && searchResults.length === 0 && (

No novels found for "{searchTerm}".

)} {!searchTerm && latestNovels.length === 0 && (

No novels available yet. Add some from the admin panel!

)}
{!searchTerm && ( <>

Popular Novels

{popularNovels.map(novel => ( ))} {popularNovels.length === 0 && (

No popular novels yet.

)}
)}
); }; // Novel Detail Page Component const NovelDetailPage = ({ navigate, novelId }) => { const { db } = useFirebase(); const [novel, setNovel] = useState(null); const [chapters, setChapters] = useState([]); const [loading, setLoading] = useState(true); const [errorMessage, setErrorMessage] = useState(''); useEffect(() => { if (!db || !novelId) { setLoading(false); return; } setLoading(true); const fetchNovelAndChapters = async () => { try { // Fetch novel details const novelRef = doc(db, `artifacts/${appId}/public/data/novels`, novelId); const novelSnap = await getDoc(novelRef); if (novelSnap.exists()) { setNovel({ id: novelSnap.id, ...novelSnap.data() }); // Increment views (simple approach, could be more robust) await updateDoc(novelRef, { views: (novelSnap.data().views || 0) + 1 }); // Fetch chapters const chaptersQuery = query( collection(db, `artifacts/${appId}/public/data/novels/${novelId}/chapters`), orderBy('chapterNumber', 'asc') // Order chapters numerically ); const unsubscribeChapters = onSnapshot(chaptersQuery, (snapshot) => { const fetchedChapters = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })); setChapters(fetchedChapters); setLoading(false); }, (error) => { console.error("Error listening to chapters:", error); setErrorMessage("Failed to load chapters."); setLoading(false); }); return () => unsubscribeChapters(); // Cleanup listener } else { setErrorMessage("Novel not found."); setLoading(false); } } catch (error) { console.error("Error fetching novel or chapters:", error); setErrorMessage("Failed to load novel details."); setLoading(false); } }; fetchNovelAndChapters(); }, [db, novelId]); if (loading) { return (
Loading novel...
); } if (errorMessage) { return (

{errorMessage}

); } if (!novel) { return (

Novel data not available.

); } const placeholderImage = `https://placehold.co/300x400/2d3748/cbd5e0?text=No+Cover`; return (
{novel.title} { e.target.onerror = null; e.target.src = placeholderImage; }} />

{novel.title}

by {novel.author}

{novel.genres && novel.genres.map((genre, index) => ( {genre} ))}

{novel.synopsis}

Chapters

{chapters.length > 0 ? ( ) : (

No chapters available for this novel yet.

)}
); }; // Chapter Reading Page Component const ChapterPage = ({ navigate, novelId, chapterId }) => { const { db } = useFirebase(); const [novel, setNovel] = useState(null); const [chapter, setChapter] = useState(null); const [allChapters, setAllChapters] = useState([]); const [loading, setLoading] = useState(true); const [errorMessage, setErrorMessage] = useState(''); useEffect(() => { if (!db || !novelId || !chapterId) { setLoading(false); return; } setLoading(true); const fetchChapterData = async () => { try { // Fetch novel details const novelRef = doc(db, `artifacts/${appId}/public/data/novels`, novelId); const novelSnap = await getDoc(novelRef); if (novelSnap.exists()) { setNovel({ id: novelSnap.id, ...novelSnap.data() }); } else { setErrorMessage("Novel not found."); setLoading(false); return; } // Fetch all chapters for navigation const chaptersQuery = query( collection(db, `artifacts/${appId}/public/data/novels/${novelId}/chapters`), orderBy('chapterNumber', 'asc') ); const chaptersSnapshot = await getDocs(chaptersQuery); const fetchedChapters = chaptersSnapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })); setAllChapters(fetchedChapters); // Fetch current chapter content const chapterRef = doc(db, `artifacts/${appId}/public/data/novels/${novelId}/chapters`, chapterId); const chapterSnap = await getDoc(chapterRef); if (chapterSnap.exists()) { setChapter({ id: chapterSnap.id, ...chapterSnap.data() }); } else { setErrorMessage("Chapter not found."); } } catch (error) { console.error("Error fetching chapter data:", error); setErrorMessage("Failed to load chapter."); } finally { setLoading(false); } }; fetchChapterData(); }, [db, novelId, chapterId]); if (loading) { return (
Loading chapter...
); } if (errorMessage) { return (

{errorMessage}

); } if (!novel || !chapter) { return (

Chapter or novel data not available.

); } const currentChapterIndex = allChapters.findIndex(c => c.id === chapter.id); const prevChapter = currentChapterIndex > 0 ? allChapters[currentChapterIndex - 1] : null; const nextChapter = currentChapterIndex < allChapters.length - 1 ? allChapters[currentChapterIndex + 1] : null; return (

{novel.title}

{/* Spacer for alignment */}

Chapter {chapter.chapterNumber}: {chapter.title}

{/* Render chapter content, preserving line breaks */} {chapter.content.split('\n').map((paragraph, index) => (

{paragraph}

))}
{prevChapter ? ( ) : ( )} {nextChapter ? ( ) : ( )}
); }; // Admin Panel Component const AdminPanel = ({ navigate }) => { const { db, userId } = useFirebase(); const [novelTitle, setNovelTitle] = useState(''); const [novelAuthor, setNovelAuthor] = useState(''); const [novelSynopsis, setNovelSynopsis] = useState(''); const [novelCoverUrl, setNovelCoverUrl] = useState(''); const [novelGenres, setNovelGenres] = useState(''); // Comma separated string const [message, setMessage] = useState(''); const [novels, setNovels] = useState([]); const [selectedNovelId, setSelectedNovelId] = useState(''); const [chapterNumber, setChapterNumber] = useState(''); const [chapterTitle, setChapterTitle] = useState(''); const [chapterContent, setChapterContent] = useState(''); const [loading, setLoading] = useState(true); useEffect(() => { if (!db) return; setLoading(true); const novelsCollectionRef = collection(db, `artifacts/${appId}/public/data/novels`); const unsubscribe = onSnapshot(novelsCollectionRef, (snapshot) => { const fetchedNovels = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })); setNovels(fetchedNovels); if (fetchedNovels.length > 0 && !selectedNovelId) { setSelectedNovelId(fetchedNovels[0].id); } setLoading(false); }, (error) => { console.error("Error fetching novels for admin panel:", error); setMessage("Failed to load novels for admin panel."); setLoading(false); }); return () => unsubscribe(); }, [db, selectedNovelId]); const handleAddNovel = async (e) => { e.preventDefault(); if (!db || !userId) { setMessage("Authentication required to add novels."); return; } if (!novelTitle || !novelAuthor || !novelSynopsis) { setMessage("Please fill in all required novel fields (Title, Author, Synopsis)."); return; } try { const newNovel = { title: novelTitle, author: novelAuthor, synopsis: novelSynopsis, coverImageUrl: novelCoverUrl, genres: novelGenres.split(',').map(g => g.trim()).filter(g => g), createdAt: Date.now(), views: 0, // Initialize views uploadedBy: userId // Track who uploaded it }; await addDoc(collection(db, `artifacts/${appId}/public/data/novels`), newNovel); setMessage("Novel added successfully!"); setNovelTitle(''); setNovelAuthor(''); setNovelSynopsis(''); setNovelCoverUrl(''); setNovelGenres(''); } catch (error) { console.error("Error adding novel:", error); setMessage("Failed to add novel: " + error.message); } }; const handleAddChapter = async (e) => { e.preventDefault(); if (!db || !userId) { setMessage("Authentication required to add chapters."); return; } if (!selectedNovelId || !chapterNumber || !chapterTitle || !chapterContent) { setMessage("Please select a novel and fill in all chapter fields."); return; } try { const chapterNum = parseInt(chapterNumber); if (isNaN(chapterNum) || chapterNum <= 0) { setMessage("Chapter number must be a positive integer."); return; } const newChapter = { chapterNumber: chapterNum, title: chapterTitle, content: chapterContent, createdAt: Date.now(), uploadedBy: userId }; await addDoc(collection(db, `artifacts/${appId}/public/data/novels/${selectedNovelId}/chapters`), newChapter); setMessage("Chapter added successfully!"); setChapterNumber(''); setChapterTitle(''); setChapterContent(''); } catch (error) { console.error("Error adding chapter:", error); setMessage("Failed to add chapter: " + error.message); } }; if (loading) { return (
Loading admin panel...
); } return (
setMessage('')} />

Admin Panel

Your User ID: {userId} (Only you can upload content)

{/* Add New Novel Form */}

Add New Novel

setNovelTitle(e.target.value)} className="shadow appearance-none border border-gray-700 rounded-md w-full py-2 px-3 bg-gray-700 text-white leading-tight focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" required />
setNovelAuthor(e.target.value)} className="shadow appearance-none border border-gray-700 rounded-md w-full py-2 px-3 bg-gray-700 text-white leading-tight focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" required />
setNovelCoverUrl(e.target.value)} className="shadow appearance-none border border-gray-700 rounded-md w-full py-2 px-3 bg-gray-700 text-white leading-tight focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" />
setNovelGenres(e.target.value)} className="shadow appearance-none border border-gray-700 rounded-md w-full py-2 px-3 bg-gray-700 text-white leading-tight focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" />
{/* Add New Chapter Form */}

Add New Chapter

setChapterNumber(e.target.value)} className="shadow appearance-none border border-gray-700 rounded-md w-full py-2 px-3 bg-gray-700 text-white leading-tight focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" required />
setChapterTitle(e.target.value)} className="shadow appearance-none border border-gray-700 rounded-md w-full py-2 px-3 bg-gray-700 text-white leading-tight focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" required />
); }; // Main App Component export default function App() { // Current path is now just a string representing the "page" const [currentPath, setCurrentPath] = useState(routes.home); const [novelId, setNovelId] = useState(null); const [chapterId, setChapterId] = useState(null); const [searchTerm, setSearchTerm] = useState(''); // Simple client-side router const navigate = (path, id = null, subId = null) => { setCurrentPath(path); setNovelId(id); setChapterId(subId); setSearchTerm(''); // Clear search term on navigation }; const handleSearch = (term) => { setSearchTerm(term); navigate(routes.home); // Navigate to home to show search results }; let PageComponent; switch (currentPath) { case routes.home: PageComponent = ; break; case routes.novelDetail: PageComponent = ; break; case routes.chapterRead: PageComponent = ; break; case routes.admin: PageComponent = ; break; default: PageComponent = ; } return (
{/* Tailwind CSS CDN */} {/* Inter font from Google Fonts */}
{PageComponent}
); }