import React, { useState, useEffect, useMemo, Suspense, lazy } from 'react'; import { BrowserRouter as Router, Routes, Route, Link, useNavigate, useLocation } from 'react-router-dom'; import { ThemeProvider, CssBaseline, AppBar, Toolbar, Typography, Button, Box, IconButton, Avatar, Menu, MenuItem, Drawer, Divider, List, ListItem, ListItemButton, ListItemText, Collapse, TextField, InputAdornment, Autocomplete, CircularProgress } from '@mui/material'; import MenuIcon from '@mui/icons-material/Menu'; import SearchIcon from '@mui/icons-material/Search'; import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'; import ExpandLess from '@mui/icons-material/ExpandLess'; import ExpandMore from '@mui/icons-material/ExpandMore'; import { getTheme, type CatppuccinTheme } from './theme'; import api from './api'; // Code splitting const CategoryList = lazy(() => import('./pages/CategoryList')); const CategoryFlavors = lazy(() => import('./pages/categories/CategoryFlavors')); const FlavorDetail = lazy(() => import('./pages/flavors/FlavorDetail')); const Dashboard = lazy(() => import('./pages/Dashboard')); const PublicProfile = lazy(() => import('./pages/PublicProfile')); const CommunityFeed = lazy(() => import('./pages/CommunityFeed')); const Login = lazy(() => import('./pages/Login')); const Settings = lazy(() => import('./pages/Settings')); interface SearchFlavor { id: number; name: string; image_url: string | null; category_name: string; } const GlobalSearch = () => { const [query, setQuery] = useState(''); const [options, setOptions] = useState([]); const navigate = useNavigate(); const location = useLocation(); useEffect(() => { const fetchFlavors = async () => { try { // Fetch from unpaginated search endpoint for full catalog coverage const res = await api.get('flavors/search/'); setOptions(res.data); } catch (err) { console.error('Failed to fetch search options'); } }; fetchFlavors(); }, []); // Only fetch once to avoid excessive API calls useEffect(() => { const params = new URLSearchParams(location.search); setQuery(params.get('q') || ''); }, [location.search]); const handleSearch = (e: React.FormEvent) => { e.preventDefault(); if (query.trim()) { navigate(`/?q=${encodeURIComponent(query.trim())}`); } else { navigate('/'); } }; return ( { if (typeof option === 'string') return option; return option.category_name ? `${option.name} (${option.category_name})` : (option.name || ''); }} inputValue={query} onInputChange={(_, newValue) => setQuery(typeof newValue === 'string' ? newValue : '')} onChange={(_, newValue) => { if (newValue && typeof newValue !== 'string') { navigate(`/flavor/${newValue.id}`); } }} onKeyDown={(e) => { if (e.key === 'Enter') { handleSearch(e as any); } }} renderInput={(params) => ( ), }} /> )} renderOption={(props, option) => { const { key, ...optionProps } = props as any; return ( {option.image_url && ( )} {option.name} {option.category_name} ); }} /> ); }; const App: React.FC = () => { const [themeName, setThemeName] = useState((localStorage.getItem('theme') as CatppuccinTheme) || 'mocha'); const [anchorEl, setAnchorEl] = useState(null); const [catAnchorEl, setCatAnchorEl] = useState(null); const [categories, setCategories] = useState<{name: string, slug: string}[]>([]); const [user, setUser] = useState<{username: string, avatar: string | null} | null>(null); const [loadingUser, setLoadingUser] = useState(true); const [drawerOpen, setDrawerOpen] = useState(false); const [catOpen, setCatOpen] = useState(false); useEffect(() => { const fetchData = async () => { try { const token = localStorage.getItem('token'); const [userRes, catRes] = await Promise.all([ token ? api.get('users/me/') : Promise.resolve({ data: null }), api.get('categories/') ]); if (userRes.data && userRes.data.username) { setUser(userRes.data); if (userRes.data.theme) setThemeName(userRes.data.theme); } else { setUser(null); } const catData = Array.isArray(catRes.data) ? catRes.data : (catRes.data.results || []); setCategories(catData); } catch (err) { setUser(null); } finally { setLoadingUser(false); } }; fetchData(); }, []); const theme = useMemo(() => getTheme(themeName), [themeName]); const handleThemeChange = async (newTheme: CatppuccinTheme) => { setThemeName(newTheme); localStorage.setItem('theme', newTheme); if (user) { try { await api.patch('users/update_theme/', { theme: newTheme }); } catch (err) { console.error('Failed to update theme on server'); } } setAnchorEl(null); }; const handleLogout = () => { localStorage.removeItem('token'); setUser(null); window.location.href = '/'; }; const drawer = ( {user?.username ? user.username.charAt(0).toUpperCase() : '?'} setDrawerOpen(false)} style={{ color: 'inherit', textDecoration: 'none' }}>Holy Flavors setDrawerOpen(false)}> setDrawerOpen(false)}> setCatOpen(!catOpen)}> {catOpen ? : } {categories.map(cat => ( setDrawerOpen(false)} sx={{ pl: 4 }} > ))} {user ? ( <> setDrawerOpen(false)}> setDrawerOpen(false)}> ) : ( setDrawerOpen(false)}> )} ); return ( theme.zIndex.drawer + 1 }}> Holy Flavors Holy setCatAnchorEl(null)} elevation={3} sx={{ mt: 1 }} > {categories.map(cat => ( setCatAnchorEl(null)} > {cat.name} ))} {!loadingUser && ( user ? ( setAnchorEl(e.currentTarget)} sx={{ ml: 1 }}> {user.username ? user.username.charAt(0).toUpperCase() : '?'} setAnchorEl(null)} elevation={3} sx={{ mt: 1 }} > setAnchorEl(null)}>Dashboard setAnchorEl(null)}>Settings Logout ) : ( ) )} setDrawerOpen(true)} sx={{ ml: 1 }} > setDrawerOpen(false)} > {drawer} }> } /> } /> } /> } /> } /> } /> } /> } /> 404 - Page Not Found} /> ); }; export default App;