fix(frontend): resolve build-blocking lint and type errors

next build runs ESLint + full type-check and fails on errors (not just
warnings) - the production build was broken. Fixes: unused imports/vars,
any-typed diff/preference lookups replaced with the actual union/Record
types, ternary-as-statement flagged by no-unused-expressions, unescaped
JSX entities, a UserPreferences interface missing two fields the video
editor already reads/writes at runtime, and an import of
ColumnVisibilityState which @tanstack/react-table does not export
(the real name is VisibilityState).
This commit is contained in:
2026-08-11 14:18:26 +02:00
parent 5a1f9a2e50
commit b51c79e889
9 changed files with 29 additions and 34 deletions
@@ -45,8 +45,6 @@ const RULE_CATALOG = [
{ code: 'REMOTE_CONFLICT', severity: 'ERROR', label: 'Remote conflict', desc: 'The video was modified on YouTube after the last sync — local and remote diverged.' }, { code: 'REMOTE_CONFLICT', severity: 'ERROR', label: 'Remote conflict', desc: 'The video was modified on YouTube after the last sync — local and remote diverged.' },
] as const; ] as const;
type RuleCode = typeof RULE_CATALOG[number]['code'];
// ── Helpers ─────────────────────────────────────────────────────────────────── // ── Helpers ───────────────────────────────────────────────────────────────────
function SevBadge({ sev }: { sev: string }) { function SevBadge({ sev }: { sev: string }) {
@@ -156,7 +154,7 @@ export default function LintingPage() {
function toggleSelect(id: string) { function toggleSelect(id: string) {
setSelected((prev) => { setSelected((prev) => {
const next = new Set(prev); const next = new Set(prev);
next.has(id) ? next.delete(id) : next.add(id); if (next.has(id)) next.delete(id); else next.add(id);
return next; return next;
}); });
} }
@@ -172,7 +170,7 @@ export default function LintingPage() {
function toggleFix(id: string) { function toggleFix(id: string) {
setExpandedFix((prev) => { setExpandedFix((prev) => {
const next = new Set(prev); const next = new Set(prev);
next.has(id) ? next.delete(id) : next.add(id); if (next.has(id)) next.delete(id); else next.add(id);
return next; return next;
}); });
} }
@@ -483,7 +483,7 @@ export default function SettingsPage() {
<section className={styles.section}> <section className={styles.section}>
<h2 className={styles.sectionTitle}>Publishing Schedule</h2> <h2 className={styles.sectionTitle}>Publishing Schedule</h2>
<p className={styles.sectionDesc}> <p className={styles.sectionDesc}>
Define the time slots when videos are allowed to publish. Used by the "Next free slot" feature in the video editor. Define the time slots when videos are allowed to publish. Used by the &quot;Next free slot&quot; feature in the video editor.
</p> </p>
<div className={styles.scheduleBox}> <div className={styles.scheduleBox}>
@@ -581,7 +581,7 @@ export default function SettingsPage() {
/> />
<span>Show Canva link in video editor</span> <span>Show Canva link in video editor</span>
<span className={styles.toggleDesc}> <span className={styles.toggleDesc}>
Adds a link next to YouTube Studio that searches Canva for the video's Game Title. Adds a link next to YouTube Studio that searches Canva for the video&apos;s Game Title.
</span> </span>
</label> </label>
) : ( ) : (
@@ -601,7 +601,7 @@ export default function SettingsPage() {
/> />
<span>Show deleted videos tab</span> <span>Show deleted videos tab</span>
<span className={styles.toggleDesc}> <span className={styles.toggleDesc}>
Reveals a "Deleted" tab in the video list for videos removed from YouTube. They are kept locally for 30 days before being permanently deleted. Reveals a &quot;Deleted&quot; tab in the video list for videos removed from YouTube. They are kept locally for 30 days before being permanently deleted.
</span> </span>
</label> </label>
) : ( ) : (
@@ -38,8 +38,7 @@ function TagsDiff({ before, after }: { before: string[]; after: string[] }) {
function DiffRow({ field, item }: { field: string; item: PushPendingPreviewItem }) { function DiffRow({ field, item }: { field: string; item: PushPendingPreviewItem }) {
const label = FIELD_LABELS[field] ?? field; const label = FIELD_LABELS[field] ?? field;
const diff = item.diff as any; const d = item.diff[field as keyof typeof item.diff];
const d = diff[field];
if (!d) return null; if (!d) return null;
if (field === 'tags') { if (field === 'tags') {
@@ -47,7 +46,7 @@ function DiffRow({ field, item }: { field: string; item: PushPendingPreviewItem
<tr> <tr>
<td className={styles.diffField}>{label}</td> <td className={styles.diffField}>{label}</td>
<td className={styles.diffBefore}>{(d.before as string[]).join(', ') || '—'}</td> <td className={styles.diffBefore}>{(d.before as string[]).join(', ') || '—'}</td>
<td className={styles.diffAfter}><TagsDiff before={d.before} after={d.after} /></td> <td className={styles.diffAfter}><TagsDiff before={d.before as string[]} after={d.after as string[]} /></td>
</tr> </tr>
); );
} }
@@ -60,7 +59,7 @@ function DiffRow({ field, item }: { field: string; item: PushPendingPreviewItem
</tr> </tr>
); );
} }
const fmt = (v: any) => { const fmt = (v: string | number | boolean | string[] | null | undefined) => {
if (v === null || v === undefined) return '—'; if (v === null || v === undefined) return '—';
if (typeof v === 'boolean') return v ? 'Yes' : 'No'; if (typeof v === 'boolean') return v ? 'Yes' : 'No';
return String(v); return String(v);
@@ -13,7 +13,7 @@ import { SortableContext, verticalListSortingStrategy, arrayMove } from '@dnd-ki
import { import {
ArrowLeft, ExternalLink, Loader2, AlertCircle, CheckCircle2, ArrowLeft, ExternalLink, Loader2, AlertCircle, CheckCircle2,
Save, Tag, Shield, Calendar, Hash, RefreshCw, RotateCcw, Save, Tag, Shield, Calendar, Hash, RefreshCw, RotateCcw,
Plus, Minus, Layers3, X, Upload, CloudOff, Eye, EyeOff, Search, CalendarPlus, Layers3, X, Upload, CloudOff, Eye, EyeOff, Search, CalendarPlus,
ChevronDown, ChevronUp, ChevronDown, ChevronUp,
} from 'lucide-react'; } from 'lucide-react';
import { formatDistanceToNow } from 'date-fns'; import { formatDistanceToNow } from 'date-fns';
@@ -34,7 +34,7 @@ import SortableSection from '@/components/shared/SortableSection';
import f from '@/components/shared/FormField.module.css'; import f from '@/components/shared/FormField.module.css';
import styles from './page.module.css'; import styles from './page.module.css';
const PRIVACY_LABELS = { PUBLIC: 'Public', PRIVATE: 'Private', UNLISTED: 'Unlisted' } as const; const PRIVACY_LABELS: Record<string, string> = { PUBLIC: 'Public', PRIVATE: 'Private', UNLISTED: 'Unlisted' };
function utcToLocalInput(utcIso: string): string { function utcToLocalInput(utcIso: string): string {
const d = new Date(utcIso); const d = new Date(utcIso);
+5 -13
View File
@@ -3,8 +3,8 @@
import { useState, useEffect, useCallback, useMemo } from 'react'; import { useState, useEffect, useCallback, useMemo } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useSearchParams, useRouter } from 'next/navigation'; import { useSearchParams, useRouter } from 'next/navigation';
import { Download, Loader2, AlertCircle, RefreshCw, CheckCircle2, X, Plus } from 'lucide-react'; import { Download, Loader2, AlertCircle, RefreshCw, CheckCircle2, Plus } from 'lucide-react';
import { type SortingState, type ColumnVisibilityState } from '@tanstack/react-table'; import { type SortingState, type VisibilityState } from '@tanstack/react-table';
import VideoTable, { VideoRow, VIDEO_COLUMN_DEFS } from '@/components/video-table/VideoTable'; import VideoTable, { VideoRow, VIDEO_COLUMN_DEFS } from '@/components/video-table/VideoTable';
import ColumnPicker from '@/components/shared/ColumnPicker'; import ColumnPicker from '@/components/shared/ColumnPicker';
import SavedViewManager from '@/components/shared/SavedViewManager'; import SavedViewManager from '@/components/shared/SavedViewManager';
@@ -107,9 +107,9 @@ function parseExtraFilters(searchParams: URLSearchParams): Partial<VideosQuery>
const v = searchParams.get(k); const v = searchParams.get(k);
if (v !== null) { if (v !== null) {
if (k === 'embeddable' || k === 'selfDeclaredMadeForKids') { if (k === 'embeddable' || k === 'selfDeclaredMadeForKids') {
(result as any)[k] = v === 'true'; (result as Record<string, string | boolean>)[k] = v === 'true';
} else { } else {
(result as any)[k] = v; (result as Record<string, string | boolean>)[k] = v;
} }
} }
} }
@@ -133,7 +133,7 @@ export default function VideosPage() {
const { visible, order, setColumns } = useColumnPreferences('videos'); const { visible, order, setColumns } = useColumnPreferences('videos');
const columnVisibility = useMemo<ColumnVisibilityState>(() => { const columnVisibility = useMemo<VisibilityState>(() => {
const allIds = VIDEO_COLUMN_DEFS.map((c) => c.id); const allIds = VIDEO_COLUMN_DEFS.map((c) => c.id);
return Object.fromEntries(allIds.map((id) => [id, visible.includes(id)])); return Object.fromEntries(allIds.map((id) => [id, visible.includes(id)]));
}, [visible]); }, [visible]);
@@ -193,14 +193,6 @@ export default function VideosPage() {
setPage(1); setPage(1);
}, [searchParams, router]); }, [searchParams, router]);
const clearExtraFilters = useCallback(() => {
const params = new URLSearchParams(searchParams.toString());
for (const k of EXTRA_FILTER_KEYS) params.delete(k);
params.delete('pendingSync'); params.delete('remoteConflict');
params.delete('lintStatus'); params.delete('privacyStatus'); params.delete('search');
router.replace(`/videos?${params.toString()}`);
}, [searchParams, router]);
// Build combined filters for API (base tab query + extra overlays) // Build combined filters for API (base tab query + extra overlays)
const urlSearch = searchParams.get('search') ?? undefined; const urlSearch = searchParams.get('search') ?? undefined;
const urlLintStatus = searchParams.get('lintStatus') ?? undefined; const urlLintStatus = searchParams.get('lintStatus') ?? undefined;
@@ -5,7 +5,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { X, Plus, Pin, PinOff, Trash2, ChevronUp, ChevronDown, Loader2 } from 'lucide-react'; import { X, Plus, Pin, PinOff, Trash2, ChevronUp, ChevronDown, Loader2 } from 'lucide-react';
import { import {
fetchSavedViews, createSavedView, updateSavedView, deleteSavedView, fetchSavedViews, createSavedView, updateSavedView, deleteSavedView,
type SavedView, type VideosQuery, type UserPreferences, type SavedView, type VideosQuery,
} from '@/lib/api'; } from '@/lib/api';
import styles from './SavedViewManager.module.css'; import styles from './SavedViewManager.module.css';
@@ -8,7 +8,8 @@ import {
createColumnHelper, createColumnHelper,
type SortingState, type SortingState,
type OnChangeFn, type OnChangeFn,
type ColumnVisibilityState, type VisibilityState,
type Updater,
} from '@tanstack/react-table'; } from '@tanstack/react-table';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { import {
@@ -522,8 +523,8 @@ export default function VideoTable({
data: VideoRow[]; data: VideoRow[];
sorting: SortingState; sorting: SortingState;
onSortingChange: OnChangeFn<SortingState>; onSortingChange: OnChangeFn<SortingState>;
columnVisibility?: ColumnVisibilityState; columnVisibility?: VisibilityState;
onColumnVisibilityChange?: (v: ColumnVisibilityState) => void; onColumnVisibilityChange?: (v: VisibilityState) => void;
columnOrder?: string[]; columnOrder?: string[];
filters?: Partial<VideosQuery>; filters?: Partial<VideosQuery>;
onFiltersChange?: (patch: Partial<VideosQuery>) => void; onFiltersChange?: (patch: Partial<VideosQuery>) => void;
@@ -542,7 +543,10 @@ export default function VideoTable({
columnOrder: columnOrder ?? [], columnOrder: columnOrder ?? [],
}, },
onSortingChange, onSortingChange,
onColumnVisibilityChange: onColumnVisibilityChange as any, onColumnVisibilityChange: ((updater: Updater<VisibilityState>) => {
const next = typeof updater === 'function' ? updater(columnVisibility ?? {}) : updater;
onColumnVisibilityChange?.(next);
}) as OnChangeFn<VisibilityState>,
onColumnOrderChange: undefined, onColumnOrderChange: undefined,
getCoreRowModel: getCoreRowModel(), getCoreRowModel: getCoreRowModel(),
manualSorting: true, manualSorting: true,
@@ -561,7 +565,7 @@ export default function VideoTable({
const clearFilters = () => { const clearFilters = () => {
const clear: Partial<VideosQuery> = {}; const clear: Partial<VideosQuery> = {};
ACTIVE_FILTER_KEYS.forEach((k) => { (clear as any)[k] = undefined; }); ACTIVE_FILTER_KEYS.forEach((k) => { (clear as Record<string, undefined>)[k] = undefined; });
onFiltersChange?.(clear); onFiltersChange?.(clear);
}; };
+2 -2
View File
@@ -1,7 +1,7 @@
'use client'; 'use client';
import { useQuery, useQueryClient } from '@tanstack/react-query'; import { useQuery, useQueryClient } from '@tanstack/react-query';
import { fetchPreferences, patchPreferences } from '@/lib/api'; import { fetchPreferences, patchPreferences, type UserPreferences } from '@/lib/api';
import { useCallback } from 'react'; import { useCallback } from 'react';
export type TableKey = 'videos' | 'linting'; export type TableKey = 'videos' | 'linting';
@@ -37,7 +37,7 @@ export function useColumnPreferences(tableKey: TableKey) {
[tableKey]: { visible: nextVisible, order: nextOrder }, [tableKey]: { visible: nextVisible, order: nextOrder },
}, },
}; };
qc.setQueryData(['preferences'], (old: any) => ({ ...old, ...patch })); qc.setQueryData(['preferences'], (old: UserPreferences | undefined) => ({ ...old, ...patch }));
await patchPreferences(patch); await patchPreferences(patch);
}, },
[prefs, tableKey, qc], [prefs, tableKey, qc],
+2
View File
@@ -630,6 +630,8 @@ export interface UserPreferences {
systemOrder?: string[]; systemOrder?: string[];
hiddenSystem?: string[]; hiddenSystem?: string[];
}; };
videoEditLeftCol?: string[];
videoEditRightCol?: string[];
} }
export const fetchSavedViews = () => export const fetchSavedViews = () =>