diff --git a/TODO.md b/TODO.md index 3cb3b69..20be1bf 100644 --- a/TODO.md +++ b/TODO.md @@ -138,72 +138,51 @@ - Document backend API requirements for live data - **Note**: Deferred to Phase 5 after basic features are implemented -## Phase 4 – Application Architecture & Routing +## Phase 4 – Application Architecture & Routing (IN PROGRESS) -- [ ] Create SvelteKit route structure in `src/routes/`: - ``` - src/routes/ - ├── +layout.svelte # Root layout (header, footer, navigation) - ├── +layout.ts # Root load function (user prefs, i18n) - ├── +page.svelte # Homepage (/) - ├── +page.ts # Homepage data (featured matches) - ├── matches/ - │ ├── +page.svelte # Match listing (/matches) - │ └── +page.ts # Load matches with filters - ├── player/ - │ └── [id]/ - │ ├── +page.svelte # Player profile (/player/[id]) - │ └── +page.ts # Load player data - └── match/ - └── [id]/ - ├── +layout.svelte # Match layout (tabs navigation) - ├── +layout.ts # Load match data (shared by all tabs) - ├── +page.svelte # Match overview (/match/[id]) - ├── economy/ - │ └── +page.svelte - ├── details/ - │ └── +page.svelte - ├── flashes/ - │ └── +page.svelte - ├── damage/ - │ └── +page.svelte - └── chat/ - └── +page.svelte - ``` -- [ ] Implement root layout (`src/routes/+layout.svelte`): - - Global header with logo, navigation, search bar - - Language switcher component (dropdown with flag icons) - - Theme toggle (light/dark/auto) with smooth transitions - - Footer with links, version info, backend status indicator - - Toast/notification system (top-right positioned) - - Loading bar (NProgress-style) for route transitions -- [ ] Create reusable layout components in `src/lib/components/layout/`: - - `Header.svelte`: responsive navigation, mobile menu drawer - - `Footer.svelte`: links, social, donation info - - `SearchBar.svelte`: global search with keyboard shortcuts (Cmd+K) - - `ThemeToggle.svelte`: theme switcher with icon animations - - `LanguageSwitcher.svelte`: i18n selector with persistence - - `Breadcrumbs.svelte`: contextual navigation breadcrumbs -- [ ] Configure load functions with error handling: - - Implement `+page.ts` load functions for data fetching - - Add error boundaries: `+error.svelte` for each route level - - Create loading skeletons: `+loading.svelte` (SvelteKit streaming) - - Handle 404s, 500s, and API errors gracefully - - Implement redirect logic for invalid match/player IDs -- [ ] Set up state management with Svelte stores (`src/lib/stores/`): - - `preferences.ts`: user settings (theme, language, units, favorites) - - `search.ts`: search query, filters, recent searches - - `cache.ts`: client-side data cache with TTL - - `toast.ts`: notification queue and display logic - - `auth.ts`: user authentication state (if auth is implemented) - - Use `writable`, `derived`, and `readable` stores appropriately - - Persist critical stores to localStorage with sync +- [x] Create SvelteKit route structure in `src/routes/` (partial): + - ✅ Created: `+layout.svelte`, `+layout.ts`, `+error.svelte` + - ✅ Homepage: `+page.svelte`, `+page.ts` with featured matches loader + - ✅ Matches listing: `matches/+page.ts` with query params + - ✅ Players: `players/+page.ts` placeholder + - ✅ About: `about/+page.ts` static page + - ⚠️ Match detail routes (nested layouts): **TODO Phase 5** + - ⚠️ Player profile `[id]` route: **TODO Phase 5** +- [x] Implement root layout (`src/routes/+layout.svelte`): + - ✅ Global header with logo and navigation (Header.svelte) + - ✅ Footer with links (Footer.svelte) + - ✅ Toast notification system (ToastContainer.svelte, top-right) + - ⚠️ Search bar with keyboard shortcuts: **TODO Phase 5** + - ⚠️ Language switcher: **TODO Phase 6 (Localization)** + - ⚠️ Theme toggle: **TODO Phase 5** + - ⚠️ Loading bar: **TODO Phase 5** +- [x] Create reusable layout components in `src/lib/components/layout/` (partial): + - ✅ `Header.svelte`: responsive navigation, mobile menu drawer + - ✅ `Footer.svelte`: links, social, donation info + - ⚠️ `SearchBar.svelte`: **TODO Phase 5** + - ⚠️ `ThemeToggle.svelte`: **TODO Phase 5** + - ⚠️ `LanguageSwitcher.svelte`: **TODO Phase 6** + - ⚠️ `Breadcrumbs.svelte`: **TODO Phase 5** +- [x] Configure load functions with error handling: + - ✅ Implemented `+page.ts` load functions (homepage, matches, players, about) + - ✅ Added error boundary: `+error.svelte` at root level (404, 500, 503 handling) + - ✅ API error handling in load functions (catch/fallback) + - ⚠️ Loading skeletons: **TODO Phase 5** + - ⚠️ Redirect logic for invalid IDs: **TODO Phase 5** +- [x] Set up state management with Svelte stores (`src/lib/stores/`): + - ✅ `preferences.ts`: theme, language, favorites, advanced stats toggle, date format + - ✅ `search.ts`: search query, filters, recent searches (localStorage) + - ✅ `toast.ts`: notification queue with auto-dismiss, typed messages + - ✅ Used `writable` stores with custom methods + - ✅ localStorage persistence with browser environment detection + - ⚠️ `cache.ts`: **Deferred to Phase 7 (Performance)** + - ⚠️ `auth.ts`: **Not needed (no authentication planned)** - [ ] Add analytics and privacy: - - Choose privacy-respecting analytics (Plausible, Umami, or self-hosted) - - Implement consent banner (GDPR-compliant) - - Create analytics utility: `trackPageView()`, `trackEvent()` - - Ensure SSR compatibility (client-side only execution) - - Add opt-out mechanism + - ⚠️ Choose analytics solution: **TODO Phase 5** + - ⚠️ Implement consent banner: **TODO Phase 5** + - ⚠️ Create analytics utility: **TODO Phase 5** + - ⚠️ Ensure SSR compatibility: **TODO Phase 5** + - ⚠️ Add opt-out mechanism: **TODO Phase 5** ## Phase 5 – Feature Delivery (Parity + Enhancements) diff --git a/src/lib/components/ui/Toast.svelte b/src/lib/components/ui/Toast.svelte new file mode 100644 index 0000000..8d186a6 --- /dev/null +++ b/src/lib/components/ui/Toast.svelte @@ -0,0 +1,49 @@ + + + diff --git a/src/lib/components/ui/ToastContainer.svelte b/src/lib/components/ui/ToastContainer.svelte new file mode 100644 index 0000000..6aa561f --- /dev/null +++ b/src/lib/components/ui/ToastContainer.svelte @@ -0,0 +1,11 @@ + + + +
+ {#each $toast as toastItem (toastItem.id)} + + {/each} +
diff --git a/src/lib/stores/index.ts b/src/lib/stores/index.ts new file mode 100644 index 0000000..21fc761 --- /dev/null +++ b/src/lib/stores/index.ts @@ -0,0 +1,12 @@ +/** + * Central export for all Svelte stores + */ + +export { preferences } from './preferences'; +export type { UserPreferences } from './preferences'; + +export { search, isSearchActive } from './search'; +export type { SearchState } from './search'; + +export { toast } from './toast'; +export type { Toast } from './toast'; diff --git a/src/lib/stores/preferences.ts b/src/lib/stores/preferences.ts new file mode 100644 index 0000000..1035dd1 --- /dev/null +++ b/src/lib/stores/preferences.ts @@ -0,0 +1,100 @@ +import { writable } from 'svelte/store'; +import { browser } from '$app/environment'; + +/** + * User preferences store + * Persisted to localStorage + */ + +export interface UserPreferences { + theme: 'cs2dark' | 'cs2light' | 'auto'; + language: string; + favoriteMap?: string; + favoritePlayers: number[]; + showAdvancedStats: boolean; + dateFormat: 'relative' | 'absolute'; + timezone: string; +} + +const defaultPreferences: UserPreferences = { + theme: 'cs2dark', + language: 'en', + favoritePlayers: [], + showAdvancedStats: false, + dateFormat: 'relative', + timezone: Intl.DateTimeFormat().resolvedOptions().timeZone +}; + +// Load preferences from localStorage +const loadPreferences = (): UserPreferences => { + if (!browser) return defaultPreferences; + + try { + const stored = localStorage.getItem('cs2wtf-preferences'); + if (stored) { + return { ...defaultPreferences, ...JSON.parse(stored) }; + } + } catch (error) { + console.error('Failed to load preferences:', error); + } + + return defaultPreferences; +}; + +// Create the store +const createPreferencesStore = () => { + const { subscribe, set, update } = writable(loadPreferences()); + + return { + subscribe, + set: (value: UserPreferences) => { + if (browser) { + localStorage.setItem('cs2wtf-preferences', JSON.stringify(value)); + } + set(value); + }, + update: (fn: (value: UserPreferences) => UserPreferences) => { + update((current) => { + const newValue = fn(current); + if (browser) { + localStorage.setItem('cs2wtf-preferences', JSON.stringify(newValue)); + } + return newValue; + }); + }, + reset: () => { + if (browser) { + localStorage.removeItem('cs2wtf-preferences'); + } + set(defaultPreferences); + }, + + // Convenience methods + setTheme: (theme: UserPreferences['theme']) => { + update((prefs) => ({ ...prefs, theme })); + }, + setLanguage: (language: string) => { + update((prefs) => ({ ...prefs, language })); + }, + addFavoritePlayer: (playerId: number) => { + update((prefs) => ({ + ...prefs, + favoritePlayers: [...new Set([...prefs.favoritePlayers, playerId])] + })); + }, + removeFavoritePlayer: (playerId: number) => { + update((prefs) => ({ + ...prefs, + favoritePlayers: prefs.favoritePlayers.filter((id) => id !== playerId) + })); + }, + toggleAdvancedStats: () => { + update((prefs) => ({ + ...prefs, + showAdvancedStats: !prefs.showAdvancedStats + })); + } + }; +}; + +export const preferences = createPreferencesStore(); diff --git a/src/lib/stores/search.ts b/src/lib/stores/search.ts new file mode 100644 index 0000000..a551d35 --- /dev/null +++ b/src/lib/stores/search.ts @@ -0,0 +1,118 @@ +import { writable, derived } from 'svelte/store'; +import { browser } from '$app/environment'; + +/** + * Search state store + * Manages search queries and recent searches + */ + +export interface SearchState { + query: string; + recentSearches: string[]; + filters: { + map?: string; + playerId?: number; + dateFrom?: string; + dateTo?: string; + }; +} + +const defaultState: SearchState = { + query: '', + recentSearches: [], + filters: {} +}; + +// Load recent searches from localStorage +const loadRecentSearches = (): string[] => { + if (!browser) return []; + + try { + const stored = localStorage.getItem('cs2wtf-recent-searches'); + if (stored) { + return JSON.parse(stored); + } + } catch (error) { + console.error('Failed to load recent searches:', error); + } + + return []; +}; + +// Create the store +const createSearchStore = () => { + const { subscribe, set, update } = writable({ + ...defaultState, + recentSearches: loadRecentSearches() + }); + + return { + subscribe, + set, + update, + + // Set search query + setQuery: (query: string) => { + update((state) => ({ ...state, query })); + }, + + // Clear search query + clearQuery: () => { + update((state) => ({ ...state, query: '' })); + }, + + // Add to recent searches (max 10) + addRecentSearch: (query: string) => { + if (!query.trim()) return; + + update((state) => { + const recent = [query, ...state.recentSearches.filter((q) => q !== query)].slice(0, 10); + + if (browser) { + localStorage.setItem('cs2wtf-recent-searches', JSON.stringify(recent)); + } + + return { ...state, recentSearches: recent }; + }); + }, + + // Clear recent searches + clearRecentSearches: () => { + if (browser) { + localStorage.removeItem('cs2wtf-recent-searches'); + } + update((state) => ({ ...state, recentSearches: [] })); + }, + + // Set filters + setFilters: (filters: SearchState['filters']) => { + update((state) => ({ ...state, filters })); + }, + + // Update single filter + setFilter: (key: keyof SearchState['filters'], value: unknown) => { + update((state) => ({ + ...state, + filters: { ...state.filters, [key]: value } + })); + }, + + // Clear filters + clearFilters: () => { + update((state) => ({ ...state, filters: {} })); + }, + + // Reset entire search state + reset: () => { + set({ ...defaultState, recentSearches: loadRecentSearches() }); + } + }; +}; + +export const search = createSearchStore(); + +// Derived store: is search active? +export const isSearchActive = derived( + search, + ($search) => $search.query.length > 0 || Object.keys($search.filters).length > 0 +); diff --git a/src/lib/stores/toast.ts b/src/lib/stores/toast.ts new file mode 100644 index 0000000..a558068 --- /dev/null +++ b/src/lib/stores/toast.ts @@ -0,0 +1,84 @@ +import { writable } from 'svelte/store'; + +/** + * Toast notification store + * Manages temporary notifications to the user + */ + +export interface Toast { + id: string; + message: string; + type: 'success' | 'error' | 'warning' | 'info'; + duration?: number; // milliseconds, default 5000 + dismissible?: boolean; +} + +type ToastInput = Omit; + +const createToastStore = () => { + const { subscribe, update } = writable([]); + + let nextId = 0; + + const addToast = (toast: ToastInput) => { + const id = `toast-${++nextId}`; + const duration = toast.duration ?? 5000; + const dismissible = toast.dismissible ?? true; + + const newToast: Toast = { + ...toast, + id, + duration, + dismissible + }; + + update((toasts) => [...toasts, newToast]); + + // Auto-dismiss after duration + if (duration > 0) { + setTimeout(() => { + removeToast(id); + }, duration); + } + + return id; + }; + + const removeToast = (id: string) => { + update((toasts) => toasts.filter((t) => t.id !== id)); + }; + + return { + subscribe, + + // Add toast with specific type + success: (message: string, duration?: number) => { + return addToast({ message, type: 'success', duration }); + }, + + error: (message: string, duration?: number) => { + return addToast({ message, type: 'error', duration }); + }, + + warning: (message: string, duration?: number) => { + return addToast({ message, type: 'warning', duration }); + }, + + info: (message: string, duration?: number) => { + return addToast({ message, type: 'info', duration }); + }, + + // Add custom toast + add: addToast, + + // Remove specific toast + dismiss: removeToast, + + // Clear all toasts + clear: () => { + update(() => []); + } + }; +}; + +export const toast = createToastStore(); diff --git a/src/routes/+error.svelte b/src/routes/+error.svelte new file mode 100644 index 0000000..6168d4b --- /dev/null +++ b/src/routes/+error.svelte @@ -0,0 +1,97 @@ + + + + {status} - {getErrorTitle(status)} | CS2.WTF + + +
+ +
+ +
+ {status} +
+ + +

+ {getErrorTitle(status)} +

+ + +

+ {getErrorMessage(status)} +

+ + + {#if import.meta.env?.DEV && error} +
+

+ Debug Info: +

+
{JSON.stringify(
+							error,
+							null,
+							2
+						)}
+
+ {/if} + + +
+ + + +
+ + +

+ If this problem persists, please + + report it on GitHub + +

+
+
+
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index ccefdef..488213c 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -2,6 +2,7 @@ import '../app.css'; import Header from '$lib/components/layout/Header.svelte'; import Footer from '$lib/components/layout/Footer.svelte'; + import ToastContainer from '$lib/components/ui/ToastContainer.svelte'; let { children } = $props(); @@ -12,4 +13,7 @@ {@render children()}