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