diff --git a/cinetrack-app/.gitignore b/cinetrack-app/.gitignore index b50664c..4c4a1b6 100644 --- a/cinetrack-app/.gitignore +++ b/cinetrack-app/.gitignore @@ -14,6 +14,7 @@ dist-ssr .env .env.* !.env.example +infra/data # Editor directories and files .vscode/* diff --git a/cinetrack-app/client/.env.example b/cinetrack-app/client/.env.example index 06581bd..4388b4a 100644 --- a/cinetrack-app/client/.env.example +++ b/cinetrack-app/client/.env.example @@ -1,10 +1,10 @@ # Client Environment Variables # Copy this file to .env and fill in your values -# TMDB API Read Access Token (required) -# Used as fallback if backend proxy fails +# TMDB API Read Access Token (optional - only needed as fallback) +# The server proxies all TMDB requests, so this is only used if proxy fails # Get one at: https://www.themoviedb.org/settings/api -VITE_TMDB_API_READ_ACCESS_TOKEN=your_tmdb_api_read_access_token +VITE_TMDB_API_READ_ACCESS_TOKEN= # API Base URL (optional, for development when server runs separately) # Leave empty in production as the client is served by the backend diff --git a/cinetrack-app/client/package.json b/cinetrack-app/client/package.json index e1368ad..7625931 100644 --- a/cinetrack-app/client/package.json +++ b/cinetrack-app/client/package.json @@ -10,12 +10,13 @@ "preview": "vite preview" }, "dependencies": { + "framer-motion": "^12.23.26", "react": "^19.2.1", "react-dom": "^19.2.1", "react-icons": "^5.5.0", + "react-router-dom": "^7.11.0", "scheduler": "^0.27.0", "socket.io-client": "^4.8.1", - "use-context-selector": "^2.0.0", "zustand": "^5.0.9" }, "devDependencies": { diff --git a/cinetrack-app/client/src/App.tsx b/cinetrack-app/client/src/App.tsx index ad407f9..52fc90d 100644 --- a/cinetrack-app/client/src/App.tsx +++ b/cinetrack-app/client/src/App.tsx @@ -1,387 +1,23 @@ -import React, { Suspense, lazy, memo, useMemo } from "react"; -import { FiSettings, FiSearch, FiBell, FiGithub, FiArrowLeft } from "react-icons/fi"; +import React, { Suspense, lazy } from "react"; import { ErrorBoundary } from "./components/common/ErrorBoundary"; -import { WatchlistProvider } from "./contexts/WatchlistContext"; -import { useWatchlistStore, getWatchlistIds } from "./store/useWatchlistStore"; -import { UIProvider, useUIContext } from "./contexts/UIContext"; +import { UIProvider } from "./contexts/UIContext"; import { DiscoverProvider } from "./contexts/DiscoverContext"; import { AuthProvider, useAuthContext } from "./contexts/AuthContext"; -import { SearchBar } from "./components/common/SearchBar"; -import { SearchPalette } from "./components/common/SearchPalette"; -import { NotificationsModal } from "./components/common/NotificationsModal"; -import { LoadingPosterAnimation } from "./components/common/LoadingPosterAnimation"; -import { BottomNavBar } from "./components/layout/BottomNavBar"; -import { SideNavBar } from "./components/layout/SideNavBar"; -import { MediaGrid } from "./components/media/MediaGrid"; -import { useLocalStorage } from "./hooks/useLocalStorage"; -import { getTVSeasonDetails } from "./services/tmdbService"; +import { DemoWelcomeModal, useDemoWelcome } from "./components/common/DemoWelcomeModal"; +import { RouterProvider } from "react-router-dom"; +import { router } from "./router"; +import { useWatchlistInit } from "./store/useWatchlistStore"; -const AuthPage = lazy(() => import("./pages/AuthPage").then(m => ({ default: m.AuthPage }))); -const DiscoverPage = lazy(() => import("./pages/DiscoverPage").then(m => ({ default: m.DiscoverPage }))); -const ListsPage = lazy(() => import("./pages/ListsPage").then(m => ({ default: m.ListsPage }))); -const RecommendationsPage = lazy(() => import("./pages/RecommendationsPage").then(m => ({ default: m.RecommendationsPage }))); -const StatisticsPage = lazy(() => import("./pages/StatisticsPage").then(m => ({ default: m.StatisticsPage }))); -const ViewAllPage = lazy(() => import("./pages/ViewAllPage").then(m => ({ default: m.ViewAllPage }))); +const AuthPage = lazy(() => import("./pages/AuthPage").then((m) => ({ default: m.AuthPage }))); -// Lazy load heavy modal components -const MediaDetailModal = lazy(() => - import("./components/media/MediaDetailModal").then((module) => ({ - default: module.MediaDetailModal, - })) -); -const SettingsModal = lazy(() => - import("./components/features/SettingsModal").then((module) => ({ - default: module.SettingsModal, - })) -); - -// Loading fallback for modals -const ModalLoadingFallback: React.FC = () => ( -
-
Loading...
-
-); - -// Header component -const Header: React.FC = memo(() => { - const { handleSearch, isSearchLoading, isSearchExpanded, setIsSearchExpanded, openSettings, openNotifications } = useUIContext(); - - return ( -
-
- {/* Desktop Header */} -
-
- SCENESTACK -
- {/* Tablet - Regular SearchBar */} -
-
- -
- - - - - -
- {/* Desktop - Command Palette Search */} -
- -
-
- - {/* Mobile Header */} -
- {isSearchExpanded ? ( -
- -
- -
-
- ) : ( - <> -

- SCENESTACK -

-
- - - - - - -
- - )} -
-
-
- ); -}); - -Header.displayName = "Header"; - -// Main content component -const MainContent: React.FC = memo(() => { - const { activeTab, searchResults, selectedMediaId, handleSelectMedia, error, handleSearch, isSearchLoading } = useUIContext(); - const watchlist = useWatchlistStore(state => state.watchlist); - const watchlistIds = useMemo(() => getWatchlistIds(watchlist), [watchlist]); - const isDbLoading = useWatchlistStore(state => state.isLoading); - - const handleBackToHome = () => { - handleSearch(""); - }; - - return ( -
-
- {error &&

{error}

} - - {/* Search Loading State */} - {isSearchLoading && ( -
-
-
-
-

Searching...

-
- )} - - {/* Search Results */} - {!isSearchLoading && searchResults.length > 0 ? ( -
-
- -
-

- Search Results - - ({searchResults.length} found) - -

-
- -
- ) : !isSearchLoading && isDbLoading ? ( -
-

Loading your watchlist...

-
- ) : !isSearchLoading && ( - -
Loading...
-
- }> - {activeTab === "discover" && } - {activeTab === "lists" && } - {activeTab === "recommendations" && } - {activeTab === "stats" && } - - )} -
-
- ); -}); - -MainContent.displayName = "MainContent"; - -// Modals component with lazy loading -const Modals: React.FC = () => { - const { - detailedMedia, - animatingMedia, - handleCloseModal, - isSettingsOpen, - closeSettings, - isNotificationsOpen, - closeNotifications, - handleSearch, - } = useUIContext(); - const watchlist = useWatchlistStore(state => state.watchlist); - const watchlistIds = useMemo(() => getWatchlistIds(watchlist), [watchlist]); - const toggleWatchlist = useWatchlistStore(state => state.toggleWatchlist); - const toggleMovieWatched = useWatchlistStore(state => state.toggleMovieWatched); - const toggleEpisodeWatched = useWatchlistStore(state => state.toggleEpisodeWatched); - const toggleSeasonWatched = useWatchlistStore(state => state.toggleSeasonWatched); - const updateTags = useWatchlistStore(state => state.updateTags); - const exportWatchlist = useWatchlistStore(state => state.exportWatchlist); - const storeImportWatchlist = useWatchlistStore(state => state.importWatchlist); - - // Adapter for importWatchlist to match previous Context signature - const handleImportWatchlist = (event: React.ChangeEvent) => { - const file = event.target.files?.[0]; - if (file) { - if (window.confirm("Are you sure you want to overwrite your current watchlist? This action cannot be undone.")) { - storeImportWatchlist(file); - } - } - event.target.value = ""; - }; - - return ( - <> - {animatingMedia && !detailedMedia && ( - - )} - - {detailedMedia && ( - }> - item.id === detailedMedia.id)} - onSearch={handleSearch} - onToggleSeasonWatched={toggleSeasonWatched} - onUpdateTags={updateTags} - /> - - )} - - - - - - - - ); -}; - -// App layout component -const AppLayout: React.FC = () => { - const { activeTab, setActiveTab, searchResults, openSettings, openNotifications, viewAllSection, closeViewAll, handleSearch } = useUIContext(); - const [isSidebarCollapsed, setIsSidebarCollapsed] = useLocalStorage("sidebarCollapsed", false); - - // Handle tab change - clear search results if any - const handleTabChange = (tab: "discover" | "lists" | "recommendations" | "stats") => { - if (searchResults.length > 0) { - handleSearch(""); // Clear search results - } - setActiveTab(tab); - }; - - // If viewing all items in a section, render the ViewAllPage - if (viewAllSection) { - return ( -
- -
Loading...
-
- }> - - - - - ); - } - - return ( -
- setIsSidebarCollapsed((prev) => !prev)} - onOpenSettings={openSettings} - onOpenNotifications={openNotifications} - /> - -
-
- -
- - {searchResults.length === 0 && ( - - )} - - -
- ); +const WatchlistInitializer: React.FC<{ children: React.ReactNode }> = ({ children }) => { + useWatchlistInit(); + return <>{children}; }; -// Main App component -const App: React.FC = () => { - return ( - - - - - - ); -}; - -// Renders either AuthPage or the main app based on auth state const AuthenticatedApp: React.FC = () => { - const { isAuthenticated, isLoading } = useAuthContext(); + const { isAuthenticated, isLoading, user } = useAuthContext(); + const { showWelcome, closeWelcome } = useDemoWelcome(user?.isDemo); if (isLoading) { return ( @@ -397,11 +33,13 @@ const AuthenticatedApp: React.FC = () => { if (!isAuthenticated) { return (
- -
Loading...
-
- }> + +
Loading...
+ + } + >
@@ -410,13 +48,14 @@ const AuthenticatedApp: React.FC = () => { return (
- + - + - + + {showWelcome && }