chore: add eslint for unused imports (#242)

* chore: add eslint for unused imports check

- adds eslint with typescript and svelte plugins
- configures pre-commit hook to run eslint
- focuses on catching unused vars/imports
- ignores build output directories
- found 51 unused vars across the codebase

* fix: add svelte runes and crypto to eslint globals

* fix: resolve all eslint unused variable errors

- fixed catch blocks to use _ prefix for unused error params
- fixed interface type signatures with _ prefix for unused callback params
- removed unused variables (albumId, isOwnProfile)
- disabled base no-unused-vars rule in favor of @typescript-eslint version
- configured @typescript-eslint/no-unused-vars to:
- ignore args/vars with ^_ pattern
- skip caught error checking (caughtErrors: none)

all 24 eslint errors now fixed - lint passes cleanly

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

authored by zzstoatzz.io Claude and committed by GitHub af64fa89 796e818c

+7
.pre-commit-config.yaml
··· 35 35 files: ^frontend/ 36 36 pass_filenames: false 37 37 38 + - id: eslint 39 + name: eslint 40 + entry: bash -c 'cd frontend && bun run lint' 41 + language: system 42 + files: ^frontend/.*\.(ts|svelte)$ 43 + pass_filenames: false 44 + 38 45 - repo: https://github.com/pre-commit/pre-commit-hooks 39 46 rev: v6.0.0 40 47 hooks:
frontend/bun.lockb

This is a binary file and will not be displayed.

+82
frontend/eslint.config.js
··· 1 + import js from '@eslint/js'; 2 + import tsParser from '@typescript-eslint/parser'; 3 + import tsPlugin from '@typescript-eslint/eslint-plugin'; 4 + import sveltePlugin from 'eslint-plugin-svelte'; 5 + import svelteParser from 'svelte-eslint-parser'; 6 + 7 + export default [ 8 + { 9 + ignores: [ 10 + '.svelte-kit/**', 11 + '.vercel/**', 12 + 'build/**', 13 + 'node_modules/**', 14 + 'dist/**', 15 + '**/*.config.js' 16 + ] 17 + }, 18 + js.configs.recommended, 19 + { 20 + files: ['**/*.ts', '**/*.svelte'], 21 + languageOptions: { 22 + parser: tsParser, 23 + parserOptions: { 24 + project: './tsconfig.json', 25 + extraFileExtensions: ['.svelte'] 26 + }, 27 + globals: { 28 + // browser globals 29 + window: 'readonly', 30 + document: 'readonly', 31 + console: 'readonly', 32 + fetch: 'readonly', 33 + localStorage: 'readonly', 34 + sessionStorage: 'readonly', 35 + setTimeout: 'readonly', 36 + clearTimeout: 'readonly', 37 + alert: 'readonly', 38 + confirm: 'readonly', 39 + navigator: 'readonly', 40 + location: 'readonly', 41 + history: 'readonly', 42 + crypto: 'readonly', 43 + // svelte 5 runes 44 + $state: 'readonly', 45 + $derived: 'readonly', 46 + $effect: 'readonly', 47 + $props: 'readonly', 48 + $bindable: 'readonly', 49 + $inspect: 'readonly' 50 + } 51 + }, 52 + plugins: { 53 + '@typescript-eslint': tsPlugin 54 + }, 55 + rules: { 56 + 'no-unused-vars': 'off', 57 + '@typescript-eslint/no-unused-vars': [ 58 + 'error', 59 + { 60 + argsIgnorePattern: '^_', 61 + varsIgnorePattern: '^_', 62 + caughtErrors: 'none' 63 + } 64 + ] 65 + } 66 + }, 67 + { 68 + files: ['**/*.svelte'], 69 + languageOptions: { 70 + parser: svelteParser, 71 + parserOptions: { 72 + parser: tsParser 73 + } 74 + }, 75 + plugins: { 76 + svelte: sveltePlugin 77 + }, 78 + rules: { 79 + ...sveltePlugin.configs.recommended.rules 80 + } 81 + } 82 + ];
+6 -1
frontend/package.json
··· 9 9 "preview": "vite preview", 10 10 "prepare": "svelte-kit sync || echo ''", 11 11 "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 12 - "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch" 12 + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 13 + "lint": "eslint ." 13 14 }, 14 15 "devDependencies": { 15 16 "@sveltejs/adapter-auto": "^7.0.0", ··· 17 18 "@sveltejs/adapter-static": "^3.0.10", 18 19 "@sveltejs/kit": "^2.43.2", 19 20 "@sveltejs/vite-plugin-svelte": "^6.2.0", 21 + "@typescript-eslint/eslint-plugin": "^8.46.4", 22 + "@typescript-eslint/parser": "^8.46.4", 23 + "eslint": "^9.39.1", 24 + "eslint-plugin-svelte": "^3.13.0", 20 25 "svelte": "^5.39.5", 21 26 "svelte-check": "^4.3.2", 22 27 "typescript": "^5.9.2",
+1 -1
frontend/src/lib/components/BrokenTracks.svelte
··· 53 53 }); 54 54 55 55 if (response.ok) { 56 - const data = await response.json(); 56 + await response.json(); 57 57 toast.success(`restored record for ${trackTitle}`); 58 58 // remove from broken tracks list 59 59 brokenTracks = brokenTracks.filter(t => t.id !== trackId);
+4 -4
frontend/src/lib/components/HandleSearch.svelte
··· 4 4 5 5 interface Props { 6 6 selected: FeaturedArtist[]; 7 - onAdd: (artist: FeaturedArtist) => void; 8 - onRemove: (did: string) => void; 7 + onAdd: (_artist: FeaturedArtist) => void; 8 + onRemove: (_did: string) => void; 9 9 maxFeatures?: number; 10 10 disabled?: boolean; 11 11 } ··· 32 32 results = data.results; 33 33 showResults = true; 34 34 } 35 - } catch (e) { 36 - console.error('search failed:', e); 35 + } catch (_e) { 36 + console.error('search failed:', _e); 37 37 } finally { 38 38 searching = false; 39 39 }
+2 -2
frontend/src/lib/components/LikeButton.svelte
··· 8 8 initialLiked?: boolean; 9 9 disabled?: boolean; 10 10 disabledReason?: string; 11 - onLikeChange?: (liked: boolean) => void; 11 + onLikeChange?: (_liked: boolean) => void; 12 12 } 13 13 14 14 let { trackId, trackTitle, initialLiked = false, disabled = false, disabledReason, onLikeChange }: Props = $props(); ··· 52 52 toast.info(`unliked ${trackTitle}`); 53 53 } 54 54 } 55 - } catch (e) { 55 + } catch { 56 56 // revert on error 57 57 liked = previousState; 58 58 toast.error('failed to update like');
+1 -1
frontend/src/lib/components/TrackActionsMenu.svelte
··· 85 85 } 86 86 } 87 87 closeMenu(); 88 - } catch (e) { 88 + } catch { 89 89 // revert on error 90 90 liked = previousState; 91 91 toast.error('failed to update like');
+1 -1
frontend/src/lib/components/TrackItem.svelte
··· 10 10 interface Props { 11 11 track: Track; 12 12 isPlaying?: boolean; 13 - onPlay: (track: Track) => void; 13 + onPlay: (_track: Track) => void; 14 14 isAuthenticated?: boolean; 15 15 hideAlbum?: boolean; 16 16 }
+6 -7
frontend/src/lib/uploader.svelte.ts
··· 17 17 } 18 18 19 19 interface UploadProgressCallback { 20 - onProgress?: (loaded: number, total: number) => void; 21 - onSuccess?: (uploadId: string) => void; 22 - onError?: (error: string) => void; 20 + onProgress?: (_loaded: number, _total: number) => void; 21 + onSuccess?: (_uploadId: string) => void; 22 + onError?: (_error: string) => void; 23 23 } 24 24 25 25 // global upload manager using Svelte 5 runes ··· 137 137 this.activeUploads.delete(taskId); 138 138 toast.error('lost connection to server'); 139 139 }; 140 - } catch (e) { 140 + } catch { 141 141 toast.dismiss(toastId); 142 - const errorMsg = 'failed to parse server response'; 143 - toast.error(errorMsg); 142 + toast.error('failed to parse server response'); 144 143 if (callbacks?.onError) { 145 - callbacks.onError(errorMsg); 144 + callbacks.onError('failed to parse server response'); 146 145 } 147 146 } 148 147 } else {
+16 -17
frontend/src/routes/portal/+page.svelte
··· 32 32 33 33 // upload form fields 34 34 let title = ''; 35 - let albumId: string | null = null; 36 35 let albumTitle = ''; 37 36 let file: File | null = null; 38 37 let imageFile: File | null = null; ··· 78 77 auth.setSessionId(data.session_id); 79 78 await auth.initialize(); 80 79 } 81 - } catch (e) { 82 - console.error('failed to exchange token:', e); 80 + } catch (_e) { 81 + console.error('failed to exchange token:', _e); 83 82 } 84 83 85 84 // remove exchange_token from URL ··· 95 94 await loadMyTracks(); 96 95 await loadArtistProfile(); 97 96 await loadMyAlbums(); 98 - } catch (e) { 99 - console.error('error loading portal data:', e); 97 + } catch (_e) { 98 + console.error('error loading portal data:', _e); 100 99 error = 'failed to load portal data'; 101 100 } finally { 102 101 loading = false; ··· 113 112 const data = await response.json(); 114 113 tracks = data.tracks; 115 114 } 116 - } catch (e) { 117 - console.error('failed to load tracks:', e); 115 + } catch (_e) { 116 + console.error('failed to load tracks:', _e); 118 117 } finally { 119 118 loadingTracks = false; 120 119 } ··· 131 130 bio = artist.bio || ''; 132 131 avatarUrl = artist.avatar_url || ''; 133 132 } 134 - } catch (e) { 135 - console.error('failed to load artist profile:', e); 133 + } catch (_e) { 134 + console.error('failed to load artist profile:', _e); 136 135 } 137 136 } 138 137 ··· 145 144 const data = await response.json(); 146 145 albums = data.albums; 147 146 } 148 - } catch (e) { 149 - console.error('failed to load albums:', e); 147 + } catch (_e) { 148 + console.error('failed to load albums:', _e); 150 149 } finally { 151 150 loadingAlbums = false; 152 151 } ··· 177 176 const data = await response.json(); 178 177 toast.error(data.detail || 'failed to upload cover'); 179 178 } 180 - } catch (e) { 181 - console.error('failed to upload album cover:', e); 179 + } catch (_e) { 180 + console.error('failed to upload album cover:', _e); 182 181 toast.error('failed to upload cover art'); 183 182 } 184 183 } ··· 369 368 file = null; 370 369 return; 371 370 } 372 - } catch (e) { 373 - console.error('failed to validate file size:', e); 371 + } catch (_e) { 372 + console.error('failed to validate file size:', _e); 374 373 // continue anyway - server will validate 375 374 } 376 375 ··· 395 394 imageFile = null; 396 395 return; 397 396 } 398 - } catch (e) { 399 - console.error('failed to validate image size:', e); 397 + } catch (_e) { 398 + console.error('failed to validate image size:', _e); 400 399 // continue anyway - server will validate 401 400 } 402 401
+4 -4
frontend/src/routes/profile/setup/+page.svelte
··· 35 35 auth.setSessionId(data.session_id); 36 36 await auth.initialize(); 37 37 } 38 - } catch (e) { 39 - console.error('failed to exchange token:', e); 38 + } catch (_e) { 39 + console.error('failed to exchange token:', _e); 40 40 } 41 41 42 42 // remove exchange_token from URL ··· 54 54 55 55 // try to fetch avatar from bluesky 56 56 await fetchAvatar(); 57 - } catch (e) { 57 + } catch { 58 58 auth.clearSession(); 59 59 window.location.href = '/login'; 60 60 } finally { ··· 78 78 window.location.href = '/portal'; 79 79 return; 80 80 } 81 - } catch (e) { 81 + } catch { 82 82 // profile doesn't exist, which is expected 83 83 } finally { 84 84 fetchingAvatar = false;
+2 -6
frontend/src/routes/u/[handle]/+page.svelte
··· 22 22 let analytics: Analytics | null = $state(null); 23 23 let analyticsLoading = $state(false); 24 24 25 - function checkIsOwnProfile(): boolean { 26 - return auth.user !== null && artist !== null && auth.user.did === artist.did; 27 - } 28 - let isOwnProfile = $derived(checkIsOwnProfile()); 29 25 30 26 async function handleLogout() { 31 27 await auth.logout(); ··· 44 40 if (response.ok) { 45 41 analytics = await response.json(); 46 42 } 47 - } catch (e) { 48 - console.error('failed to load analytics:', e); 43 + } catch (_e) { 44 + console.error('failed to load analytics:', _e); 49 45 } finally { 50 46 // ensure loading state shows for at least minDisplayTime 51 47 const elapsed = Date.now() - startTime;