chore(frontend): ESLint dla Vue i TypeScriptu

Konfiguracja we flat configu: zalecane zestawy js, typescript-eslint oraz
eslint-plugin-vue. Bloki <script setup lang="ts"> obsługuje vue-eslint-parser
przekazujący zawartość do parsera TypeScriptu.

Odstępstwa od domyślnych reguł:

- vue/multi-word-component-names wyłączone - kafelki panelu mają nazwy
  własne i nie każda jest dwuczłonowa
- no-unused-vars przepuszcza nazwy z prefiksem podkreślenia
- no-console wyłączone dla testu dymnego, który raportuje wynik na stdout

Szablony przepuszczone przez eslint --fix, co ujednoliciło łamanie
atrybutów i domykanie znaczników. Po zmianach kontrola typów, build
i test dymny nadal przechodzą.

Dochodzą skrypty npm: lint (z --max-warnings 0) oraz lint:fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
lgooseandClaude Bot committed 2026-08-21 01:34:55 +02:00
1 parent 1b5ce2109b
commit 105f3e6c6a
17 files changed
+1682 -84

No files matched your search

+51
View File
@@ -0,0 +1,51 @@
import js from '@eslint/js'
import globals from 'globals'
import ts from 'typescript-eslint'
import vue from 'eslint-plugin-vue'
// Kolejność ma znaczenie: w flat config późniejszy wpis nadpisuje wcześniejszy,
// więc wyjątki od reguł muszą stać za blokiem, który te reguły ustawia.
export default ts.config(
{ ignores: ['dist/**', 'node_modules/**'] },
js.configs.recommended,
ts.configs.recommended,
vue.configs['flat/recommended'],
{
// Pliki .vue parsuje vue-eslint-parser, a bloki <script setup lang="ts">
// przekazuje dalej do parsera TypeScriptu.
files: ['**/*.vue'],
languageOptions: {
parserOptions: { parser: ts.parser },
},
},
{
files: ['src/**/*.{ts,vue}'],
languageOptions: { globals: globals.browser },
},
{
files: ['*.mjs', '*.ts'],
languageOptions: { globals: globals.node },
},
{
rules: {
// Kafelki panelu mają nazwy własne, nie zawsze dwuczłonowe.
'vue/multi-word-component-names': 'off',
'no-console': ['warn', { allow: ['warn', 'error'] }],
'@typescript-eslint/no-unused-vars': [
'error',
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
],
},
},
{
// Test dymny raportuje wynik na stdout - to jego zadanie, nie zapomniany debug.
files: ['smoke-test.mjs'],
rules: { 'no-console': 'off' },
},
)
+1286
View File
File diff suppressed because it is too large. Load diff
+8
View File
@@ -8,6 +8,8 @@
"dev": "vite",
"build": "vue-tsc --noEmit && vite build",
"preview": "vite preview",
"lint": "eslint . --max-warnings 0",
"lint:fix": "eslint . --fix",
"typecheck": "vue-tsc --noEmit",
"smoke": "node smoke-test.mjs"
},
@@ -16,11 +18,17 @@
"vue": "^3.5.41"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@types/node": "^26.2.0",
"@vitejs/plugin-vue": "^6.0.8",
"eslint": "^10.8.1",
"eslint-plugin-vue": "^10.10.0",
"globals": "^17.11.0",
"jsdom": "^30.0.1",
"typescript": "^5.9.3",
"typescript-eslint": "^8.67.0",
"vite": "^8.2.2",
"vue-eslint-parser": "^10.4.1",
"vue-tsc": "^3.3.10"
}
}
+20 -6
View File
@@ -26,11 +26,14 @@ onMounted(() => {
</script>
<template>
<div class="background-glow bg-glow-1"></div>
<div class="background-glow bg-glow-2"></div>
<div class="background-glow bg-glow-1" />
<div class="background-glow bg-glow-2" />
<div class="app-container">
<AppHeader :status="status" @open-settings="settingsOpen = true" />
<AppHeader
:status="status"
@open-settings="settingsOpen = true"
/>
<main class="dashboard-grid">
<section class="grid-column left-column">
@@ -40,15 +43,26 @@ onMounted(() => {
<section class="grid-column right-column">
<AddTrackPanel :status="status" />
<UploadsPanel :status="status" @open-library="libraryOpen = true" />
<UploadsPanel
:status="status"
@open-library="libraryOpen = true"
/>
<QueuePanel :status="status" />
<HistoryPanel :status="status" />
</section>
</main>
</div>
<SettingsModal v-if="settingsOpen" :status="status" @close="settingsOpen = false" />
<UploadsLibraryModal v-if="libraryOpen" :status="status" @close="libraryOpen = false" />
<SettingsModal
v-if="settingsOpen"
:status="status"
@close="settingsOpen = false"
/>
<UploadsLibraryModal
v-if="libraryOpen"
:status="status"
@close="libraryOpen = false"
/>
<ToastHost />
</template>
+17 -5
View File
@@ -29,8 +29,13 @@ async function submit() {
<template>
<div class="glass-card add-track-panel">
<h2 class="card-title"><PlusCircle /> Dodaj do kolejki</h2>
<form class="add-form" @submit.prevent="submit">
<h2 class="card-title">
<PlusCircle /> Dodaj do kolejki
</h2>
<form
class="add-form"
@submit.prevent="submit"
>
<div class="input-with-icon">
<Search class="input-icon" />
<input
@@ -40,10 +45,17 @@ async function submit() {
placeholder="Wklej link YouTube lub wpisz wyszukiwanie..."
:disabled="busy"
required
/>
>
</div>
<button type="submit" class="btn btn-primary" :disabled="busy">
<span v-if="busy" class="spinner"></span>
<button
type="submit"
class="btn btn-primary"
:disabled="busy"
>
<span
v-if="busy"
class="spinner"
/>
<span v-else><Plus /> Dodaj</span>
</button>
</form>
+11 -3
View File
@@ -18,7 +18,9 @@ const label = computed(() =>
<template>
<header class="main-header">
<div class="logo-area">
<div class="logo-icon"><Music /></div>
<div class="logo-icon">
<Music />
</div>
<div class="logo-text">
<h1>ŻyrarBOT</h1>
<span class="subtitle">Kontroler Web</span>
@@ -26,11 +28,17 @@ const label = computed(() =>
</div>
<div class="header-actions">
<button class="btn btn-secondary" @click="$emit('openSettings')">
<button
class="btn btn-secondary"
@click="$emit('openSettings')"
>
<Settings /> Ustawienia
</button>
<div class="bot-status-badge">
<span class="status-indicator" :class="activeChannel ? 'online' : 'offline'"></span>
<span
class="status-indicator"
:class="activeChannel ? 'online' : 'offline'"
/>
<span>{{ label }}</span>
</div>
</div>
+24 -6
View File
@@ -37,19 +37,34 @@ async function replay(entry: HistoryTrack) {
<template>
<div class="glass-card history-panel">
<div class="queue-header">
<h2 class="card-title"><History /> Historia odtwarzania</h2>
<h2 class="card-title">
<History /> Historia odtwarzania
</h2>
<span class="queue-count">{{ tracksLabel(history.length) }}</span>
</div>
<div class="queue-list-container list-short">
<ul class="queue-list">
<li v-for="entry in history" :key="entry.id" class="queue-item">
<div class="queue-index"><RotateCcw /></div>
<li
v-for="entry in history"
:key="entry.id"
class="queue-item"
>
<div class="queue-index">
<RotateCcw />
</div>
<div class="queue-track-info">
<div class="queue-track-title" :title="entry.title">{{ entry.title }}</div>
<div
class="queue-track-title"
:title="entry.title"
>
{{ entry.title }}
</div>
<div class="queue-track-meta">
{{ entry.duration ? formatTime(entry.duration) : '--:--' }}
<template v-if="entry.is_local"> • plik lokalny</template>
<template v-if="entry.is_local">
• plik lokalny
</template>
</div>
</div>
<div class="queue-item-actions">
@@ -63,7 +78,10 @@ async function replay(entry: HistoryTrack) {
</div>
</li>
<div v-if="!history.length" class="empty-queue-message">
<div
v-if="!history.length"
class="empty-queue-message"
>
<Clock />
<p>Historia odtwarzania jest pusta.</p>
</div>
+18 -4
View File
@@ -14,11 +14,25 @@ onUnmounted(() => window.removeEventListener('keydown', onKeydown))
</script>
<template>
<div class="modal-overlay" @click.self="$emit('close')">
<div class="modal-card glass-card" :class="{ wide }">
<div
class="modal-overlay"
@click.self="$emit('close')"
>
<div
class="modal-card glass-card"
:class="{ wide }"
>
<div class="modal-header">
<h2 class="card-title"><slot name="title" /></h2>
<button class="btn-icon" title="Zamknij" @click="$emit('close')"><X /></button>
<h2 class="card-title">
<slot name="title" />
</h2>
<button
class="btn-icon"
title="Zamknij"
@click="$emit('close')"
>
<X />
</button>
</div>
<div class="modal-body">
<slot />
+56 -16
View File
@@ -106,43 +106,74 @@ async function cycleLoop() {
</div>
<div class="visualizer-container">
<div class="vinyl-record" :class="{ playing }">
<div class="vinyl-center"></div>
<div class="vinyl-grooves"></div>
<div class="vinyl-glow"></div>
<div class="vinyl-icon"><Music4 /></div>
<div
class="vinyl-record"
:class="{ playing }"
>
<div class="vinyl-center" />
<div class="vinyl-grooves" />
<div class="vinyl-glow" />
<div class="vinyl-icon">
<Music4 />
</div>
</div>
<div class="music-wave" :class="{ active: playing }">
<span v-for="bar in 5" :key="bar"></span>
<div
class="music-wave"
:class="{ active: playing }"
>
<span
v-for="bar in 5"
:key="bar"
/>
</div>
</div>
<div class="now-playing-info">
<h3 class="track-title">{{ current?.title ?? 'Nic nie jest odtwarzane' }}</h3>
<h3 class="track-title">
{{ current?.title ?? 'Nic nie jest odtwarzane' }}
</h3>
<p class="track-artist">
{{ current ? (current.url ? 'Źródło: YouTube' : 'Źródło: plik lokalny') : 'Połącz bota i dodaj utwór' }}
</p>
<a v-if="current?.url" :href="current.url" target="_blank" class="track-link-btn">
<a
v-if="current?.url"
:href="current.url"
target="_blank"
class="track-link-btn"
>
<ExternalLink /> Otwórz w YouTube
</a>
</div>
<div v-if="current" class="track-progress-container">
<div
v-if="current"
class="track-progress-container"
>
<span class="time-text">{{ formatTime(elapsed) }}</span>
<div class="progress-bar-bg">
<div class="progress-bar-fill" :style="{ width: `${percent}%` }"></div>
<div
class="progress-bar-fill"
:style="{ width: `${percent}%` }"
/>
</div>
<span class="time-text">{{ duration > 0 ? formatTime(duration) : 'Live' }}</span>
</div>
<div class="playback-state-bar">
<span v-if="loopButton.badge" class="badge">
<span
v-if="loopButton.badge"
class="badge"
>
<component :is="loopButton.icon" /> {{ loopButton.badge }}
</span>
</div>
<div class="player-controls">
<button class="btn-icon btn-secondary" title="Zatrzymaj i wyczyść" @click="stop">
<button
class="btn-icon btn-secondary"
title="Zatrzymaj i wyczyść"
@click="stop"
>
<Square />
</button>
<button
@@ -152,7 +183,11 @@ async function cycleLoop() {
>
<component :is="playing ? Pause : Play" />
</button>
<button class="btn-icon btn-secondary" title="Pomiń utwór" @click="skip">
<button
class="btn-icon btn-secondary"
title="Pomiń utwór"
@click="skip"
>
<SkipForward />
</button>
<button
@@ -166,7 +201,12 @@ async function cycleLoop() {
</div>
<div class="volume-container">
<button class="btn-icon-small" title="Głośność"><component :is="volumeIcon" /></button>
<button
class="btn-icon-small"
title="Głośność"
>
<component :is="volumeIcon" />
</button>
<input
type="range"
class="premium-slider"
@@ -175,7 +215,7 @@ async function cycleLoop() {
:value="volume"
@input="onVolumeInput"
@change="commitVolume"
/>
>
<span class="volume-value">{{ volume }}%</span>
</div>
</div>
+62 -12
View File
@@ -62,20 +62,48 @@ function commit() {
}"
draggable="true"
>
<div class="drag-handle" title="Przeciągnij, aby zmienić kolejność"><GripVertical /></div>
<div
class="drag-handle"
title="Przeciągnij, aby zmienić kolejność"
>
<GripVertical />
</div>
<div v-if="index === queueIndex" class="queue-index" title="Aktualnie odtwarzane">
<div
v-if="index === queueIndex"
class="queue-index"
title="Aktualnie odtwarzane"
>
<Volume2 class="playing-icon" />
</div>
<div v-else class="queue-index">{{ index + 1 }}</div>
<div
v-else
class="queue-index"
>
{{ index + 1 }}
</div>
<div class="queue-track-info">
<div class="queue-track-title" :title="track.title">{{ track.title }}</div>
<div
class="queue-track-title"
:title="track.title"
>
{{ track.title }}
</div>
<div class="track-plays-control">
<Repeat class="plays-icon" />
<span class="plays-label" title="Pozostało odtworzeń z kolejki">Pozostało:</span>
<button class="btn-tiny-action" title="Zmniejsz o 1" @click="step(-1)"><Minus /></button>
<span
class="plays-label"
title="Pozostało odtworzeń z kolejki"
>Pozostało:</span>
<button
class="btn-tiny-action"
title="Zmniejsz o 1"
@click="step(-1)"
>
<Minus />
</button>
<input
v-model.number="plays"
type="number"
@@ -85,22 +113,44 @@ function commit() {
@focus="editing = true"
@blur="commit"
@keyup.enter="($event.target as HTMLInputElement).blur()"
/>
<button class="btn-tiny-action" title="Zwiększ o 1" @click="step(1)"><Plus /></button>
>
<button
class="btn-tiny-action"
title="Zwiększ o 1"
@click="step(1)"
>
<Plus />
</button>
</div>
</div>
<div class="queue-item-actions">
<button class="btn-action-icon" title="Odtwarzaj od tej pozycji" @click="$emit('jump', index)">
<button
class="btn-action-icon"
title="Odtwarzaj od tej pozycji"
@click="$emit('jump', index)"
>
<Play />
</button>
<button class="btn-action-icon" title="Przesuń w górę" @click="$emit('moveUp', index)">
<button
class="btn-action-icon"
title="Przesuń w górę"
@click="$emit('moveUp', index)"
>
<ArrowUp />
</button>
<button class="btn-action-icon" title="Przesuń w dół" @click="$emit('moveDown', index)">
<button
class="btn-action-icon"
title="Przesuń w dół"
@click="$emit('moveDown', index)"
>
<ArrowDown />
</button>
<button class="btn-action-icon delete" title="Usuń z kolejki" @click="$emit('remove', track.id)">
<button
class="btn-action-icon delete"
title="Usuń z kolejki"
@click="$emit('remove', track.id)"
>
<Trash2 />
</button>
</div>
+7 -2
View File
@@ -78,7 +78,9 @@ const warnInvalidPlays = () => notify('Wartość musi być z przedziału 1-999',
<template>
<div class="glass-card queue-panel">
<div class="queue-header">
<h2 class="card-title"><ListMusic /> Kolejka odtwarzania</h2>
<h2 class="card-title">
<ListMusic /> Kolejka odtwarzania
</h2>
<span class="queue-count">{{ tracksLabel(queue.length) }}</span>
</div>
@@ -105,7 +107,10 @@ const warnInvalidPlays = () => notify('Wartość musi być z przedziału 1-999',
@set-plays="setPlays"
@invalid-plays="warnInvalidPlays"
/>
<div v-if="!queue.length" class="empty-queue-message">
<div
v-if="!queue.length"
class="empty-queue-message"
>
<Disc />
<p>Kolejka jest pusta. Dodaj coś!</p>
</div>
+31 -9
View File
@@ -50,7 +50,9 @@ const saveLogChannel = (guildId: string, channelId: string) =>
<template>
<ModalShell @close="$emit('close')">
<template #title><Settings /> Ustawienia ŻyrarBOT</template>
<template #title>
<Settings /> Ustawienia ŻyrarBOT
</template>
<section class="settings-section divided">
<h3><Repeat /> Domyślna liczba odtworzeń w kolejce</h3>
@@ -68,8 +70,13 @@ const saveLogChannel = (guildId: string, channelId: string) =>
:max="MAX_PLAYS"
@focus="editing = true"
@blur="editing = false"
/>
<button class="btn btn-primary" @click="saveDefaultPlays"><Save /> Zapisz</button>
>
<button
class="btn btn-primary"
@click="saveDefaultPlays"
>
<Save /> Zapisz
</button>
</div>
</section>
@@ -81,12 +88,22 @@ const saveLogChannel = (guildId: string, channelId: string) =>
</p>
<div class="guilds-list">
<div v-for="guild in guilds" :key="guild.guild_id" class="guild-setting-row glass-card-subtle">
<div
v-for="guild in guilds"
:key="guild.guild_id"
class="guild-setting-row glass-card-subtle"
>
<div class="guild-info">
<div class="guild-icon"><Shield /></div>
<div class="guild-icon">
<Shield />
</div>
<div>
<div class="guild-name">{{ guild.guild_name }}</div>
<div class="guild-id">ID: {{ guild.guild_id }}</div>
<div class="guild-name">
{{ guild.guild_name }}
</div>
<div class="guild-id">
ID: {{ guild.guild_id }}
</div>
</div>
</div>
<div class="guild-control">
@@ -95,7 +112,9 @@ const saveLogChannel = (guildId: string, channelId: string) =>
:value="guild.active_notification_channel_id ?? ''"
@change="saveLogChannel(guild.guild_id, ($event.target as HTMLSelectElement).value)"
>
<option value="">-- Brak / Domyślny --</option>
<option value="">
-- Brak / Domyślny --
</option>
<option
v-for="channel in guild.text_channels"
:key="channel.channel_id"
@@ -107,7 +126,10 @@ const saveLogChannel = (guildId: string, channelId: string) =>
</div>
</div>
<div v-if="!guilds.length" class="empty-queue-message">
<div
v-if="!guilds.length"
class="empty-queue-message"
>
<Server />
<p>Brak serwerów Discord w pamięci bota.</p>
</div>
+6 -1
View File
@@ -14,7 +14,12 @@ const icons = {
<template>
<div class="toast-container">
<TransitionGroup name="toast">
<div v-for="toast in toasts" :key="toast.id" class="toast" :class="toast.type">
<div
v-for="toast in toasts"
:key="toast.id"
class="toast"
:class="toast.type"
>
<component :is="icons[toast.type]" />
<span>{{ toast.message }}</span>
</div>
+22 -5
View File
@@ -8,16 +8,33 @@ defineEmits<{ enqueue: [filename: string]; remove: [filename: string] }>()
<template>
<li class="queue-item">
<div class="queue-index"><FileAudio /></div>
<div class="queue-index">
<FileAudio />
</div>
<div class="queue-track-info">
<div class="queue-track-title" :title="file.filename">{{ file.filename }}</div>
<div class="queue-track-meta">{{ file.size_mb }} MB</div>
<div
class="queue-track-title"
:title="file.filename"
>
{{ file.filename }}
</div>
<div class="queue-track-meta">
{{ file.size_mb }} MB
</div>
</div>
<div class="queue-item-actions">
<button class="btn btn-primary btn-compact" title="Dodaj do kolejki" @click="$emit('enqueue', file.filename)">
<button
class="btn btn-primary btn-compact"
title="Dodaj do kolejki"
@click="$emit('enqueue', file.filename)"
>
<Plus /> Dodaj
</button>
<button class="btn-action-icon delete" title="Usuń z serwera" @click="$emit('remove', file.filename)">
<button
class="btn-action-icon delete"
title="Usuń z serwera"
@click="$emit('remove', file.filename)"
>
<Trash2 />
</button>
</div>
@@ -22,8 +22,13 @@ function play(filename: string) {
</script>
<template>
<ModalShell wide @close="$emit('close')">
<template #title><FolderUp /> Wszystkie pliki na serwerze</template>
<ModalShell
wide
@close="$emit('close')"
>
<template #title>
<FolderUp /> Wszystkie pliki na serwerze
</template>
<p class="settings-desc">
Pełna lista plików zapisanych w serwerowej bazie danych (posortowana od ostatnio używanych /
@@ -38,7 +43,10 @@ function play(filename: string) {
@enqueue="play"
@remove="remove"
/>
<div v-if="!files.length" class="empty-queue-message">
<div
v-if="!files.length"
class="empty-queue-message"
>
<FolderOpen />
<p>Brak przesłanych plików na serwerze.</p>
</div>
+28 -7
View File
@@ -34,18 +34,33 @@ function play(filename: string) {
<template>
<div class="glass-card uploads-panel">
<div class="queue-header">
<h2 class="card-title"><FolderUp /> Serwerowa baza plików</h2>
<h2 class="card-title">
<FolderUp /> Serwerowa baza plików
</h2>
<span class="queue-count">{{ filesLabel(files.length) }}</span>
</div>
<div class="upload-area">
<input ref="fileInput" type="file" hidden multiple @change="onFilesPicked" />
<button class="btn btn-secondary btn-block" :disabled="uploading" @click="fileInput?.click()">
<input
ref="fileInput"
type="file"
hidden
multiple
@change="onFilesPicked"
>
<button
class="btn btn-secondary btn-block"
:disabled="uploading"
@click="fileInput?.click()"
>
<UploadCloud /> Prześlij plik wideo/audio na serwer
</button>
<div v-if="uploading" class="upload-progress">
<div
v-if="uploading"
class="upload-progress"
>
<div class="progress-bar-bg">
<div class="progress-bar-fill indeterminate"></div>
<div class="progress-bar-fill indeterminate" />
</div>
<span class="time-text">{{ uploadLabel }}</span>
</div>
@@ -60,14 +75,20 @@ function play(filename: string) {
@enqueue="play"
@remove="remove"
/>
<div v-if="!files.length" class="empty-queue-message">
<div
v-if="!files.length"
class="empty-queue-message"
>
<FolderOpen />
<p>Brak przesłanych plików na serwerze.</p>
</div>
</ul>
</div>
<button class="btn btn-secondary btn-block" @click="$emit('openLibrary')">
<button
class="btn btn-secondary btn-block"
@click="$emit('openLibrary')"
>
<FolderOpen /> Pokaż wszystkie pliki na serwerze ({{ files.length }})
</button>
</div>
+24 -5
View File
@@ -41,15 +41,34 @@ async function toggle() {
<template>
<div class="glass-card connection-panel">
<h2 class="card-title"><Radio /> Kanał Głosowy</h2>
<h2 class="card-title">
<Radio /> Kanał Głosowy
</h2>
<div class="control-row">
<select v-model="selected" class="premium-select" :disabled="!channels.length">
<option v-if="!channels.length" value="">Brak dostępnych kanałów</option>
<option v-for="channel in channels" :key="channel.channel_id" :value="channel.channel_id">
<select
v-model="selected"
class="premium-select"
:disabled="!channels.length"
>
<option
v-if="!channels.length"
value=""
>
Brak dostępnych kanałów
</option>
<option
v-for="channel in channels"
:key="channel.channel_id"
:value="channel.channel_id"
>
{{ channel.guild_name }} - {{ channel.channel_name }}
</option>
</select>
<button class="btn" :class="active ? 'btn-danger' : 'btn-primary btn-glow'" @click="toggle">
<button
class="btn"
:class="active ? 'btn-danger' : 'btn-primary btn-glow'"
@click="toggle"
>
<component :is="active ? LogOut : LogIn" />
{{ active ? 'Odłącz' : 'Połącz' }}
</button>