| 1 | import React, { Suspense, lazy, ReactNode, useEffect } from 'react'
|
|---|
| 2 | import { Routes, Route, Navigate } from 'react-router-dom'
|
|---|
| 3 | import { useAuthStore } from './store/authStore'
|
|---|
| 4 | import { useUIStore } from './store/uiStore'
|
|---|
| 5 | import { useStoryStore } from './store/storyStore'
|
|---|
| 6 | import { UserRole } from './types'
|
|---|
| 7 | import { Navbar } from './components/layout/Navbar'
|
|---|
| 8 | import { Footer } from './components/layout/Footer'
|
|---|
| 9 | import { ToastContainer } from './components/ui/Toast'
|
|---|
| 10 | import { Spinner } from './components/ui/Spinner'
|
|---|
| 11 |
|
|---|
| 12 | // Lazy imports
|
|---|
| 13 | const LandingPage = lazy(() => import('./pages/LandingPage').then(m => ({ default: m.LandingPage })))
|
|---|
| 14 | const LoginPage = lazy(() => import('./pages/auth/LoginPage').then(m => ({ default: m.LoginPage })))
|
|---|
| 15 | const RegisterPage = lazy(() => import('./pages/auth/RegisterPage').then(m => ({ default: m.RegisterPage })))
|
|---|
| 16 | const BrowsePage = lazy(() => import('./pages/browse/BrowsePage').then(m => ({ default: m.BrowsePage })))
|
|---|
| 17 | const GenrePage = lazy(() => import('./pages/browse/GenrePage').then(m => ({ default: m.GenrePage })))
|
|---|
| 18 | const GenresListPage = lazy(() => import('./pages/browse/GenrePage').then(m => ({ default: m.GenresListPage })))
|
|---|
| 19 | const StoryDetailPage = lazy(() => import('./pages/story/StoryDetailPage').then(m => ({ default: m.StoryDetailPage })))
|
|---|
| 20 | const ChapterReadPage = lazy(() => import('./pages/story/ChapterReadPage').then(m => ({ default: m.ChapterReadPage })))
|
|---|
| 21 | const WriterDashboard = lazy(() => import('./pages/writer/WriterDashboard').then(m => ({ default: m.WriterDashboard })))
|
|---|
| 22 | const CreateStoryPage = lazy(() => import('./pages/writer/CreateStoryPage').then(m => ({ default: m.CreateStoryPage })))
|
|---|
| 23 | const EditStoryPage = lazy(() => import('./pages/writer/EditStoryPage').then(m => ({ default: m.EditStoryPage })))
|
|---|
| 24 | const CreateChapterPage = lazy(() => import('./pages/writer/CreateChapterPage').then(m => ({ default: m.CreateChapterPage })))
|
|---|
| 25 | const EditChapterPage = lazy(() => import('./pages/writer/EditChapterPage').then(m => ({ default: m.EditChapterPage })))
|
|---|
| 26 | const ReadingListPage = lazy(() => import('./pages/reading-list/ReadingListPage').then(m => ({ default: m.ReadingListPage })))
|
|---|
| 27 | const CommunityListsPage = lazy(() => import('./pages/reading-list/CommunityListsPage').then(m => ({ default: m.CommunityListsPage })))
|
|---|
| 28 | const ProfilePage = lazy(() => import('./pages/profile/ProfilePage').then(m => ({ default: m.ProfilePage })))
|
|---|
| 29 | const AdminDashboard = lazy(() => import('./pages/admin/AdminDashboard').then(m => ({ default: m.AdminDashboard })))
|
|---|
| 30 | const AdminUsersPage = lazy(() => import('./pages/admin/AdminUsersPage').then(m => ({ default: m.AdminUsersPage })))
|
|---|
| 31 | const AdminContentPage = lazy(() => import('./pages/admin/AdminContentPage').then(m => ({ default: m.AdminContentPage })))
|
|---|
| 32 | const AdminGenresPage = lazy(() => import('./pages/admin/AdminGenresPage').then(m => ({ default: m.AdminGenresPage })))
|
|---|
| 33 | const NotificationsPage = lazy(() => import('./pages/NotificationsPage').then(m => ({ default: m.NotificationsPage })))
|
|---|
| 34 |
|
|---|
| 35 | const SuspenseFallback = () => (
|
|---|
| 36 | <div className="flex items-center justify-center min-h-[50vh]">
|
|---|
| 37 | <Spinner size="lg" />
|
|---|
| 38 | </div>
|
|---|
| 39 | )
|
|---|
| 40 |
|
|---|
| 41 | // Protected route component
|
|---|
| 42 | const ProtectedRoute = ({
|
|---|
| 43 | children,
|
|---|
| 44 | requiredRole,
|
|---|
| 45 | }: {
|
|---|
| 46 | children: ReactNode
|
|---|
| 47 | requiredRole?: UserRole | UserRole[]
|
|---|
| 48 | }) => {
|
|---|
| 49 | const { currentUser } = useAuthStore()
|
|---|
| 50 | const { addToast } = useUIStore()
|
|---|
| 51 |
|
|---|
| 52 | if (!currentUser) {
|
|---|
| 53 | return <Navigate to="/login" replace />
|
|---|
| 54 | }
|
|---|
| 55 |
|
|---|
| 56 | if (requiredRole) {
|
|---|
| 57 | const roles = Array.isArray(requiredRole) ? requiredRole : [requiredRole]
|
|---|
| 58 | if (!roles.includes(currentUser.role)) {
|
|---|
| 59 | addToast('You do not have permission to access this page.', 'error')
|
|---|
| 60 | return <Navigate to="/" replace />
|
|---|
| 61 | }
|
|---|
| 62 | }
|
|---|
| 63 |
|
|---|
| 64 | return <>{children}</>
|
|---|
| 65 | }
|
|---|
| 66 |
|
|---|
| 67 | function App() {
|
|---|
| 68 | const { fetchStories, fetchChapters, fetchCollaborations, fetchReadingLists } = useStoryStore()
|
|---|
| 69 |
|
|---|
| 70 | useEffect(() => {
|
|---|
| 71 | fetchStories()
|
|---|
| 72 | fetchChapters()
|
|---|
| 73 | fetchCollaborations()
|
|---|
| 74 | fetchReadingLists()
|
|---|
| 75 | }, [])
|
|---|
| 76 |
|
|---|
| 77 | return (
|
|---|
| 78 | <div className="flex flex-col min-h-screen">
|
|---|
| 79 | <Navbar />
|
|---|
| 80 | <main className="flex-1">
|
|---|
| 81 | <Suspense fallback={<SuspenseFallback />}>
|
|---|
| 82 | <Routes>
|
|---|
| 83 | {/* Public */}
|
|---|
| 84 | <Route path="/" element={<LandingPage />} />
|
|---|
| 85 | <Route path="/login" element={<LoginPage />} />
|
|---|
| 86 | <Route path="/register" element={<RegisterPage />} />
|
|---|
| 87 | <Route path="/browse" element={<BrowsePage />} />
|
|---|
| 88 | <Route path="/genres" element={<GenresListPage />} />
|
|---|
| 89 | <Route path="/genres/:genre" element={<GenrePage />} />
|
|---|
| 90 | <Route path="/story/:id" element={<StoryDetailPage />} />
|
|---|
| 91 | <Route path="/story/:storyId/chapter/:chapterId" element={<ChapterReadPage />} />
|
|---|
| 92 | <Route path="/community-lists" element={<CommunityListsPage />} />
|
|---|
| 93 | <Route path="/profile/:username" element={<ProfilePage />} />
|
|---|
| 94 | <Route path="/notifications" element={<NotificationsPage />} />
|
|---|
| 95 |
|
|---|
| 96 | {/* Authenticated */}
|
|---|
| 97 | <Route
|
|---|
| 98 | path="/reading-lists"
|
|---|
| 99 | element={
|
|---|
| 100 | <ProtectedRoute>
|
|---|
| 101 | <ReadingListPage />
|
|---|
| 102 | </ProtectedRoute>
|
|---|
| 103 | }
|
|---|
| 104 | />
|
|---|
| 105 |
|
|---|
| 106 | {/* Writer routes */}
|
|---|
| 107 | <Route
|
|---|
| 108 | path="/writer"
|
|---|
| 109 | element={
|
|---|
| 110 | <ProtectedRoute requiredRole={['writer', 'admin']}>
|
|---|
| 111 | <WriterDashboard />
|
|---|
| 112 | </ProtectedRoute>
|
|---|
| 113 | }
|
|---|
| 114 | />
|
|---|
| 115 | <Route
|
|---|
| 116 | path="/writer/create-story"
|
|---|
| 117 | element={
|
|---|
| 118 | <ProtectedRoute requiredRole={['writer', 'admin']}>
|
|---|
| 119 | <CreateStoryPage />
|
|---|
| 120 | </ProtectedRoute>
|
|---|
| 121 | }
|
|---|
| 122 | />
|
|---|
| 123 | <Route
|
|---|
| 124 | path="/writer/edit-story/:id"
|
|---|
| 125 | element={
|
|---|
| 126 | <ProtectedRoute requiredRole={['writer', 'admin']}>
|
|---|
| 127 | <EditStoryPage />
|
|---|
| 128 | </ProtectedRoute>
|
|---|
| 129 | }
|
|---|
| 130 | />
|
|---|
| 131 | <Route
|
|---|
| 132 | path="/writer/create-chapter/:storyId"
|
|---|
| 133 | element={
|
|---|
| 134 | <ProtectedRoute requiredRole={['writer', 'admin']}>
|
|---|
| 135 | <CreateChapterPage />
|
|---|
| 136 | </ProtectedRoute>
|
|---|
| 137 | }
|
|---|
| 138 | />
|
|---|
| 139 | <Route
|
|---|
| 140 | path="/writer/edit-chapter/:chapterId"
|
|---|
| 141 | element={
|
|---|
| 142 | <ProtectedRoute requiredRole={['writer', 'admin']}>
|
|---|
| 143 | <EditChapterPage />
|
|---|
| 144 | </ProtectedRoute>
|
|---|
| 145 | }
|
|---|
| 146 | />
|
|---|
| 147 |
|
|---|
| 148 | {/* Admin routes */}
|
|---|
| 149 | <Route
|
|---|
| 150 | path="/admin"
|
|---|
| 151 | element={
|
|---|
| 152 | <ProtectedRoute requiredRole="admin">
|
|---|
| 153 | <AdminDashboard />
|
|---|
| 154 | </ProtectedRoute>
|
|---|
| 155 | }
|
|---|
| 156 | />
|
|---|
| 157 | <Route
|
|---|
| 158 | path="/admin/users"
|
|---|
| 159 | element={
|
|---|
| 160 | <ProtectedRoute requiredRole="admin">
|
|---|
| 161 | <AdminUsersPage />
|
|---|
| 162 | </ProtectedRoute>
|
|---|
| 163 | }
|
|---|
| 164 | />
|
|---|
| 165 | <Route
|
|---|
| 166 | path="/admin/content"
|
|---|
| 167 | element={
|
|---|
| 168 | <ProtectedRoute requiredRole="admin">
|
|---|
| 169 | <AdminContentPage />
|
|---|
| 170 | </ProtectedRoute>
|
|---|
| 171 | }
|
|---|
| 172 | />
|
|---|
| 173 | <Route
|
|---|
| 174 | path="/admin/genres"
|
|---|
| 175 | element={
|
|---|
| 176 | <ProtectedRoute requiredRole="admin">
|
|---|
| 177 | <AdminGenresPage />
|
|---|
| 178 | </ProtectedRoute>
|
|---|
| 179 | }
|
|---|
| 180 | />
|
|---|
| 181 |
|
|---|
| 182 | {/* 404 */}
|
|---|
| 183 | <Route path="*" element={<Navigate to="/" replace />} />
|
|---|
| 184 | </Routes>
|
|---|
| 185 | </Suspense>
|
|---|
| 186 | </main>
|
|---|
| 187 | <Footer />
|
|---|
| 188 | <ToastContainer />
|
|---|
| 189 | </div>
|
|---|
| 190 | )
|
|---|
| 191 | }
|
|---|
| 192 |
|
|---|
| 193 | export default App
|
|---|