mirror of
https://github.com/KuekHaoYang/KVideo.git
synced 2026-08-13 15:53:44 +08:00
feat: Implement URL state persistence for search queries, source filters, and type filters, and update search form styling.
This commit is contained in:
+13
-12
@@ -29,19 +29,20 @@ function HomePage() {
|
||||
<Navbar onReset={handleReset} />
|
||||
|
||||
{/* Search Form - Separate from navbar */}
|
||||
<div className="max-w-7xl mx-auto px-4 mt-6 mb-8 relative" style={{
|
||||
transform: 'translate3d(0, 0, 0)',
|
||||
zIndex: 1000
|
||||
<div className="sticky top-[72px] sm:top-[88px] z-[1000] py-2 bg-gradient-to-b from-[var(--bg-color)] via-[var(--bg-color)]/95 to-transparent backdrop-blur-md transition-all duration-300" style={{
|
||||
willChange: 'transform, opacity'
|
||||
}}>
|
||||
<SearchForm
|
||||
onSearch={handleSearch}
|
||||
onClear={handleReset}
|
||||
isLoading={loading}
|
||||
initialQuery={query}
|
||||
currentSource=""
|
||||
checkedSources={completedSources}
|
||||
totalSources={totalSources}
|
||||
/>
|
||||
<div className="max-w-7xl mx-auto px-4">
|
||||
<SearchForm
|
||||
onSearch={handleSearch}
|
||||
onClear={handleReset}
|
||||
isLoading={loading}
|
||||
initialQuery={query}
|
||||
currentSource=""
|
||||
checkedSources={completedSources}
|
||||
totalSources={totalSources}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
|
||||
@@ -18,7 +18,13 @@ export function useHomePage() {
|
||||
const [currentSortBy, setCurrentSortBy] = useState<SortOption>('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
|
||||
|
||||
@@ -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<T extends { source?: string; sourceName?: string }>(
|
||||
videos: T[],
|
||||
availableSources: SourceBadge[]
|
||||
) {
|
||||
const [selectedSources, setSelectedSources] = useState<Set<string>>(new Set());
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const isInitialMount = useRef(true);
|
||||
|
||||
// Initialize from URL
|
||||
const [selectedSources, setSelectedSources] = useState<Set<string>>(() => {
|
||||
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<T extends { source?: string; sourceName?: string
|
||||
|
||||
// Auto-cleanup: remove selected sources that no longer exist
|
||||
useEffect(() => {
|
||||
if (availableSources.length === 0) return;
|
||||
const availableSourceIds = new Set(availableSources.map(s => s.id));
|
||||
|
||||
setSelectedSources(prev => {
|
||||
|
||||
@@ -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<T extends { type_name?: string }>(videos: T[]) {
|
||||
const [selectedTypes, setSelectedTypes] = useState<Set<string>>(new Set());
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const isInitialMount = useRef(true);
|
||||
|
||||
// Initialize from URL
|
||||
const [selectedTypes, setSelectedTypes] = useState<Set<string>>(() => {
|
||||
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<TypeBadge[]>(() => {
|
||||
@@ -46,7 +92,7 @@ export function useTypeBadges<T extends { type_name?: string }>(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<T extends { type_name?: string }>(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 => {
|
||||
|
||||
Generated
+2
-2
@@ -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",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "kvideo",
|
||||
"version": "4.0.4",
|
||||
"version": "4.0.5",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
|
||||
Reference in New Issue
Block a user