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

{ 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 (
);
}
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 (
);
}
if (errorMessage) {
return (
);
}
if (!novel) {
return (
Novel data not available.
);
}
const placeholderImage = `https://placehold.co/300x400/2d3748/cbd5e0?text=No+Cover`;
return (

{ 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 ? (
{chapters.map(chapter => (
-
))}
) : (
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 (
);
}
if (errorMessage) {
return (
);
}
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 (
);
}
return (
setMessage('')} />
Admin Panel
Your User ID: {userId} (Only you can upload content)
{/* Add New Novel Form */}
{/* Add New Chapter Form */}
);
};
// 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}
);
}