import { useState, useEffect, useRef } from 'react'
import { Routes, Route, NavLink, Navigate, useLocation } from 'react-router-dom'
import { getMe, getNewMediaCount, checkAuth, appAuthLogout } from './api'
import { useAuth } from './AuthContext'
import AppLogin from './pages/AppLogin'
import AppSetup from './pages/AppSetup'
import Login from './pages/Login'
import Dashboard from './pages/Dashboard'
import Feed from './pages/Feed'
import Users from './pages/Users'
import UserPosts from './pages/UserPosts'
import Downloads from './pages/Downloads'
import Search from './pages/Search'
import Gallery from './pages/Gallery'
import Duplicates from './pages/Duplicates'
import Scrape from './pages/Scrape'
import Videos from './pages/Videos'
import VideoDetail from './pages/VideoDetail'
import VideoUpload from './pages/VideoUpload'
import UserManagement from './pages/UserManagement'
// Route key mapping for nav items (null = always visible, undefined = no check needed)
const allNavItems = [
{ to: '/', label: 'Home', icon: HomeIcon, exact: true, routeKey: 'dashboard' },
{ to: '/feed', label: 'Feed', icon: FeedIcon, routeKey: 'feed' },
{ to: '/users', label: 'Users', icon: UsersIcon, routeKey: 'users' },
{ to: '/search', label: 'Search', icon: SearchIcon, routeKey: 'users' },
{ to: '/downloads', label: 'Downloads', icon: DownloadIcon, routeKey: 'downloads' },
{ to: '/gallery', label: 'Gallery', icon: GalleryNavIcon, routeKey: 'gallery' },
{ to: '/videos', label: 'Videos', icon: VideoNavIcon, routeKey: 'videos' },
{ to: '/scrape', label: 'Scrape', icon: ScrapeIcon, routeKey: 'scrape' },
{ to: '/settings', label: 'Settings', icon: SettingsIcon, routeKey: 'settings' },
]
const allMobileNavItems = [
{ to: '/', label: 'Home', icon: HomeIcon, exact: true, routeKey: 'dashboard' },
{ to: '/users', label: 'Users', icon: UsersIcon, routeKey: 'users' },
{ to: '/gallery', label: 'Gallery', icon: GalleryNavIcon, routeKey: 'gallery' },
{ to: '/search', label: 'Search', icon: SearchIcon, routeKey: 'users' },
{ to: '/more', label: 'More', icon: MoreIcon },
]
const allMoreNavItems = [
{ to: '/feed', label: 'Feed', icon: FeedIcon, routeKey: 'feed' },
{ to: '/downloads', label: 'Downloads', icon: DownloadIcon, routeKey: 'downloads' },
{ to: '/videos', label: 'Videos', icon: VideoNavIcon, routeKey: 'videos' },
{ to: '/scrape', label: 'Scrape', icon: ScrapeIcon, routeKey: 'scrape' },
{ to: '/settings', label: 'Settings', icon: SettingsIcon, routeKey: 'settings' },
]
function ProtectedRoute({ routeKey, children }) {
const { hasRoute, appUser } = useAuth()
// 'admin' is a special key — only admin role can access
if (routeKey === 'admin') {
if (appUser?.role !== 'admin') return
return children
}
if (!hasRoute(routeKey)) {
return
}
return children
}
export default function App() {
const { appUser, setupRequired, loading: authLoading, hasRoute } = useAuth()
// Show auth screens if needed
if (authLoading) {
return (
)
}
if (setupRequired) return
if (!appUser) return
// Filter nav items based on route permissions
const navItems = allNavItems.filter((item) =>
!item.routeKey || hasRoute(item.routeKey)
)
const mobileNavItems = allMobileNavItems.filter((item) =>
!item.routeKey || hasRoute(item.routeKey)
)
const moreNavItems = allMoreNavItems.filter((item) =>
!item.routeKey || hasRoute(item.routeKey)
)
// Add admin nav item for admins
if (appUser.role === 'admin') {
navItems.push({ to: '/admin/users', label: 'Admin', icon: AdminIcon })
moreNavItems.push({ to: '/admin/users', label: 'Admin', icon: AdminIcon })
}
return
}
function AppShell({ navItems, mobileNavItems, moreNavItems, hasRoute, appUser }) {
const { logout } = useAuth()
const [currentUser, setCurrentUser] = useState(null)
const [moreOpen, setMoreOpen] = useState(false)
const [authWarning, setAuthWarning] = useState(null)
const authPollRef = useRef(null)
const location = useLocation()
useEffect(() => {
getMe().then((data) => {
if (!data.error) {
setCurrentUser(data)
}
})
}, [])
// Auth validity polling (every 5 min)
useEffect(() => {
const poll = () => {
checkAuth().then((data) => {
if (data && !data.error && !data.valid) {
setAuthWarning(data.error || 'Auth expired')
} else if (data?.valid) {
setAuthWarning(null)
}
})
}
poll()
authPollRef.current = setInterval(poll, 5 * 60 * 1000)
return () => clearInterval(authPollRef.current)
}, [])
// Close "more" menu on route change
useEffect(() => {
setMoreOpen(false)
}, [location.pathname])
// New media badge
const [newMediaCount, setNewMediaCount] = useState(0)
useEffect(() => {
const fetchCount = () => {
getNewMediaCount().then((data) => {
if (!data.error) setNewMediaCount(data.count || 0)
})
}
fetchCount()
const id = setInterval(fetchCount, 60000)
return () => clearInterval(id)
}, [])
// Instantly clear badge when on gallery page
useEffect(() => {
if (location.pathname.startsWith('/gallery')) {
setNewMediaCount(0)
}
}, [location.pathname])
const refreshUser = () => {
getMe().then((data) => {
if (!data.error) {
setCurrentUser(data)
}
})
}
const handleLogout = async () => {
await appAuthLogout()
logout()
}
const isMoreActive = moreNavItems.some((item) =>
item.to !== '/more' && location.pathname.startsWith(item.to)
)
return (
{/* Desktop Sidebar */}
{/* Main Content */}
{/* Auth Warning Banner */}
{authWarning && (
Auth expired —{' '}
Update credentials in Settings
)}
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
{/* Mobile Bottom Nav */}