banbaa
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
import { Media, MediaCategory } from '@/types';
|
||||
import MediaCard from './MediaCard';
|
||||
import MediaListItem from './MediaListItem';
|
||||
import { Filter, LayoutGrid, List, Star, ChevronLeft, ChevronRight, ArrowUpDown, Plus } from 'lucide-react';
|
||||
import { LayoutGrid, List, Star, ChevronLeft, ChevronRight, ArrowUpDown, Plus, Search } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import React, { useState, useMemo, useEffect } from 'react';
|
||||
import { createMedia, type CreateMediaInput } from '@/api';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -43,65 +44,130 @@ export default function BrowseView({ mediaList, onMediaClick, onAddMedia, active
|
||||
title: '',
|
||||
year: '',
|
||||
poster: '',
|
||||
banner: '',
|
||||
description: '',
|
||||
rating: '',
|
||||
category: activeCategory as MediaCategory,
|
||||
aspectRatio: '2/3' as '2/3' | '16/9' | '1/1'
|
||||
type: 'Movie' as string,
|
||||
status: 'Released' as string,
|
||||
aspectRatio: '2/3' as '2/3' | '16/9' | '1/1',
|
||||
runtime: '',
|
||||
director: '',
|
||||
writer: '',
|
||||
releaseDate: '',
|
||||
genres: '' as string,
|
||||
tags: '' as string,
|
||||
studios: '' as string
|
||||
});
|
||||
|
||||
// Update category and default aspect ratio when activeCategory changes
|
||||
// Update category, default aspect ratio, and default type when activeCategory changes
|
||||
useEffect(() => {
|
||||
let defaultAspect: '2/3' | '16/9' | '1/1' = '2/3';
|
||||
if (activeCategory === 'Music') defaultAspect = '1/1';
|
||||
if (activeCategory === 'Games' || activeCategory === 'Adult') defaultAspect = '16/9';
|
||||
let defaultType = 'Movie';
|
||||
|
||||
if (activeCategory === 'Music') {
|
||||
defaultAspect = '1/1';
|
||||
defaultType = 'Album';
|
||||
} else if (activeCategory === 'Games') {
|
||||
defaultAspect = '16/9';
|
||||
defaultType = 'Game';
|
||||
} else if (activeCategory === 'Adult') {
|
||||
defaultAspect = '16/9';
|
||||
defaultType = 'Movie';
|
||||
} else if (activeCategory === 'Anime') {
|
||||
defaultType = 'TV';
|
||||
} else if (activeCategory === 'Books') {
|
||||
defaultType = 'Hardcover';
|
||||
} else if (activeCategory === 'Consoles') {
|
||||
defaultType = 'Console';
|
||||
}
|
||||
|
||||
setNewMedia(prev => ({
|
||||
...prev,
|
||||
category: activeCategory,
|
||||
aspectRatio: defaultAspect
|
||||
aspectRatio: defaultAspect,
|
||||
type: defaultType
|
||||
}));
|
||||
}, [activeCategory]);
|
||||
|
||||
const handleAddSubmit = (e: React.FormEvent) => {
|
||||
const handleAddSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!newMedia.title || !newMedia.poster) return;
|
||||
|
||||
onAddMedia({
|
||||
id: Math.random().toString(36).substr(2, 9),
|
||||
// Convert category from plural to singular to match API format
|
||||
const categoryMap: Record<string, string> = {
|
||||
'Anime': 'Anime',
|
||||
'Movies': 'Movie',
|
||||
'Music': 'Music',
|
||||
'Books': 'Book',
|
||||
'Consoles': 'Console',
|
||||
'Games': 'Game',
|
||||
'Adult': 'Adult'
|
||||
};
|
||||
|
||||
const mediaInput: CreateMediaInput = {
|
||||
title: newMedia.title,
|
||||
year: newMedia.year || new Date().getFullYear().toString(),
|
||||
year: parseInt(newMedia.year) || new Date().getFullYear(),
|
||||
poster: newMedia.poster,
|
||||
category: newMedia.category,
|
||||
banner: newMedia.banner || null,
|
||||
description: newMedia.description || null,
|
||||
rating: newMedia.rating ? parseFloat(newMedia.rating) : null,
|
||||
category: categoryMap[newMedia.category] || newMedia.category,
|
||||
type: newMedia.type,
|
||||
status: newMedia.status,
|
||||
aspectRatio: newMedia.aspectRatio,
|
||||
status: 'planned'
|
||||
});
|
||||
runtime: newMedia.runtime ? parseInt(newMedia.runtime) : null,
|
||||
director: newMedia.director || null,
|
||||
writer: newMedia.writer || null,
|
||||
releaseDate: newMedia.releaseDate || null,
|
||||
genres: newMedia.genres ? newMedia.genres.split(',').map(g => g.trim()) : [],
|
||||
tags: newMedia.tags ? newMedia.tags.split(',').map(t => t.trim()) : [],
|
||||
studios: newMedia.studios ? newMedia.studios.split(',').map(s => s.trim()) : []
|
||||
};
|
||||
|
||||
const createdMedia = await createMedia(mediaInput);
|
||||
|
||||
if (createdMedia) {
|
||||
onAddMedia(createdMedia);
|
||||
}
|
||||
|
||||
setNewMedia({
|
||||
title: '',
|
||||
year: '',
|
||||
poster: '',
|
||||
banner: '',
|
||||
description: '',
|
||||
rating: '',
|
||||
category: activeCategory,
|
||||
aspectRatio: '2/3'
|
||||
type: 'Movie',
|
||||
status: 'Released',
|
||||
aspectRatio: '2/3',
|
||||
runtime: '',
|
||||
director: '',
|
||||
writer: '',
|
||||
releaseDate: '',
|
||||
genres: '',
|
||||
tags: '',
|
||||
studios: ''
|
||||
});
|
||||
setIsAddDialogOpen(false);
|
||||
};
|
||||
|
||||
// Filter states
|
||||
const [selectedType, setSelectedType] = useState<string | null>(null);
|
||||
const [selectedGenre, setSelectedGenre] = useState<string | null>(null);
|
||||
const [selectedStudio, setSelectedStudio] = useState<string | null>(null);
|
||||
|
||||
// Extract unique values for filters
|
||||
const allTypes = useMemo(() => Array.from(new Set(mediaList.map(m => m.type).filter(Boolean))), [mediaList]);
|
||||
const allGenres = useMemo(() => Array.from(new Set(mediaList.flatMap(m => m.genres || []))), [mediaList]);
|
||||
const allStudios = useMemo(() => Array.from(new Set(mediaList.flatMap(m => m.studios || []))), [mediaList]);
|
||||
|
||||
const filteredMedia = useMemo(() => {
|
||||
return mediaList.filter(media => {
|
||||
if (selectedType && media.type !== selectedType) return false;
|
||||
if (selectedGenre && !media.genres?.includes(selectedGenre)) return false;
|
||||
if (selectedStudio && !media.studios?.includes(selectedStudio)) return false;
|
||||
return true;
|
||||
});
|
||||
}, [mediaList, selectedType, selectedGenre, selectedStudio]);
|
||||
}, [mediaList, selectedGenre, selectedStudio]);
|
||||
|
||||
// Reset to first page when mediaList or filters change
|
||||
useEffect(() => {
|
||||
@@ -141,29 +207,13 @@ export default function BrowseView({ mediaList, onMediaClick, onAddMedia, active
|
||||
{/* Filters Bar */}
|
||||
<div className="flex flex-wrap items-center justify-between gap-4 mb-8">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{/* Type Filter */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="sm" className={cn("font-bold gap-2", selectedType ? "text-[#6d28d9] bg-[#6d28d9]/10" : "text-zinc-600")}>
|
||||
<Filter size={16} />
|
||||
{selectedType || 'Media Type'}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuItem onClick={() => setSelectedType(null)}>All Types</DropdownMenuItem>
|
||||
{allTypes.map(type => (
|
||||
<DropdownMenuItem key={type} onClick={() => setSelectedType(type!)}>{type}</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
{/* Genre Filter */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="sm" className={cn("font-bold gap-2", selectedGenre ? "text-[#6d28d9] bg-[#6d28d9]/10" : "text-zinc-600")}>
|
||||
<button type="button" className={cn("group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50 h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5 font-bold gap-2", selectedGenre ? "text-[#6d28d9] bg-[#6d28d9]/10" : "text-zinc-600")}>
|
||||
<Star size={16} />
|
||||
{selectedGenre || 'Genres'}
|
||||
</Button>
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="max-h-[300px] overflow-y-auto">
|
||||
<DropdownMenuItem onClick={() => setSelectedGenre(null)}>All Genres</DropdownMenuItem>
|
||||
@@ -176,9 +226,9 @@ export default function BrowseView({ mediaList, onMediaClick, onAddMedia, active
|
||||
{/* Studio Filter */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="sm" className={cn("font-bold gap-2", selectedStudio ? "text-[#6d28d9] bg-[#6d28d9]/10" : "text-zinc-600")}>
|
||||
<button type="button" className={cn("group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50 h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5 font-bold gap-2", selectedStudio ? "text-[#6d28d9] bg-[#6d28d9]/10" : "text-zinc-600")}>
|
||||
Studios
|
||||
</Button>
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="max-h-[300px] overflow-y-auto">
|
||||
<DropdownMenuItem onClick={() => setSelectedStudio(null)}>All Studios</DropdownMenuItem>
|
||||
@@ -188,13 +238,12 @@ export default function BrowseView({ mediaList, onMediaClick, onAddMedia, active
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
{(selectedType || selectedGenre || selectedStudio) && (
|
||||
{(selectedGenre || selectedStudio) && (
|
||||
<Button
|
||||
variant="link"
|
||||
size="sm"
|
||||
className="text-zinc-400 font-bold"
|
||||
onClick={() => {
|
||||
setSelectedType(null);
|
||||
setSelectedGenre(null);
|
||||
setSelectedStudio(null);
|
||||
}}
|
||||
@@ -207,10 +256,10 @@ export default function BrowseView({ mediaList, onMediaClick, onAddMedia, active
|
||||
<div className="flex items-center gap-4">
|
||||
<Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="bg-[#6d28d9] hover:bg-[#5b21b6] text-white font-black rounded-full px-6 h-11 shadow-lg shadow-[#6d28d9]/20 gap-2">
|
||||
<button type="button" className="group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 bg-[#6d28d9] hover:bg-[#5b21b6] text-white font-black rounded-full px-6 h-11 shadow-lg shadow-[#6d28d9]/20 gap-2">
|
||||
<Plus size={20} />
|
||||
ADD NEW
|
||||
</Button>
|
||||
</button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[425px] bg-white rounded-3xl">
|
||||
<form onSubmit={handleAddSubmit}>
|
||||
@@ -220,7 +269,7 @@ export default function BrowseView({ mediaList, onMediaClick, onAddMedia, active
|
||||
Manually add a new item to your {activeCategory} library.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-6 py-6">
|
||||
<div className="grid gap-4 py-6 max-h-[60vh] overflow-y-auto">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="title" className="text-sm font-black text-zinc-700">Title</Label>
|
||||
<Input
|
||||
@@ -257,6 +306,64 @@ export default function BrowseView({ mediaList, onMediaClick, onAddMedia, active
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="type" className="text-sm font-black text-zinc-700">Type</Label>
|
||||
<select
|
||||
id="type"
|
||||
value={newMedia.type}
|
||||
onChange={e => setNewMedia(prev => ({ ...prev, type: e.target.value }))}
|
||||
className="bg-zinc-50 border border-zinc-100 rounded-xl h-11 px-3 text-sm font-bold text-zinc-700 focus:ring-2 focus:ring-[#6d28d9] outline-none"
|
||||
>
|
||||
{newMedia.category === 'Music' ? (
|
||||
<>
|
||||
<option value="Album">Album</option>
|
||||
<option value="Single">Single</option>
|
||||
</>
|
||||
) : newMedia.category === 'Books' ? (
|
||||
<>
|
||||
<option value="Hardcover">Hardcover</option>
|
||||
<option value="E-book">E-book</option>
|
||||
</>
|
||||
) : newMedia.category === 'Games' ? (
|
||||
<>
|
||||
<option value="Game">Game</option>
|
||||
</>
|
||||
) : newMedia.category === 'Consoles' ? (
|
||||
<>
|
||||
<option value="Console">Console</option>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<option value="TV">TV</option>
|
||||
<option value="Movie">Movie</option>
|
||||
<option value="OVA">OVA</option>
|
||||
<option value="ONA">ONA</option>
|
||||
</>
|
||||
)}
|
||||
</select>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="status" className="text-sm font-black text-zinc-700">Status</Label>
|
||||
<select
|
||||
id="status"
|
||||
value={newMedia.status}
|
||||
onChange={e => setNewMedia(prev => ({ ...prev, status: e.target.value }))}
|
||||
className="bg-zinc-50 border border-zinc-100 rounded-xl h-11 px-3 text-sm font-bold text-zinc-700 focus:ring-2 focus:ring-[#6d28d9] outline-none"
|
||||
>
|
||||
<option value="Released">Released</option>
|
||||
<option value="Ongoing">Ongoing</option>
|
||||
<option value="Upcoming">Upcoming</option>
|
||||
<option value="Completed">Completed</option>
|
||||
<option value="Watching">Watching</option>
|
||||
<option value="Reading">Reading</option>
|
||||
<option value="Listening">Listening</option>
|
||||
<option value="Playing">Playing</option>
|
||||
<option value="Dropped">Dropped</option>
|
||||
<option value="On Hold">On Hold</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="aspectRatio" className="text-sm font-black text-zinc-700">Aspect Ratio (Format)</Label>
|
||||
<select
|
||||
@@ -265,9 +372,9 @@ export default function BrowseView({ mediaList, onMediaClick, onAddMedia, active
|
||||
onChange={e => setNewMedia(prev => ({ ...prev, aspectRatio: e.target.value as '2/3' | '16/9' | '1/1' }))}
|
||||
className="bg-zinc-50 border border-zinc-100 rounded-xl h-11 px-3 text-sm font-bold text-zinc-700 focus:ring-2 focus:ring-[#6d28d9] outline-none"
|
||||
>
|
||||
<option value="2/3">2:3 (Standard Poster - Anime/Movies)</option>
|
||||
<option value="16/9">16:9 (Wide Thumbnail - Games/Adult)</option>
|
||||
<option value="1/1">1:1 (Square - Music)</option>
|
||||
<option value="2/3">2:3 (Standard Poster)</option>
|
||||
<option value="16/9">16:9 (Wide Thumbnail)</option>
|
||||
<option value="1/1">1:1 (Square)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
@@ -281,6 +388,117 @@ export default function BrowseView({ mediaList, onMediaClick, onAddMedia, active
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="banner" className="text-sm font-black text-zinc-700">Banner URL (Optional)</Label>
|
||||
<Input
|
||||
id="banner"
|
||||
value={newMedia.banner}
|
||||
onChange={e => setNewMedia(prev => ({ ...prev, banner: e.target.value }))}
|
||||
placeholder="https://example.com/banner.jpg"
|
||||
className="bg-zinc-50 border-zinc-100 rounded-xl h-11 focus:ring-[#6d28d9]"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="description" className="text-sm font-black text-zinc-700">Description (Optional)</Label>
|
||||
<textarea
|
||||
id="description"
|
||||
value={newMedia.description}
|
||||
onChange={e => setNewMedia(prev => ({ ...prev, description: e.target.value }))}
|
||||
placeholder="Brief description..."
|
||||
className="bg-zinc-50 border-zinc-100 rounded-xl p-3 h-20 text-sm font-bold text-zinc-700 focus:ring-2 focus:ring-[#6d28d9] outline-none resize-none"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="rating" className="text-sm font-black text-zinc-700">Rating (Optional)</Label>
|
||||
<Input
|
||||
id="rating"
|
||||
type="number"
|
||||
step="0.1"
|
||||
min="0"
|
||||
max="10"
|
||||
value={newMedia.rating}
|
||||
onChange={e => setNewMedia(prev => ({ ...prev, rating: e.target.value }))}
|
||||
placeholder="8.5"
|
||||
className="bg-zinc-50 border-zinc-100 rounded-xl h-11 focus:ring-[#6d28d9]"
|
||||
/>
|
||||
</div>
|
||||
{(newMedia.category === 'Anime' || newMedia.category === 'Movies' || newMedia.category === 'Adult') && (
|
||||
<>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="runtime" className="text-sm font-black text-zinc-700">Runtime (min)</Label>
|
||||
<Input
|
||||
id="runtime"
|
||||
type="number"
|
||||
value={newMedia.runtime}
|
||||
onChange={e => setNewMedia(prev => ({ ...prev, runtime: e.target.value }))}
|
||||
placeholder="120"
|
||||
className="bg-zinc-50 border-zinc-100 rounded-xl h-11 focus:ring-[#6d28d9]"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="releaseDate" className="text-sm font-black text-zinc-700">Release Date</Label>
|
||||
<Input
|
||||
id="releaseDate"
|
||||
type="date"
|
||||
value={newMedia.releaseDate}
|
||||
onChange={e => setNewMedia(prev => ({ ...prev, releaseDate: e.target.value }))}
|
||||
className="bg-zinc-50 border-zinc-100 rounded-xl h-11 focus:ring-[#6d28d9]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="director" className="text-sm font-black text-zinc-700">Director</Label>
|
||||
<Input
|
||||
id="director"
|
||||
value={newMedia.director}
|
||||
onChange={e => setNewMedia(prev => ({ ...prev, director: e.target.value }))}
|
||||
placeholder="Director name"
|
||||
className="bg-zinc-50 border-zinc-100 rounded-xl h-11 focus:ring-[#6d28d9]"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="writer" className="text-sm font-black text-zinc-700">Writer</Label>
|
||||
<Input
|
||||
id="writer"
|
||||
value={newMedia.writer}
|
||||
onChange={e => setNewMedia(prev => ({ ...prev, writer: e.target.value }))}
|
||||
placeholder="Writer name"
|
||||
className="bg-zinc-50 border-zinc-100 rounded-xl h-11 focus:ring-[#6d28d9]"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="genres" className="text-sm font-black text-zinc-700">Genres (comma-separated)</Label>
|
||||
<Input
|
||||
id="genres"
|
||||
value={newMedia.genres}
|
||||
onChange={e => setNewMedia(prev => ({ ...prev, genres: e.target.value }))}
|
||||
placeholder="Action, Drama, Sci-Fi"
|
||||
className="bg-zinc-50 border-zinc-100 rounded-xl h-11 focus:ring-[#6d28d9]"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="tags" className="text-sm font-black text-zinc-700">Tags (comma-separated)</Label>
|
||||
<Input
|
||||
id="tags"
|
||||
value={newMedia.tags}
|
||||
onChange={e => setNewMedia(prev => ({ ...prev, tags: e.target.value }))}
|
||||
placeholder="Classic, Best-selling"
|
||||
className="bg-zinc-50 border-zinc-100 rounded-xl h-11 focus:ring-[#6d28d9]"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="studios" className="text-sm font-black text-zinc-700">Studios (comma-separated)</Label>
|
||||
<Input
|
||||
id="studios"
|
||||
value={newMedia.studios}
|
||||
onChange={e => setNewMedia(prev => ({ ...prev, studios: e.target.value }))}
|
||||
placeholder="Studio A, Studio B"
|
||||
className="bg-zinc-50 border-zinc-100 rounded-xl h-11 focus:ring-[#6d28d9]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button type="submit" className="w-full bg-[#6d28d9] hover:bg-[#5b21b6] text-white font-black h-12 rounded-xl shadow-lg shadow-[#6d28d9]/20">
|
||||
@@ -293,10 +511,10 @@ export default function BrowseView({ mediaList, onMediaClick, onAddMedia, active
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="sm" className="text-zinc-600 font-bold gap-2">
|
||||
<button type="button" className="group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50 h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5 text-zinc-600 font-bold gap-2">
|
||||
<ArrowUpDown size={16} />
|
||||
{sortBy === 'default' ? 'Sort' : sortBy === 'title-asc' ? 'Title (A-Z)' : 'Title (Z-A)'}
|
||||
</Button>
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => setSortBy('default')}>Default</DropdownMenuItem>
|
||||
@@ -336,7 +554,7 @@ export default function BrowseView({ mediaList, onMediaClick, onAddMedia, active
|
||||
{mediaList.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-20 text-zinc-400">
|
||||
<div className="w-16 h-16 bg-zinc-100 rounded-full flex items-center justify-center mb-4">
|
||||
<Filter size={32} />
|
||||
<Search size={32} />
|
||||
</div>
|
||||
<p className="text-lg font-bold">No results found</p>
|
||||
<p className="text-sm">Try adjusting your search or filters</p>
|
||||
|
||||
Reference in New Issue
Block a user