Playlist Analyzer Component
Detailed documentation of the main Playlist Analyzer UI component
Overview
The AnalyzeViewPage
component is the main UI component for the Playlist Analyzer feature. It orchestrates the entire user experience, from URL input to displaying analyzed playlist data.
Location
/src/features/analyze/components/analyze-view-page.tsx
Component Architecture
AnalyzeViewPage (Main Container)
│
├── PageContainer (Layout wrapper)
│ └── Heading (Title and description)
│
├── Input Form Card
│ ├── Input (Playlist URL)
│ └── Toast (Analyze/Reset buttons with states)
│
├── FeatureCard (Welcome message - conditional)
│
└── Analysis Results (Conditional on data)
├── Summary Grid
│ ├── Playlist Summary Card
│ │ ├── Total Videos
│ │ └── Total Duration
│ └── Filters Card
│ ├── Playback Speed Selector
│ ├── Sort By Selector
│ └── Range Selection
│
├── Search Input
│
└── Video Grid Card
└── ScrollArea
└── VideoCard components (mapped)
Complete Implementation
'use client';
import { useCallback } from 'react';
import { motion } from 'framer-motion';
import { Card, CardContent } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue
} from '@/components/ui/select';
import {
Clock,
SortAsc,
PlayCircle,
FastForward,
Calendar
} from 'lucide-react';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Toast } from '@/components/searchbar/toast';
import { VideoCard } from '@/components/VideoCard';
import FeatureCard from '@/components/hsr/FeatureCard';
import { formatDuration } from '@/lib/ythelper';
import PageContainer from '@/components/layout/page-container';
import { Heading } from '@/components/ui/heading';
import { useAnalyzeStore } from '../store/analyze-store';
import { useFetchPlaylist, usePlaylistFilters } from '../hooks';
import { validateYoutubePlaylistUrl } from '@/lib/youtube-validator';
import { toast } from 'sonner';
export default function AnalyzeViewPage() {
const {
playlistUrl,
playlistData,
rangeStart,
rangeEnd,
sortBy,
playbackSpeed,
searchQuery,
isLoading,
isSuccess,
setPlaylistUrl,
setRangeStart,
setRangeEnd,
setSortBy,
setPlaybackSpeed,
setSearchQuery,
setIsSuccess
} = useAnalyzeStore();
const { fetchPlaylist } = useFetchPlaylist();
const { sortedVideos, adjustedDuration, rangeOptions } = usePlaylistFilters();
const handleAnalyze = useCallback(() => {
// Check if URL is empty
if (!playlistUrl || !playlistUrl.trim()) {
toast.error('Please enter a YouTube playlist URL');
return;
}
// Validate YouTube playlist URL
const validation = validateYoutubePlaylistUrl(playlistUrl);
if (!validation.isValid) {
toast.error(
validation.error || 'Please enter a valid YouTube playlist URL'
);
return;
}
fetchPlaylist();
}, [fetchPlaylist, playlistUrl]);
const handleReset = useCallback(() => {
setIsSuccess(false);
}, [setIsSuccess]);
// Determine state for Toast component
const toastState = isLoading ? 'loading' : isSuccess ? 'success' : 'initial';
return (
<PageContainer scrollable>
<div className='w-full space-y-4'>
<div className='flex items-start justify-between'>
<Heading
title='Playlist Analyzer'
description='Analyze YouTube playlists and get insights'
/>
</div>
{/* Input Form */}
<Card className='bg-background border-zinc-800'>
<CardContent className='p-4'>
<div className='flex space-x-2'>
<Input
type='text'
value={playlistUrl}
onChange={(e) => setPlaylistUrl(e.target.value)}
placeholder='Enter YouTube Playlist URL...'
className='flex-1'
/>
<Toast
state={toastState}
onSave={handleAnalyze}
onReset={handleReset}
/>
</div>
</CardContent>
</Card>
{/* Welcome Message - Only shown initially */}
{!playlistData && <FeatureCard type='analyze' />}
{/* Analysis Results - Shown after data fetch */}
{playlistData && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className='space-y-4'
>
{/* Summary and Filters */}
<div className='grid grid-cols-1 gap-4 lg:grid-cols-2'>
{/* Playlist Summary */}
<Card className='bg-background border-zinc-800'>
<CardContent className='p-4'>
<h3 className='mb-4 text-lg font-bold'>Playlist Summary</h3>
<div className='space-y-2'>
<div className='flex items-center gap-2'>
<PlayCircle className='h-4 w-4 text-blue-400' />
<span className='font-semibold'>
Total Videos: {playlistData.totalVideos}
</span>
</div>
<div className='flex items-center gap-2'>
<Clock className='h-4 w-4 text-green-500' />
<span className='font-semibold'>
Total Duration: {formatDuration(adjustedDuration)}
</span>
</div>
</div>
</CardContent>
</Card>
{/* Filters Card */}
<Card className='bg-background border-zinc-800'>
<CardContent className='p-4'>
<h3 className='mb-4 text-lg font-semibold'>Filters</h3>
<div className='grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3'>
{/* Playback Speed */}
<div className='flex items-center gap-2'>
<FastForward className='h-4 w-4 text-purple-400' />
<Select
value={playbackSpeed}
onValueChange={setPlaybackSpeed}
>
<SelectTrigger className='w-full'>
<SelectValue placeholder='Speed' />
</SelectTrigger>
<SelectContent>
{[
'0.25',
'0.5',
'0.75',
'1',
'1.25',
'1.5',
'1.75',
'2'
].map((speed) => (
<SelectItem key={speed} value={speed}>
{speed}x
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Sort By */}
<div className='flex items-center gap-2'>
<SortAsc className='h-4 w-4 text-orange-400' />
<Select value={sortBy} onValueChange={setSortBy}>
<SelectTrigger className='w-full'>
<SelectValue placeholder='Sort' />
</SelectTrigger>
<SelectContent>
{[
'Position',
'Duration',
'Views',
'Likes',
'Publish Date'
].map((option) => (
<SelectItem
key={option}
value={option.toLowerCase()}
>
{option}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Range Selection */}
<div className='flex items-center gap-2 sm:col-span-2 lg:col-span-1'>
<Calendar className='h-4 w-4 text-red-400' />
<Select value={rangeStart} onValueChange={setRangeStart}>
<SelectTrigger className='w-full'>
<SelectValue placeholder='Start' />
</SelectTrigger>
<SelectContent>
{rangeOptions.map((option) => (
<SelectItem key={option} value={option}>
{option}
</SelectItem>
))}
</SelectContent>
</Select>
<span className='text-muted-foreground'>-</span>
<Select value={rangeEnd} onValueChange={setRangeEnd}>
<SelectTrigger className='w-full'>
<SelectValue placeholder='End' />
</SelectTrigger>
<SelectContent>
{rangeOptions.map((option) => (
<SelectItem key={option} value={option}>
{option}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<p className='text-muted-foreground mt-3 text-sm'>
Analyzing videos {rangeStart} to {rangeEnd} at speed:{' '}
{playbackSpeed}x
</p>
</CardContent>
</Card>
</div>
{/* Search Bar */}
<div className='flex justify-center'>
<Input
className='w-full max-w-md'
placeholder='Search videos...'
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
{/* Video Grid */}
<Card className='bg-background border-zinc-800'>
<CardContent className='p-4'>
<ScrollArea className='h-[500px]'>
<div className='grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3'>
{sortedVideos.map((video) => (
<VideoCard
key={video.id}
video={video}
searchQuery={searchQuery}
/>
))}
</div>
{sortedVideos.length === 0 && (
<div className='text-muted-foreground py-8 text-center'>
No videos found matching your criteria
</div>
)}
</ScrollArea>
</CardContent>
</Card>
</motion.div>
)}
</div>
</PageContainer>
);
}
Component Sections
1. Header Section
<div className='flex items-start justify-between'>
<Heading
title='Playlist Analyzer'
description='Analyze YouTube playlists and get insights'
/>
</div>
Purpose: Displays the page title and description.
2. Input Form Section
<Card className='bg-background border-zinc-800'>
<CardContent className='p-4'>
<div className='flex space-x-2'>
<Input
type='text'
value={playlistUrl}
onChange={(e) => setPlaylistUrl(e.target.value)}
placeholder='Enter YouTube Playlist URL...'
className='flex-1'
/>
<Toast
state={toastState}
onSave={handleAnalyze}
onReset={handleReset}
/>
</div>
</CardContent>
</Card>
Features:
- Full-width input for playlist URL
- Toast component with state-based button behavior:
- Initial: Shows "Analyze" button
- Loading: Shows loading spinner
- Success: Shows success checkmark and "Reset" button
State Management:
const toastState = isLoading ? 'loading' : isSuccess ? 'success' : 'initial';
3. Welcome Message (Conditional)
{!playlistData && <FeatureCard type='analyze' />}
Purpose: Shows welcome message and feature description when no playlist is loaded.
Condition: Only rendered when playlistData
is null.
4. Analysis Results Section (Conditional)
Rendered only after successful playlist fetch:
{playlistData && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className='space-y-4'
>
{/* Content */}
</motion.div>
)}
Animation: Fades in with Framer Motion when data loads.
4a. Playlist Summary Card
<Card className='bg-background border-zinc-800'>
<CardContent className='p-4'>
<h3 className='mb-4 text-lg font-bold'>Playlist Summary</h3>
<div className='space-y-2'>
<div className='flex items-center gap-2'>
<PlayCircle className='h-4 w-4 text-blue-400' />
<span className='font-semibold'>
Total Videos: {playlistData.totalVideos}
</span>
</div>
<div className='flex items-center gap-2'>
<Clock className='h-4 w-4 text-green-500' />
<span className='font-semibold'>
Total Duration: {formatDuration(adjustedDuration)}
</span>
</div>
</div>
</CardContent>
</Card>
Displays:
- Total number of videos in playlist
- Total duration (adjusted for playback speed)
- Icons for visual clarity
4b. Filters Card
Contains three filter controls in a responsive grid:
Playback Speed Selector
<div className='flex items-center gap-2'>
<FastForward className='h-4 w-4 text-purple-400' />
<Select value={playbackSpeed} onValueChange={setPlaybackSpeed}>
<SelectTrigger className='w-full'>
<SelectValue placeholder='Speed' />
</SelectTrigger>
<SelectContent>
{['0.25', '0.5', '0.75', '1', '1.25', '1.5', '1.75', '2'].map((speed) => (
<SelectItem key={speed} value={speed}>
{speed}x
</SelectItem>
))}
</SelectContent>
</Select>
</div>
Options: 0.25x, 0.5x, 0.75x, 1x, 1.25x, 1.5x, 1.75x, 2x
Effect: Adjusts total duration calculation based on selected speed.
Sort By Selector
<div className='flex items-center gap-2'>
<SortAsc className='h-4 w-4 text-orange-400' />
<Select value={sortBy} onValueChange={setSortBy}>
<SelectTrigger className='w-full'>
<SelectValue placeholder='Sort' />
</SelectTrigger>
<SelectContent>
{['Position', 'Duration', 'Views', 'Likes', 'Publish Date'].map((option) => (
<SelectItem key={option} value={option.toLowerCase()}>
{option}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
Options: Position, Duration, Views, Likes, Publish Date
Effect: Reorders videos based on selected criteria.
Range Selection
<div className='flex items-center gap-2 sm:col-span-2 lg:col-span-1'>
<Calendar className='h-4 w-4 text-red-400' />
<Select value={rangeStart} onValueChange={setRangeStart}>
<SelectTrigger className='w-full'>
<SelectValue placeholder='Start' />
</SelectTrigger>
<SelectContent>
{rangeOptions.map((option) => (
<SelectItem key={option} value={option}>
{option}
</SelectItem>
))}
</SelectContent>
</Select>
<span className='text-muted-foreground'>-</span>
<Select value={rangeEnd} onValueChange={setRangeEnd}>
<SelectTrigger className='w-full'>
<SelectValue placeholder='End' />
</SelectTrigger>
<SelectContent>
{rangeOptions.map((option) => (
<SelectItem key={option} value={option}>
{option}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
Options: 1 to total videos (dynamically generated)
Effect: Filters videos to show only selected range.
Filter Status Text
<p className='text-muted-foreground mt-3 text-sm'>
Analyzing videos {rangeStart} to {rangeEnd} at speed: {playbackSpeed}x
</p>
Shows current filter settings in plain text.
4c. Search Bar
<div className='flex justify-center'>
<Input
className='w-full max-w-md'
placeholder='Search videos...'
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
Features:
- Real-time search as user types
- Case-insensitive matching
- Searches video titles
- Centered with max-width constraint
4d. Video Grid
<Card className='bg-background border-zinc-800'>
<CardContent className='p-4'>
<ScrollArea className='h-[500px]'>
<div className='grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3'>
{sortedVideos.map((video) => (
<VideoCard
key={video.id}
video={video}
searchQuery={searchQuery}
/>
))}
</div>
{sortedVideos.length === 0 && (
<div className='text-muted-foreground py-8 text-center'>
No videos found matching your criteria
</div>
)}
</ScrollArea>
</CardContent>
</Card>
Features:
- Responsive grid layout (1/2/3 columns)
- Fixed height with scrolling for large playlists
- Empty state message when no videos match filters
- Search query passed to VideoCard for highlighting
Event Handlers
handleAnalyze
const handleAnalyze = useCallback(() => {
// Check if URL is empty
if (!playlistUrl || !playlistUrl.trim()) {
toast.error('Please enter a YouTube playlist URL');
return;
}
// Validate YouTube playlist URL
const validation = validateYoutubePlaylistUrl(playlistUrl);
if (!validation.isValid) {
toast.error(
validation.error || 'Please enter a valid YouTube playlist URL'
);
return;
}
fetchPlaylist();
}, [fetchPlaylist, playlistUrl]);
Purpose: Validates URL and triggers playlist fetch.
Validation Steps:
- Check for empty/whitespace-only input
- Validate YouTube playlist URL format
- Call
fetchPlaylist
hook if valid
Error Handling: Shows toast notifications for validation errors.
Memoization: Wrapped in useCallback
to prevent unnecessary re-renders.
handleReset
const handleReset = useCallback(() => {
setIsSuccess(false);
}, [setIsSuccess]);
Purpose: Resets success state to hide success animation.
Trigger: Called when user clicks reset button after successful analysis.
Responsive Design
Mobile (< 640px)
- Single column layout
- Full-width inputs and selects
- Stacked filter controls
- 1 video per row in grid
Tablet (640px - 1024px)
- 2-column grid for summary/filters
- 2 videos per row in grid
- Filter controls in 2 columns
Desktop (> 1024px)
- 2-column grid for summary/filters
- 3 filter controls in one row
- 3 videos per row in grid
Responsive Grid Classes
// Summary/Filters grid
className='grid grid-cols-1 gap-4 lg:grid-cols-2'
// Filter controls grid
className='grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3'
// Video grid
className='grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3'
Animations
Fade-in Animation (Framer Motion)
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className='space-y-4'
>
Effect: Results section fades in smoothly when playlist loads.
Duration: Default (300ms)
Video Card Animations
Individual VideoCard components have their own animations:
- Fade in with slight upward movement
- Hover lift effect
- Smooth transitions
State Flow Diagram
User enters URL
↓
handleAnalyze triggered
↓
Validation
↓ (valid)
fetchPlaylist (hook)
↓
API call
↓
Store updated
↓
usePlaylistFilters processes data
↓
Component re-renders with results
↓
User applies filters
↓
usePlaylistFilters recalculates
↓
Video grid updates
Performance Optimizations
1. Memoized Callbacks
const handleAnalyze = useCallback(() => {
// ...
}, [fetchPlaylist, playlistUrl]);
Prevents function recreation on every render.
2. Conditional Rendering
{!playlistData && <FeatureCard />}
{playlistData && <Results />}
Only renders necessary sections.
3. Memoized Filters
All filtering/sorting happens in usePlaylistFilters
with useMemo
.
4. Virtual Scrolling
ScrollArea component handles large video lists efficiently.
5. Key Props
{sortedVideos.map((video) => (
<VideoCard key={video.id} video={video} />
))}
Proper keys for React reconciliation.
Accessibility Features
Semantic HTML
- Proper heading hierarchy (h1 → h3)
- Form controls with labels (via placeholder)
- Button roles (Toast component)
Keyboard Navigation
- Tab through inputs and selects
- Enter to submit form
- Arrow keys in dropdowns
Screen Readers
- Descriptive placeholders
- Icon + text labels
- Status messages (toast notifications)
Visual Feedback
- Loading states
- Success/error animations
- Hover states on interactive elements
Dependencies
UI Components (Shadcn/ui)
Card
,CardContent
Input
Select
,SelectContent
,SelectItem
,SelectTrigger
,SelectValue
ScrollArea
Heading
Custom Components
PageContainer
- Layout wrapperToast
- Action button with statesVideoCard
- Individual video displayFeatureCard
- Welcome message
External Libraries
framer-motion
- Animationslucide-react
- Iconssonner
- Toast notifications
Internal Modules
useAnalyzeStore
- State managementuseFetchPlaylist
- Data fetchingusePlaylistFilters
- Data processingvalidateYoutubePlaylistUrl
- URL validationformatDuration
- Time formatting
Usage Example
// In your Next.js app router
// app/dashboard/analyze/page.tsx
import AnalyzeViewPage from '@/features/analyze/components/analyze-view-page';
export default function AnalyzePage() {
return <AnalyzeViewPage />;
}
That's it! The component is completely self-contained.
Customization
Changing Colors
// Icon colors
<PlayCircle className='h-4 w-4 text-blue-400' /> // Change text-blue-400
<Clock className='h-4 w-4 text-green-500' /> // Change text-green-500
<FastForward className='h-4 w-4 text-purple-400' /> // Change text-purple-400
Adjusting Grid Breakpoints
// Video grid
className='grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4'
// ^^^ Add 4-column for xl screens
Changing ScrollArea Height
<ScrollArea className='h-[500px]'> // Change to h-[600px], h-[700px], etc.