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.' },
] as const;
type RuleCode = typeof RULE_CATALOG[number]['code'];
// ── Helpers ───────────────────────────────────────────────────────────────────
function SevBadge({ sev }: { sev: string }) {
@@ -156,7 +154,7 @@ export default function LintingPage() {
function toggleSelect(id: string) {
setSelected((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;
});
}
@@ -172,7 +170,7 @@ export default function LintingPage() {
function toggleFix(id: string) {
setExpandedFix((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;
});
}
@@ -483,7 +483,7 @@ export default function SettingsPage() {
<section className={styles.section}>
<h2 className={styles.sectionTitle}>Publishing Schedule</h2>
<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>
<div className={styles.scheduleBox}>
@@ -581,7 +581,7 @@ export default function SettingsPage() {
/>
<span>Show Canva link in video editor</span>
<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>
</label>
) : (
@@ -601,7 +601,7 @@ export default function SettingsPage() {
/>
<span>Show deleted videos tab</span>
<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>
</label>
) : (
@@ -38,8 +38,7 @@ function TagsDiff({ before, after }: { before: string[]; after: string[] }) {
function DiffRow({ field, item }: { field: string; item: PushPendingPreviewItem }) {
const label = FIELD_LABELS[field] ?? field;
const diff = item.diff as any;
const d = diff[field];
const d = item.diff[field as keyof typeof item.diff];
if (!d) return null;
if (field === 'tags') {
@@ -47,7 +46,7 @@ function DiffRow({ field, item }: { field: string; item: PushPendingPreviewItem
<tr>
<td className={styles.diffField}>{label}</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>
);
}
@@ -60,7 +59,7 @@ function DiffRow({ field, item }: { field: string; item: PushPendingPreviewItem
</tr>
);
}
const fmt = (v: any) => {
const fmt = (v: string | number | boolean | string[] | null | undefined) => {
if (v === null || v === undefined) return '—';
if (typeof v === 'boolean') return v ? 'Yes' : 'No';
return String(v);
@@ -13,7 +13,7 @@ import { SortableContext, verticalListSortingStrategy, arrayMove } from '@dnd-ki
import {
ArrowLeft, ExternalLink, Loader2, AlertCircle, CheckCircle2,
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,
} from 'lucide-react';
import { formatDistanceToNow } from 'date-fns';
@@ -34,7 +34,7 @@ import SortableSection from '@/components/shared/SortableSection';
import f from '@/components/shared/FormField.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 {
const d = new Date(utcIso);
+5 -13
View File
@@ -3,8 +3,8 @@
import { useState, useEffect, useCallback, useMemo } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useSearchParams, useRouter } from 'next/navigation';
import { Download, Loader2, AlertCircle, RefreshCw, CheckCircle2, X, Plus } from 'lucide-react';
import { type SortingState, type ColumnVisibilityState } from '@tanstack/react-table';
import { Download, Loader2, AlertCircle, RefreshCw, CheckCircle2, Plus } from 'lucide-react';
import { type SortingState, type VisibilityState } from '@tanstack/react-table';
import VideoTable, { VideoRow, VIDEO_COLUMN_DEFS } from '@/components/video-table/VideoTable';
import ColumnPicker from '@/components/shared/ColumnPicker';
import SavedViewManager from '@/components/shared/SavedViewManager';
@@ -107,9 +107,9 @@ function parseExtraFilters(searchParams: URLSearchParams): Partial<VideosQuery>
const v = searchParams.get(k);
if (v !== null) {
if (k === 'embeddable' || k === 'selfDeclaredMadeForKids') {
(result as any)[k] = v === 'true';
(result as Record<string, string | boolean>)[k] = v === 'true';
} 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 columnVisibility = useMemo<ColumnVisibilityState>(() => {
const columnVisibility = useMemo<VisibilityState>(() => {
const allIds = VIDEO_COLUMN_DEFS.map((c) => c.id);
return Object.fromEntries(allIds.map((id) => [id, visible.includes(id)]));
}, [visible]);
@@ -193,14 +193,6 @@ export default function VideosPage() {
setPage(1);
}, [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)
const urlSearch = searchParams.get('search') ?? 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 {
fetchSavedViews, createSavedView, updateSavedView, deleteSavedView,
type SavedView, type VideosQuery, type UserPreferences,
type SavedView, type VideosQuery,
} from '@/lib/api';
import styles from './SavedViewManager.module.css';
@@ -8,7 +8,8 @@ import {
createColumnHelper,
type SortingState,
type OnChangeFn,
type ColumnVisibilityState,
type VisibilityState,
type Updater,
} from '@tanstack/react-table';
import { useRouter } from 'next/navigation';
import {
@@ -522,8 +523,8 @@ export default function VideoTable({
data: VideoRow[];
sorting: SortingState;
onSortingChange: OnChangeFn<SortingState>;
columnVisibility?: ColumnVisibilityState;
onColumnVisibilityChange?: (v: ColumnVisibilityState) => void;
columnVisibility?: VisibilityState;
onColumnVisibilityChange?: (v: VisibilityState) => void;
columnOrder?: string[];
filters?: Partial<VideosQuery>;
onFiltersChange?: (patch: Partial<VideosQuery>) => void;
@@ -542,7 +543,10 @@ export default function VideoTable({
columnOrder: columnOrder ?? [],
},
onSortingChange,
onColumnVisibilityChange: onColumnVisibilityChange as any,
onColumnVisibilityChange: ((updater: Updater<VisibilityState>) => {
const next = typeof updater === 'function' ? updater(columnVisibility ?? {}) : updater;
onColumnVisibilityChange?.(next);
}) as OnChangeFn<VisibilityState>,
onColumnOrderChange: undefined,
getCoreRowModel: getCoreRowModel(),
manualSorting: true,
@@ -561,7 +565,7 @@ export default function VideoTable({
const clearFilters = () => {
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);
};
+2 -2
View File
@@ -1,7 +1,7 @@
'use client';
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';
export type TableKey = 'videos' | 'linting';
@@ -37,7 +37,7 @@ export function useColumnPreferences(tableKey: TableKey) {
[tableKey]: { visible: nextVisible, order: nextOrder },
},
};
qc.setQueryData(['preferences'], (old: any) => ({ ...old, ...patch }));
qc.setQueryData(['preferences'], (old: UserPreferences | undefined) => ({ ...old, ...patch }));
await patchPreferences(patch);
},
[prefs, tableKey, qc],
+2
View File
@@ -630,6 +630,8 @@ export interface UserPreferences {
systemOrder?: string[];
hiddenSystem?: string[];
};
videoEditLeftCol?: string[];
videoEditRightCol?: string[];
}
export const fetchSavedViews = () =>