diff --git a/app/page.tsx b/app/page.tsx index a330121..42cadc8 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -29,19 +29,20 @@ function HomePage() { {/* Search Form - Separate from navbar */} -
- +
+ +
{/* Main Content */} diff --git a/lib/hooks/useHomePage.ts b/lib/hooks/useHomePage.ts index 7a639a3..7e64ffa 100644 --- a/lib/hooks/useHomePage.ts +++ b/lib/hooks/useHomePage.ts @@ -18,7 +18,13 @@ export function useHomePage() { const [currentSortBy, setCurrentSortBy] = useState('default'); const onUrlUpdate = useCallback((q: string) => { - router.replace(`/?q=${encodeURIComponent(q)}`, { scroll: false }); + const params = new URLSearchParams(window.location.search); + if (q) { + params.set('q', q); + } else { + params.delete('q'); + } + router.replace(`/?${params.toString()}`, { scroll: false }); }, [router]); // Search stream hook diff --git a/lib/hooks/useSourceBadges.ts b/lib/hooks/useSourceBadges.ts index cc83d48..3e4cd86 100644 --- a/lib/hooks/useSourceBadges.ts +++ b/lib/hooks/useSourceBadges.ts @@ -1,6 +1,5 @@ -'use client'; - -import { useState, useEffect, useMemo, useCallback } from 'react'; +import { useState, useEffect, useMemo, useCallback, useRef } from 'react'; +import { useSearchParams, useRouter, usePathname } from 'next/navigation'; import type { SourceBadge } from '@/lib/types'; /** @@ -10,12 +9,59 @@ import type { SourceBadge } from '@/lib/types'; * - Tracks available video sources * - Supports filtering by selected sources * - Auto-cleanup when sources no longer exist + * - Persists state in URL */ export function useSourceBadges( videos: T[], availableSources: SourceBadge[] ) { - const [selectedSources, setSelectedSources] = useState>(new Set()); + const router = useRouter(); + const pathname = usePathname(); + const searchParams = useSearchParams(); + const isInitialMount = useRef(true); + + // Initialize from URL + const [selectedSources, setSelectedSources] = useState>(() => { + const sourcesParam = searchParams.get('sources'); + if (sourcesParam) { + return new Set(sourcesParam.split(',').filter(Boolean)); + } + return new Set(); + }); + + // Sync state to URL + useEffect(() => { + if (isInitialMount.current) { + isInitialMount.current = false; + return; + } + + const currentParams = new URLSearchParams(searchParams.toString()); + const sourcesParam = Array.from(selectedSources).join(','); + + if (sourcesParam) { + currentParams.set('sources', sourcesParam); + } else { + currentParams.delete('sources'); + } + + const newUrl = `${pathname}?${currentParams.toString()}`; + router.replace(newUrl, { scroll: false }); + }, [selectedSources, pathname, router, searchParams]); + + // Handle URL changes (e.g., when clicking browser back/forward) + useEffect(() => { + const sourcesParam = searchParams.get('sources'); + const urlSources = new Set(sourcesParam ? sourcesParam.split(',').filter(Boolean) : []); + + const currentSourcesArr = Array.from(selectedSources); + const urlSourcesArr = Array.from(urlSources); + + if (currentSourcesArr.length !== urlSourcesArr.length || + !currentSourcesArr.every(s => urlSources.has(s))) { + setSelectedSources(urlSources); + } + }, [searchParams]); // Filter videos by selected sources const filteredVideos = useMemo(() => { @@ -43,6 +89,7 @@ export function useSourceBadges { + if (availableSources.length === 0) return; const availableSourceIds = new Set(availableSources.map(s => s.id)); setSelectedSources(prev => { diff --git a/lib/hooks/useTypeBadges.ts b/lib/hooks/useTypeBadges.ts index 7b14dcf..ed36d5c 100644 --- a/lib/hooks/useTypeBadges.ts +++ b/lib/hooks/useTypeBadges.ts @@ -1,6 +1,5 @@ -'use client'; - -import { useState, useEffect, useMemo, useCallback } from 'react'; +import { useState, useEffect, useMemo, useCallback, useRef } from 'react'; +import { useSearchParams, useRouter, usePathname } from 'next/navigation'; import type { TypeBadge } from '@/lib/types'; /** @@ -12,9 +11,56 @@ import type { TypeBadge } from '@/lib/types'; * - Updates dynamically as videos are added/removed * - Removes badges when count reaches 0 * - Supports filtering by selected types + * - Persists state in URL */ export function useTypeBadges(videos: T[]) { - const [selectedTypes, setSelectedTypes] = useState>(new Set()); + const router = useRouter(); + const pathname = usePathname(); + const searchParams = useSearchParams(); + const isInitialMount = useRef(true); + + // Initialize from URL + const [selectedTypes, setSelectedTypes] = useState>(() => { + const typesParam = searchParams.get('types'); + if (typesParam) { + return new Set(typesParam.split(',').filter(Boolean)); + } + return new Set(); + }); + + // Sync state to URL + useEffect(() => { + if (isInitialMount.current) { + isInitialMount.current = false; + return; + } + + const currentParams = new URLSearchParams(searchParams.toString()); + const typesParam = Array.from(selectedTypes).join(','); + + if (typesParam) { + currentParams.set('types', typesParam); + } else { + currentParams.delete('types'); + } + + const newUrl = `${pathname}?${currentParams.toString()}`; + router.replace(newUrl, { scroll: false }); + }, [selectedTypes, pathname, router, searchParams]); + + // Handle URL changes (e.g., when clicking browser back/forward) + useEffect(() => { + const typesParam = searchParams.get('types'); + const urlTypes = new Set(typesParam ? typesParam.split(',').filter(Boolean) : []); + + const currentTypesArr = Array.from(selectedTypes); + const urlTypesArr = Array.from(urlTypes); + + if (currentTypesArr.length !== urlTypesArr.length || + !currentTypesArr.every(t => urlTypes.has(t))) { + setSelectedTypes(urlTypes); + } + }, [searchParams]); // Collect and count type badges from videos const typeBadges = useMemo(() => { @@ -46,7 +92,7 @@ export function useTypeBadges(videos: T[]) { // Toggle type selection - useCallback to prevent re-creation const toggleType = useCallback((type: string) => { - // Update selected types immediately (high priority) + // Update selected types immediately setSelectedTypes(prev => { const newSet = new Set(prev); if (newSet.has(type)) { @@ -60,6 +106,7 @@ export function useTypeBadges(videos: T[]) { // Auto-cleanup: remove selected types that no longer exist in badges useEffect(() => { + if (typeBadges.length === 0) return; const availableTypes = new Set(typeBadges.map(b => b.type)); setSelectedTypes(prev => { diff --git a/package-lock.json b/package-lock.json index 6dfe967..b77d712 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "kvideo", - "version": "4.0.4", + "version": "4.0.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "kvideo", - "version": "4.0.4", + "version": "4.0.5", "dependencies": { "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", diff --git a/package.json b/package.json index 9bc3693..fd6cfa8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "kvideo", - "version": "4.0.4", + "version": "4.0.5", "private": true, "scripts": { "dev": "next dev",