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",