src/
├── app/ App entry, QueryClient, routes (lazy-loaded)
├── components/
│ ├── ui/ shadcn/ui primitives + ErrorState, EmptyState
│ ├── layout/ Header, Sidebar, PlayerBar, MobileTabBar
│ └── features/ Search, NowPlaying
├── context/ AuthContext, AuthProvider (global auth state)
├── hooks/ useSpotifyApi, useSpotifyQueries, useSpotifyMutations,
│ useAuthToken, useTheme, useProfileQuery
├── lib/
│ ├── api/ SpotifyApiClient + domain services
│ ├── auth/ PKCE auth service
│ └── utils/ tokenUtils
├── pages/ Dashboard, Login, Profile, Settings
├── stores/ Zustand — useContentStore (UI state only)
└── types/ Centralized TypeScript types (auth, spotify, playback, enums)
-
Auth: PKCE →
lib/auth/auth.service.ts→ tokens in localStorage →AuthProviderexposes{ accessToken, isLoading }→AuthContext -
API gate:
useSpotifyApi()reads token fromAuthContext→ returnsSpotifyApi | null(null when loading or unauthenticated) -
Queries:
useSpotifyQueries.tshooks useuseSpotifyApi()withenabled: api !== null→ React Query manages caching, polling, and invalidation -
Mutations:
useSpotifyMutations.tshooks useuseSpotifyApi()insidemutationFn→ On success, invalidate relevant query keys -
UI: Components read from React Query cache → re-render automatically on data changes →
useContentStore(Zustand) drives which view shows in the main panel
Login → redirectToSpotifyAuthorize() → Spotify OAuth → callback ?code=
→ AuthProvider.fetchToken() exchanges code for tokens
→ stores in localStorage → setAccessToken()
→ ProtectedRoute reads { isLoading, accessToken }
→ isLoading=true shows spinner, false+null shows Login, false+token renders app
Token refresh: getValidAccessToken() checks expiry with 5-min buffer, refreshes if needed.
401 responses: SpotifyApiClient interceptor auto-refreshes and retries once. Redirects to /login on second failure.
QueryClientlives outside the React tree to prevent cache loss on re-renderuseSpotifyApi()returnsnull(not throws) when unauthenticated — queries useenabled: api !== nullto skip gracefullyuseContentStoreis the single source of truth for which view is displayed in the main panelstrict: trueis on in tsconfig — no implicitany, no unchecked optional accesses