Remove categories and sorting options from the video search filters
Removes category/sort selection from UI and `/api/videos`, updates `getVideos`/`getVideoCount` in `storage.ts`, and removes `/api/categories` route. Replit-Commit-Author: Agent Replit-Commit-Session-Id: aa92e7e2-ec62-4c92-b21b-02ef78a664c2 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/8cc42625-c1f5-4e43-99bd-77f2c4dedee2/aa92e7e2-ec62-4c92-b21b-02ef78a664c2/Cz5fe6k
This commit is contained in:
parent
58dc46ef4b
commit
645fa51028
@ -6,19 +6,13 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
|
|||||||
|
|
||||||
interface SearchHeaderProps {
|
interface SearchHeaderProps {
|
||||||
onSearch: (query: string) => void;
|
onSearch: (query: string) => void;
|
||||||
onCategoryChange: (category: string) => void;
|
|
||||||
onSortChange: (sort: string) => void;
|
|
||||||
onViewChange: (view: "grid" | "list") => void;
|
onViewChange: (view: "grid" | "list") => void;
|
||||||
categories: string[];
|
|
||||||
currentView: "grid" | "list";
|
currentView: "grid" | "list";
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SearchHeader({
|
export default function SearchHeader({
|
||||||
onSearch,
|
onSearch,
|
||||||
onCategoryChange,
|
|
||||||
onSortChange,
|
|
||||||
onViewChange,
|
onViewChange,
|
||||||
categories,
|
|
||||||
currentView
|
currentView
|
||||||
}: SearchHeaderProps) {
|
}: SearchHeaderProps) {
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
@ -79,61 +73,33 @@ export default function SearchHeader({
|
|||||||
<p className="text-bunny-muted">Streaming from Bunny.net CDN</p>
|
<p className="text-bunny-muted">Streaming from Bunny.net CDN</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-3">
|
<div className="flex bg-bunny-gray rounded-lg p-1">
|
||||||
<Select onValueChange={onCategoryChange} data-testid="select-category">
|
<Button
|
||||||
<SelectTrigger className="bg-bunny-gray border border-gray-600 rounded-lg px-4 py-2 text-sm focus:outline-none focus:border-bunny-blue w-40">
|
variant={currentView === "grid" ? "default" : "ghost"}
|
||||||
<SelectValue placeholder="All Categories" />
|
size="sm"
|
||||||
</SelectTrigger>
|
onClick={() => onViewChange("grid")}
|
||||||
<SelectContent>
|
className={`px-3 py-1 rounded text-sm ${
|
||||||
<SelectItem value="All Categories">All Categories</SelectItem>
|
currentView === "grid"
|
||||||
{categories.map((category) => (
|
? "bg-bunny-blue text-white"
|
||||||
<SelectItem key={category} value={category}>
|
: "text-bunny-muted hover:text-white"
|
||||||
{category}
|
}`}
|
||||||
</SelectItem>
|
data-testid="button-grid-view"
|
||||||
))}
|
>
|
||||||
</SelectContent>
|
<Grid3X3 className="w-4 h-4" />
|
||||||
</Select>
|
</Button>
|
||||||
|
<Button
|
||||||
<Select onValueChange={onSortChange} data-testid="select-sort">
|
variant={currentView === "list" ? "default" : "ghost"}
|
||||||
<SelectTrigger className="bg-bunny-gray border border-gray-600 rounded-lg px-4 py-2 text-sm focus:outline-none focus:border-bunny-blue w-32">
|
size="sm"
|
||||||
<SelectValue placeholder="Latest" />
|
onClick={() => onViewChange("list")}
|
||||||
</SelectTrigger>
|
className={`px-3 py-1 rounded text-sm ${
|
||||||
<SelectContent>
|
currentView === "list"
|
||||||
<SelectItem value="latest">Latest</SelectItem>
|
? "bg-bunny-blue text-white"
|
||||||
<SelectItem value="views">Most Viewed</SelectItem>
|
: "text-bunny-muted hover:text-white"
|
||||||
<SelectItem value="duration">Duration</SelectItem>
|
}`}
|
||||||
<SelectItem value="title">A-Z</SelectItem>
|
data-testid="button-list-view"
|
||||||
</SelectContent>
|
>
|
||||||
</Select>
|
<List className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
<div className="flex bg-bunny-gray rounded-lg p-1">
|
|
||||||
<Button
|
|
||||||
variant={currentView === "grid" ? "default" : "ghost"}
|
|
||||||
size="sm"
|
|
||||||
onClick={() => onViewChange("grid")}
|
|
||||||
className={`px-3 py-1 rounded text-sm ${
|
|
||||||
currentView === "grid"
|
|
||||||
? "bg-bunny-blue text-white"
|
|
||||||
: "text-bunny-muted hover:text-white"
|
|
||||||
}`}
|
|
||||||
data-testid="button-grid-view"
|
|
||||||
>
|
|
||||||
<Grid3X3 className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant={currentView === "list" ? "default" : "ghost"}
|
|
||||||
size="sm"
|
|
||||||
onClick={() => onViewChange("list")}
|
|
||||||
className={`px-3 py-1 rounded text-sm ${
|
|
||||||
currentView === "list"
|
|
||||||
? "bg-bunny-blue text-white"
|
|
||||||
: "text-bunny-muted hover:text-white"
|
|
||||||
}`}
|
|
||||||
data-testid="button-list-view"
|
|
||||||
>
|
|
||||||
<List className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -12,25 +12,16 @@ interface VideosResponse {
|
|||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
const [selectedCategory, setSelectedCategory] = useState("All Categories");
|
|
||||||
const [sortBy, setSortBy] = useState("latest");
|
|
||||||
const [viewMode, setViewMode] = useState<"grid" | "list">("grid");
|
const [viewMode, setViewMode] = useState<"grid" | "list">("grid");
|
||||||
const [offset, setOffset] = useState(0);
|
const [offset, setOffset] = useState(0);
|
||||||
const [allVideos, setAllVideos] = useState<Video[]>([]);
|
const [allVideos, setAllVideos] = useState<Video[]>([]);
|
||||||
|
|
||||||
// Fetch categories
|
|
||||||
const { data: categories = [] } = useQuery<string[]>({
|
|
||||||
queryKey: ["/api/categories"],
|
|
||||||
});
|
|
||||||
|
|
||||||
// Fetch videos
|
// Fetch videos
|
||||||
const { data: videosResponse, isLoading, refetch } = useQuery<VideosResponse>({
|
const { data: videosResponse, isLoading, refetch } = useQuery<VideosResponse>({
|
||||||
queryKey: ["/api/videos", {
|
queryKey: ["/api/videos", {
|
||||||
limit: 20,
|
limit: 20,
|
||||||
offset,
|
offset,
|
||||||
search: searchQuery || undefined,
|
search: searchQuery || undefined
|
||||||
category: selectedCategory !== "All Categories" ? selectedCategory : undefined,
|
|
||||||
sort: sortBy
|
|
||||||
}],
|
}],
|
||||||
queryFn: async ({ queryKey }) => {
|
queryFn: async ({ queryKey }) => {
|
||||||
const [, params] = queryKey as [string, any];
|
const [, params] = queryKey as [string, any];
|
||||||
@ -61,42 +52,27 @@ export default function Home() {
|
|||||||
}
|
}
|
||||||
}, [videosResponse, offset]);
|
}, [videosResponse, offset]);
|
||||||
|
|
||||||
// Reset videos when search/filter changes
|
// Reset videos when search changes
|
||||||
const handleSearch = (query: string) => {
|
const handleSearch = (query: string) => {
|
||||||
setSearchQuery(query);
|
setSearchQuery(query);
|
||||||
setOffset(0);
|
setOffset(0);
|
||||||
setAllVideos([]);
|
setAllVideos([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCategoryChange = (category: string) => {
|
|
||||||
setSelectedCategory(category);
|
|
||||||
setOffset(0);
|
|
||||||
setAllVideos([]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSortChange = (sort: string) => {
|
|
||||||
setSortBy(sort);
|
|
||||||
setOffset(0);
|
|
||||||
setAllVideos([]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleLoadMore = () => {
|
const handleLoadMore = () => {
|
||||||
setOffset(prev => prev + 20);
|
setOffset(prev => prev + 20);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Force refetch when filters change
|
// Force refetch when search changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
refetch();
|
refetch();
|
||||||
}, [searchQuery, selectedCategory, sortBy, offset, refetch]);
|
}, [searchQuery, offset, refetch]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-bunny-dark">
|
<div className="min-h-screen bg-bunny-dark">
|
||||||
<SearchHeader
|
<SearchHeader
|
||||||
onSearch={handleSearch}
|
onSearch={handleSearch}
|
||||||
onCategoryChange={handleCategoryChange}
|
|
||||||
onSortChange={handleSortChange}
|
|
||||||
onViewChange={setViewMode}
|
onViewChange={setViewMode}
|
||||||
categories={categories}
|
|
||||||
currentView={viewMode}
|
currentView={viewMode}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@ -10,10 +10,9 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
const limit = parseInt(req.query.limit as string) || 20;
|
const limit = parseInt(req.query.limit as string) || 20;
|
||||||
const offset = parseInt(req.query.offset as string) || 0;
|
const offset = parseInt(req.query.offset as string) || 0;
|
||||||
const search = req.query.search as string;
|
const search = req.query.search as string;
|
||||||
const category = req.query.category as string;
|
|
||||||
|
|
||||||
const videos = await storage.getVideos(limit, offset, search, category);
|
const videos = await storage.getVideos(limit, offset, search);
|
||||||
const total = await storage.getVideoCount(search, category);
|
const total = await storage.getVideoCount(search);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
videos,
|
videos,
|
||||||
@ -48,17 +47,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get video categories
|
|
||||||
app.get("/api/categories", async (req, res) => {
|
|
||||||
try {
|
|
||||||
const videos = await storage.getVideos(1000);
|
|
||||||
const categories = Array.from(new Set(videos.map(v => v.category).filter(Boolean)));
|
|
||||||
res.json(categories);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching categories:", error);
|
|
||||||
res.status(500).json({ message: "Failed to fetch categories" });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const httpServer = createServer(app);
|
const httpServer = createServer(app);
|
||||||
return httpServer;
|
return httpServer;
|
||||||
|
|||||||
@ -3,11 +3,11 @@ import { randomUUID } from "crypto";
|
|||||||
import { BunnyService } from "./bunny";
|
import { BunnyService } from "./bunny";
|
||||||
|
|
||||||
export interface IStorage {
|
export interface IStorage {
|
||||||
getVideos(limit?: number, offset?: number, search?: string, category?: string): Promise<Video[]>;
|
getVideos(limit?: number, offset?: number, search?: string): Promise<Video[]>;
|
||||||
getVideo(id: string): Promise<Video | undefined>;
|
getVideo(id: string): Promise<Video | undefined>;
|
||||||
createVideo(video: InsertVideo): Promise<Video>;
|
createVideo(video: InsertVideo): Promise<Video>;
|
||||||
updateVideoViews(id: string): Promise<void>;
|
updateVideoViews(id: string): Promise<void>;
|
||||||
getVideoCount(search?: string, category?: string): Promise<number>;
|
getVideoCount(search?: string): Promise<number>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MemStorage implements IStorage {
|
export class MemStorage implements IStorage {
|
||||||
@ -92,7 +92,7 @@ export class MemStorage implements IStorage {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getVideos(limit = 20, offset = 0, search?: string, category?: string): Promise<Video[]> {
|
async getVideos(limit = 20, offset = 0, search?: string): Promise<Video[]> {
|
||||||
let videos = Array.from(this.videos.values());
|
let videos = Array.from(this.videos.values());
|
||||||
|
|
||||||
// Filter by search
|
// Filter by search
|
||||||
@ -104,11 +104,6 @@ export class MemStorage implements IStorage {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter by category
|
|
||||||
if (category && category !== "All Categories") {
|
|
||||||
videos = videos.filter(video => video.category === category);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort by created date (newest first)
|
// Sort by created date (newest first)
|
||||||
videos.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
videos.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
||||||
|
|
||||||
@ -141,8 +136,8 @@ export class MemStorage implements IStorage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getVideoCount(search?: string, category?: string): Promise<number> {
|
async getVideoCount(search?: string): Promise<number> {
|
||||||
const videos = await this.getVideos(1000, 0, search, category);
|
const videos = await this.getVideos(1000, 0, search);
|
||||||
return videos.length;
|
return videos.length;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -156,11 +151,11 @@ class BunnyStorage implements IStorage {
|
|||||||
this.bunnyService = new BunnyService();
|
this.bunnyService = new BunnyService();
|
||||||
}
|
}
|
||||||
|
|
||||||
async getVideos(limit = 20, offset = 0, search?: string, category?: string): Promise<Video[]> {
|
async getVideos(limit = 20, offset = 0, search?: string): Promise<Video[]> {
|
||||||
try {
|
try {
|
||||||
// For search/filtering, we need to get more videos than the requested limit
|
// For search filtering, we need to get more videos than the requested limit
|
||||||
// because we'll filter client-side and then slice to the requested amount
|
// because we'll filter client-side and then slice to the requested amount
|
||||||
const fetchLimit = (search || category) ? 100 : limit;
|
const fetchLimit = search ? 100 : limit;
|
||||||
const page = Math.floor(offset / fetchLimit) + 1;
|
const page = Math.floor(offset / fetchLimit) + 1;
|
||||||
|
|
||||||
// Get videos from Bunny API
|
// Get videos from Bunny API
|
||||||
@ -178,11 +173,6 @@ class BunnyStorage implements IStorage {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter by category if specified
|
|
||||||
if (category && category !== "All Categories") {
|
|
||||||
filteredVideos = filteredVideos.filter(video => video.category === category);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply cached view counts
|
// Apply cached view counts
|
||||||
filteredVideos.forEach(video => {
|
filteredVideos.forEach(video => {
|
||||||
if (this.viewsCache.has(video.id)) {
|
if (this.viewsCache.has(video.id)) {
|
||||||
@ -228,10 +218,10 @@ class BunnyStorage implements IStorage {
|
|||||||
this.viewsCache.set(id, currentViews + 1);
|
this.viewsCache.set(id, currentViews + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getVideoCount(search?: string, category?: string): Promise<number> {
|
async getVideoCount(search?: string): Promise<number> {
|
||||||
try {
|
try {
|
||||||
// For accurate count with client-side filtering, we need to get all videos and filter them
|
// For accurate count with client-side filtering, we need to get all videos and filter them
|
||||||
const allVideos = await this.getVideos(1000, 0, search, category);
|
const allVideos = await this.getVideos(1000, 0, search);
|
||||||
return allVideos.length;
|
return allVideos.length;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error getting video count from Bunny:', error);
|
console.error('Error getting video count from Bunny:', error);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user