feat: Introduce adult content browsing with dynamic tag management, aggregated categories, and a video content grid.

This commit is contained in:
kuekhaoyang
2025-11-29 22:39:45 +08:00
parent 7489ff6887
commit da36da655c
8 changed files with 702 additions and 7 deletions
+97
View File
@@ -0,0 +1,97 @@
'use client';
import { useState, useEffect } from 'react';
import { TagManager } from '@/components/home/TagManager';
import { AdultContentGrid } from './AdultContentGrid';
import { useAdultTagManager } from '@/lib/hooks/useAdultTagManager';
import { useAdultContent } from '@/lib/hooks/useAdultContent';
interface AdultContentProps {
onSearch?: (query: string) => void;
}
export function AdultContent({ onSearch }: AdultContentProps) {
const {
tags,
selectedTag,
newTagInput,
showTagManager,
justAddedTag,
setSelectedTag,
setNewTagInput,
setShowTagManager,
setJustAddedTag,
handleAddTag,
handleDeleteTag,
handleRestoreDefaults,
handleDragEnd,
loading: tagsLoading,
} = useAdultTagManager();
// Get the category value from selected tag
const categoryValue = tags.find(t => t.id === selectedTag)?.value || '';
const {
videos,
loading: contentLoading,
hasMore,
prefetchRef,
loadMoreRef,
} = useAdultContent(categoryValue);
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
const handleVideoClick = (video: any) => {
if (onSearch) {
onSearch(video.vod_name);
}
};
if (!mounted || tagsLoading) {
return (
<div className="animate-fade-in">
<div className="mb-6 h-8 w-24 bg-[var(--glass-bg)] rounded animate-pulse" />
<div className="mb-8 h-10 w-full bg-[var(--glass-bg)] rounded animate-pulse" />
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4">
{[...Array(10)].map((_, i) => (
<div key={i} className="aspect-[2/3] bg-[var(--glass-bg)] rounded-[var(--radius-2xl)] animate-pulse" />
))}
</div>
</div>
);
}
return (
<div className="animate-fade-in">
<TagManager
tags={tags}
selectedTag={selectedTag}
showTagManager={showTagManager}
newTagInput={newTagInput}
justAddedTag={justAddedTag}
onTagSelect={(tagId) => {
setSelectedTag(tagId);
}}
onTagDelete={handleDeleteTag}
onToggleManager={() => setShowTagManager(!showTagManager)}
onRestoreDefaults={handleRestoreDefaults}
onNewTagInputChange={setNewTagInput}
onAddTag={handleAddTag}
onDragEnd={handleDragEnd}
onJustAddedTagHandled={() => setJustAddedTag(false)}
/>
<AdultContentGrid
videos={videos}
loading={contentLoading}
hasMore={hasMore}
onVideoClick={handleVideoClick}
prefetchRef={prefetchRef}
loadMoreRef={loadMoreRef}
/>
</div>
);
}
+112
View File
@@ -0,0 +1,112 @@
'use client';
import Link from 'next/link';
import Image from 'next/image';
import React from 'react';
import { Card } from '@/components/ui/Card';
interface AdultVideo {
vod_id: string | number;
vod_name: string;
vod_pic?: string;
vod_remarks?: string;
type_name?: string;
source: string;
}
interface AdultContentGridProps {
videos: AdultVideo[];
loading: boolean;
hasMore: boolean;
onVideoClick?: (video: AdultVideo) => void;
prefetchRef: React.RefObject<HTMLDivElement | null>;
loadMoreRef: React.RefObject<HTMLDivElement | null>;
}
export function AdultContentGrid({
videos,
loading,
hasMore,
onVideoClick,
prefetchRef,
loadMoreRef,
}: AdultContentGridProps) {
return (
<div className="space-y-6">
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4">
{videos.map((video) => (
<div
key={`${video.source}-${video.vod_id}`}
className="group cursor-pointer"
onClick={() => onVideoClick?.(video)}
style={{
contain: 'layout style paint',
contentVisibility: 'auto'
}}
>
<Card hover className="overflow-hidden p-0 h-full" blur={false}>
<div className="relative aspect-[2/3] overflow-hidden bg-[var(--glass-bg)] rounded-[var(--radius-2xl)]">
{video.vod_pic ? (
<Image
src={video.vod_pic}
alt={video.vod_name}
fill
sizes="(max-width: 640px) 50vw, (max-width: 768px) 33vw, (max-width: 1024px) 25vw, 20vw"
className="object-cover transition-transform duration-300 group-hover:scale-105 rounded-[var(--radius-2xl)]"
loading="eager"
unoptimized
/>
) : (
<div className="w-full h-full flex items-center justify-center text-[var(--text-color-secondary)]">
</div>
)}
{video.vod_remarks && (
<div className="absolute top-2 right-2 bg-black/80 px-2.5 py-1.5 flex items-center gap-1.5 rounded-[var(--radius-full)]">
<span className="text-xs font-bold text-white">
{video.vod_remarks}
</span>
</div>
)}
</div>
<div className="p-3">
<h3 className="font-semibold text-sm text-[var(--text-color)] line-clamp-2 group-hover:text-[var(--accent-color)] transition-colors">
{video.vod_name}
</h3>
{video.type_name && (
<p className="text-xs text-[var(--text-color-secondary)] mt-1">
{video.type_name}
</p>
)}
</div>
</Card>
</div>
))}
</div>
{/* Prefetch Trigger */}
{hasMore && !loading && <div ref={prefetchRef} className="h-1" />}
{loading && (
<div className="flex justify-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-4 border-[var(--accent-color)] border-t-transparent"></div>
</div>
)}
{/* Intersection Observer Target */}
{hasMore && !loading && <div ref={loadMoreRef} className="h-20" />}
{!loading && !hasMore && videos.length > 0 && (
<p className="text-center text-[var(--text-color-secondary)] py-8">
</p>
)}
{!loading && videos.length === 0 && (
<p className="text-center text-[var(--text-color-secondary)] py-8">
</p>
)}
</div>
);
}