diff --git a/components/favorites/FavoritesList.tsx b/components/favorites/FavoritesList.tsx index 52e8c43..ff8a058 100644 --- a/components/favorites/FavoritesList.tsx +++ b/components/favorites/FavoritesList.tsx @@ -5,6 +5,7 @@ import type { FavoriteItem } from '@/lib/types'; import { FavoritesItem } from './FavoritesItem'; import { FavoritesEmptyState } from './FavoritesEmptyState'; +import { keepRenderableFavorites } from '@/lib/utils/sync-records'; interface FavoritesListProps { favorites: FavoriteItem[]; @@ -19,7 +20,7 @@ export function FavoritesList({ favorites, onRemove, isPremium = false }: Favori return (
- {favorites.map((item) => ( + {keepRenderableFavorites(favorites).map((item) => ( ) : (
- {history.map((item) => ( + {keepRenderableHistory(history).map((item) => ( 0) { - historyStore.getState().importHistory(result.data.history); + const history = keepRenderableHistory(result.data.history); + const favorites = keepRenderableFavorites(result.data.favorites); + + if (history.length > 0) { + historyStore.getState().importHistory(history); } - if (result.data.favorites?.length > 0) { - favoritesStore.getState().importFavorites(result.data.favorites); + if (favorites.length > 0) { + favoritesStore.getState().importFavorites(favorites); } } } catch (error) { diff --git a/lib/utils/sync-records.ts b/lib/utils/sync-records.ts new file mode 100644 index 0000000..0d3dc7c --- /dev/null +++ b/lib/utils/sync-records.ts @@ -0,0 +1,32 @@ +import type { FavoriteItem, VideoHistoryItem } from '@/lib/types'; + +function hasIdentity(value: unknown): boolean { + if (!value || typeof value !== 'object') return false; + const record = value as { videoId?: unknown; source?: unknown; title?: unknown }; + const videoId = record.videoId; + const hasVideoId = typeof videoId === 'string' ? videoId.length > 0 : typeof videoId === 'number'; + + return hasVideoId && typeof record.source === 'string' && typeof record.title === 'string'; +} + +/** + * Records restored from server-side sync are not guaranteed to be well formed — + * they may come from an older schema or a partial write. Rendering one without + * `videoId` throws while building the player URL, which takes down the whole + * page, so anything lacking a usable identity is dropped on the way in. + */ +export function isRenderableHistoryItem(value: unknown): value is VideoHistoryItem { + return hasIdentity(value); +} + +export function isRenderableFavoriteItem(value: unknown): value is FavoriteItem { + return hasIdentity(value); +} + +export function keepRenderableHistory(items: unknown): VideoHistoryItem[] { + return Array.isArray(items) ? items.filter(isRenderableHistoryItem) : []; +} + +export function keepRenderableFavorites(items: unknown): FavoriteItem[] { + return Array.isArray(items) ? items.filter(isRenderableFavoriteItem) : []; +}