Files
Job-Info/SVELTE_PATTERNS_REFERENCE.md
T
aewing 42ff3e8f33
Build & Deploy / Test & Lint (push) Has been cancelled
Build & Deploy / Security Scan (push) Has been cancelled
Code Quality / Code Quality Checks (push) Has been cancelled
Code Quality / Dependency Vulnerabilities (push) Has been cancelled
Code Quality / Performance Testing (push) Has been cancelled
Build & Deploy / Build Docker Images (push) Has been cancelled
Build & Deploy / Deploy to Kubernetes (push) Has been cancelled
Add Mode6Test: copy of Mode5Test running on port 3000
2026-01-20 13:43:29 +00:00

14 KiB

/**

  • REFERENCE TOOL: Svelte + SvelteKit Best Practices & Patterns
  • Every Svelte file follows these patterns. No exceptions. */

// ============================================================================ // PATTERN 1: Svelte Store Pattern // ============================================================================

/**

  • STORE: authStore
  • PURPOSE: Centralized auth state (user, tokens, login status)
  • UPDATES: Called only through explicit update functions
  • SUBSCRIBERS: Any component needing auth state
  • Files reference this store: components that check auth, routes that require auth */

// File: src/stores/auth.ts import { writable, derived } from 'svelte/store';

/**

  • USER STORE
  • Holds current user info from Microsoft */ export const user = writable<User | null>(null);

/**

  • ACCESS TOKEN STORE
  • Holds current access token (short-lived, 1 hour)
  • RULE: Cleared when token expires
  • RULE: Never persisted to localStorage */ export const accessToken = writable<string | null>(null);

/**

  • TOKEN EXPIRY STORE
  • When does current token expire (timestamp in ms) */ export const tokenExpiry = writable<number | null>(null);

/**

  • DERIVED: Is authenticated?
  • Returns true if user exists and token is not expired */ export const isAuthenticated = derived( [user, tokenExpiry], ([$user, $expiry]) => { return !!$user && $expiry && Date.now() < $expiry; } );

/**

  • UPDATE FUNCTION: Set user after login
  • CALLED BY: OAuth callback handler
  • UPDATES: user, accessToken, tokenExpiry
  • SIDE EFFECTS: Stores token in sessionStorage, sets up refresh timer */ export async function setAuthUser( userInfo: User, token: string, expiresIn: number ): Promise { // Update stores user.set(userInfo); accessToken.set(token); tokenExpiry.set(Date.now() + expiresIn * 1000);

// Store in sessionStorage (safe, lost on browser close) sessionStorage.setItem('ms_access_token', token); sessionStorage.setItem('ms_token_expiry', (Date.now() + expiresIn * 1000).toString());

// Set up automatic refresh timer (5 minutes before expiry) const refreshIn = (expiresIn * 1000) - (5 * 60 * 1000); setTimeout(() => { refreshAccessToken(); }, refreshIn);

console.log('Auth user set:', userInfo.displayName); }

/**

  • UPDATE FUNCTION: Refresh token via backend

  • CALLED BY: Automatic timer (5 min before expiry), before API calls

  • SIDE EFFECTS: Updates accessToken, tokenExpiry, sessionStorage */ export async function refreshAccessToken(): Promise { try { const response = await fetch('/api/auth/refresh', { method: 'POST', headers: { 'Authorization': Bearer ${get(user)?.id} }, });

    if (!response.ok) { // Token refresh failed clearAuth(); // Force re-login throw new Error('Token refresh failed'); }

    const { accessToken: newToken, expiresIn } = await response.json();

    // Update stores accessToken.set(newToken); tokenExpiry.set(Date.now() + expiresIn * 1000); sessionStorage.setItem('ms_access_token', newToken); sessionStorage.setItem('ms_token_expiry', (Date.now() + expiresIn * 1000).toString());

    // Reset refresh timer setTimeout(() => { refreshAccessToken(); }, (expiresIn * 1000) - (5 * 60 * 1000));

    console.log('Access token refreshed'); } catch (error) { console.error('Token refresh error:', error); clearAuth(); } }

/**

  • UPDATE FUNCTION: Logout
  • CALLED BY: Logout button, auth failures
  • SIDE EFFECTS: Clears all auth stores, sessionStorage, redirects */ export async function clearAuth(): Promise { user.set(null); accessToken.set(null); tokenExpiry.set(null); sessionStorage.removeItem('ms_access_token'); sessionStorage.removeItem('ms_token_expiry');

// Call backend logout (optional, clears refresh token from PocketBase) try { await fetch('/api/auth/logout', { method: 'POST' }); } catch (e) { console.error('Logout API error:', e); }

console.log('Auth cleared'); }

// ============================================================================ // PATTERN 2: Svelte Component with Auth Requirement // ============================================================================

/**

  • COMPONENT: JobList.svelte
  • PURPOSE: Display list of jobs from Microsoft Graph
  • REQUIRES: User to be authenticated
  • Reactive: $isAuthenticated, $jobs, searchQuery
  • Events: None (pure presentation)
  • API calls: Loads jobs via GraphService */

// File: src/components/JobList.svelte

<script lang="ts"> // Imports - organize by type import { onMount } from 'svelte'; import { isAuthenticated, accessToken } from '../stores/auth'; import { jobs, jobsLoading, searchJobs } from '../stores/jobs'; import JobCard from './JobCard.svelte'; import SearchBar from './SearchBar.svelte'; // Props (none in this case) export let jobCategory: string = 'all'; // Reactive state let searchQuery = ''; let filteredJobs: Job[] = []; // Lifecycle: Load jobs on component mount onMount(async () => { if (!$isAuthenticated) { return; // Don't load if not authenticated } jobsLoading.set(true); try { await searchJobs(jobCategory); } catch (error) { console.error('Failed to load jobs:', error); } finally { jobsLoading.set(false); } }); // Reactive: Filter jobs based on search query $: filteredJobs = $jobs.filter(job => job.title.toLowerCase().includes(searchQuery.toLowerCase()) || job.company.toLowerCase().includes(searchQuery.toLowerCase()) ); // Event handler: Search input function handleSearch(e: CustomEvent) { searchQuery = e.detail; } </script>

{#if !$isAuthenticated}

Please sign in to view jobs
{:else if $jobsLoading}
Loading jobs...
{:else if filteredJobs.length === 0}
No jobs found matching your search
{:else}
{#each filteredJobs as job (job.id)} {/each}
{/if} <style> /* Use TailwindCSS classes only, no custom CSS needed here */ </style>

// ============================================================================ // PATTERN 3: API Service Layer // ============================================================================

/**

  • SERVICE: graphService
  • PURPOSE: Handle all Microsoft Graph API calls with proper headers
  • DEPENDENCIES: accessToken store, error handling
  • Exports: Functions for each Graph API operation
  • Error handling: Throws errors that UI catches */

// File: src/services/graphService.ts import { get } from 'svelte/store'; import { accessToken, refreshAccessToken, clearAuth } from '../stores/auth';

/**

  • Helper: Ensure token is valid before API call
  • Checks if token expired, refreshes if needed
  • Throws if no token available */ async function ensureValidToken(): Promise { const token = get(accessToken); if (!token) { throw new Error('No access token available - not authenticated'); }

// Check if token might be expired, refresh proactively const expiry = Number(sessionStorage.getItem('ms_token_expiry') || 0); if (expiry - Date.now() < 5 * 60 * 1000) { // Token expires in less than 5 minutes, refresh now await refreshAccessToken(); }

return get(accessToken) || token; }

/**

  • API CALL: Get list of files in OneDrive
  • Endpoint: GET https://graph.microsoft.com/v1.0/me/drive/root/children
  • Scopes required: Files.Read.All
  • Caching: Results cached in IndexedDB via jobsStore
  • Errors:
    • 401: Token invalid, user redirected to login
    • 403: Insufficient permissions
    • 404: Folder not found
    • 429: Rate limited, should retry with backoff */ export async function listFiles(folderId: string = 'root'): Promise<File[]> { const token = await ensureValidToken();

const endpoint = folderId === 'root' ? 'https://graph.microsoft.com/v1.0/me/drive/root/children' : https://graph.microsoft.com/v1.0/me/drive/items/${folderId}/children;

try { const response = await fetch(endpoint, { headers: { Authorization: Bearer ${token}, 'Content-Type': 'application/json', }, });

if (response.status === 401) {
  // Token invalid, clear auth and redirect
  clearAuth();
  throw new Error('Session expired - please log in again');
}

if (!response.ok) {
  const error = await response.json().catch(() => ({ message: response.statusText }));
  throw new Error(`Graph API error ${response.status}: ${error.message}`);
}

const data = await response.json();
return data.value || [];

} catch (error) { console.error('Graph API error:', error); throw error; } }

// ============================================================================ // PATTERN 4: Data Store (Derived from API) // ============================================================================

/**

  • STORE: jobs
  • PURPOSE: Cached list of jobs loaded from device storage (IndexedDB)
  • UPDATES: Called by services after fetching from Graph API
  • PERSISTENCE: IndexedDB database
  • STRUCTURE:
    • jobs: Job[] - Current list of jobs
    • jobsLoading: boolean - Loading state
    • jobsError: string | null - Last error if any */

// File: src/stores/jobs.ts import { writable } from 'svelte/store'; import { loadJobsFromDB, saveJobsToDBt } from '../services/storageService';

export const jobs = writable<Job[]>([]); export const jobsLoading = writable(false); export const jobsError = writable<string | null>(null);

/**

  • Load jobs from device storage
  • Called on app init
  • Populates jobs store from IndexedDB */ export async function loadJobs(): Promise { jobsLoading.set(true); try { const cachedJobs = await loadJobsFromDB(); jobs.set(cachedJobs); jobsError.set(null); } catch (error) { console.error('Failed to load jobs:', error); jobsError.set((error as Error).message); } finally { jobsLoading.set(false); } }

/**

  • Search jobs locally (no API call needed)
  • All job data is already in device storage
  • This filters locally for instant results */ export async function searchJobs(query: string): Promise<Job[]> { // Get current jobs const allJobs = get(jobs);

// Filter based on query const results = allJobs.filter(job => job.title.toLowerCase().includes(query.toLowerCase()) || job.company.toLowerCase().includes(query.toLowerCase()) || job.description.toLowerCase().includes(query.toLowerCase()) );

return results; }

// ============================================================================ // PATTERN 5: Layout Component (Root) // ============================================================================

/**

  • COMPONENT: +layout.svelte
  • PURPOSE: Root layout for all pages
  • FEATURES: Navigation, auth check, error handling
  • Lifecycle: Checks auth on mount, refreshes token */

// File: src/routes/+layout.svelte

<script lang="ts"> import { onMount } from 'svelte'; import { isAuthenticated, user, clearAuth } from '../stores/auth'; import { loadJobs } from '../stores/jobs'; import Navigation from '../components/Navigation.svelte'; import ErrorBoundary from '../components/ErrorBoundary.svelte'; let error: Error | null = null; onMount(async () => { // Load cached jobs on app startup try { await loadJobs(); } catch (e) { console.error('Failed to load initial jobs:', e); error = e as Error; } }); </script>

{#if error} <ErrorBoundary {error} /> {/if}

<style> /* Global styles as needed */ </style>

// ============================================================================ // PATTERN 6: Route Page with Auth Guard // ============================================================================

/**

  • PAGE: +page.svelte (Home)
  • PURPOSE: Job listing homepage
  • AUTH: Redirects to login if not authenticated */

// File: src/routes/+page.svelte

<script lang="ts"> import { goto } from '$app/navigation'; import { onMount } from 'svelte'; import { isAuthenticated } from '../stores/auth'; import JobList from '../components/JobList.svelte'; onMount(() => { if (!$isAuthenticated) { goto('/signin'); } }); </script>

{#if $isAuthenticated}

Job Listings

{/if}

// File: src/routes/signin/+page.svelte (OAuth login page)

<script lang="ts"> import { goto } from '$app/navigation'; import { isAuthenticated } from '../../stores/auth'; onMount(() => { if ($isAuthenticated) { goto('/'); // Already logged in } }); function handleLogin() { // Initiate OAuth flow initiateOAuthFlow(); } </script>
Sign in with Microsoft

// ============================================================================ // SUMMARY // ============================================================================

/**

  • SVELTE PATTERNS - GOLDEN RULES:
    1. STORES: Centralize state, update via explicit functions
    1. COMPONENTS: Keep reactive, let stores handle state
    1. SERVICES: Handle API calls with error handling, never expose tokens
    1. LIFECYCLE: Use onMount for async operations
    1. DERIVED: Use derived stores for computed state (isAuthenticated)
    1. REACTIVITY: Use $store syntax to subscribe in templates
    1. PROPS: Always document and type check
    1. ERROR HANDLING: Catch all async operations, display to user
    1. PERSISTENCE: Use IndexedDB for device storage, sessionStorage for tokens
    1. TESTING: Every component independently testable */