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