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:
@@ -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 "Next free slot" 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'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 "Deleted" 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);
|
||||||
|
|||||||
@@ -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);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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],
|
||||||
|
|||||||
@@ -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 = () =>
|
||||||
|
|||||||
Reference in New Issue
Block a user