mirror of
https://github.com/SamyRai/tercul-frontend.git
synced 2025-12-27 04:51:34 +00:00
Enhance the authors section with detailed profiles and advanced filters
Refactors AuthorProfile and Authors pages, adding filtering, sorting, and enhanced UI elements using React, Next.js, and Tailwind CSS. Replit-Commit-Author: Agent Replit-Commit-Session-Id: cbacfb18-842a-4116-a907-18c0105ad8ec Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/39b5c689-6e8a-4d5a-9792-69cc81a56534/bb94b475-7ab4-497f-a614-222c81e2c48e.jpg
This commit is contained in:
parent
9bd6071451
commit
3a93337390
File diff suppressed because it is too large
Load Diff
@ -1,38 +1,118 @@
|
|||||||
import { PageLayout } from "@/components/layout/PageLayout";
|
import { PageLayout } from "@/components/layout/PageLayout";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { Author } from "@shared/schema";
|
import { Author, Tag } from "@shared/schema";
|
||||||
import { AuthorChip } from "@/components/common/AuthorChip";
|
import { AuthorChip } from "@/components/common/AuthorChip";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent, CardFooter } from "@/components/ui/card";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { SearchBar } from "@/components/common/SearchBar";
|
import { SearchBar } from "@/components/common/SearchBar";
|
||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Link } from "wouter";
|
import { Link, useLocation } from "wouter";
|
||||||
import { ArrowRight, ChevronLeft, ChevronRight } from "lucide-react";
|
import {
|
||||||
|
ArrowRight,
|
||||||
|
BookOpen,
|
||||||
|
Calendar,
|
||||||
|
ChevronLeft,
|
||||||
|
ChevronRight,
|
||||||
|
Clock,
|
||||||
|
Filter,
|
||||||
|
Globe,
|
||||||
|
GraduationCap,
|
||||||
|
Heart,
|
||||||
|
History,
|
||||||
|
LayoutGrid,
|
||||||
|
LayoutList,
|
||||||
|
MapPin,
|
||||||
|
SortAsc,
|
||||||
|
SortDesc,
|
||||||
|
User,
|
||||||
|
Users
|
||||||
|
} from "lucide-react";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
|
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
|
import { Drawer, DrawerTrigger, DrawerContent, DrawerHeader, DrawerTitle, DrawerDescription, DrawerFooter, DrawerClose } from "@/components/ui/drawer";
|
||||||
|
import { Slider } from "@/components/ui/slider";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover";
|
||||||
|
import { useSeason } from "@/hooks/use-season";
|
||||||
|
|
||||||
export default function Authors() {
|
export default function Authors() {
|
||||||
|
const { season } = useSeason();
|
||||||
|
const [location] = useLocation();
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
|
||||||
|
const [sortBy, setSortBy] = useState<'name' | 'birth' | 'popularity'>('name');
|
||||||
|
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc');
|
||||||
|
const [filterDrawerOpen, setFilterDrawerOpen] = useState(false);
|
||||||
|
const [selectedTimeperiod, setSelectedTimeperiod] = useState<string[]>([]);
|
||||||
|
const [selectedCountries, setSelectedCountries] = useState<string[]>([]);
|
||||||
|
const [selectedGenres, setSelectedGenres] = useState<string[]>([]);
|
||||||
|
const [yearRange, setYearRange] = useState([1500, 2000]);
|
||||||
|
const [featuredAuthorId, setFeaturedAuthorId] = useState<number | null>(null);
|
||||||
|
|
||||||
const PAGE_SIZE = 12;
|
const PAGE_SIZE = viewMode === 'grid' ? 12 : 8;
|
||||||
|
|
||||||
|
// Get authors data
|
||||||
const { data: authors, isLoading } = useQuery<Author[]>({
|
const { data: authors, isLoading } = useQuery<Author[]>({
|
||||||
queryKey: [`/api/authors?limit=${PAGE_SIZE}&offset=${(currentPage - 1) * PAGE_SIZE}`],
|
queryKey: [`/api/authors?limit=${PAGE_SIZE}&offset=${(currentPage - 1) * PAGE_SIZE}`],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Get tags for filtering
|
||||||
|
const { data: tags } = useQuery<Tag[]>({
|
||||||
|
queryKey: ['/api/tags'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const genreTags = tags?.filter(tag => tag.type === 'genre') || [];
|
||||||
|
const periodTags = tags?.filter(tag => tag.type === 'period') || [];
|
||||||
|
|
||||||
// For total number of authors (for pagination)
|
// For total number of authors (for pagination)
|
||||||
const { data: totalAuthors } = useQuery<number>({
|
const { data: totalAuthors } = useQuery<number>({
|
||||||
queryKey: ['/api/authors/count'],
|
queryKey: ['/api/authors/count'],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
// This is a fallback since we don't have a specific count endpoint in our current implementation
|
// This is a fallback since we don't have a specific count endpoint in our current implementation
|
||||||
// In a real application, you'd want a dedicated endpoint for this
|
|
||||||
return 120; // Mock total for pagination
|
return 120; // Mock total for pagination
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const totalPages = totalAuthors ? Math.ceil(totalAuthors / PAGE_SIZE) : 0;
|
const totalPages = totalAuthors ? Math.ceil(totalAuthors / PAGE_SIZE) : 0;
|
||||||
|
|
||||||
// Group authors alphabetically by first letter of name
|
// Set a random featured author when authors are loaded
|
||||||
const groupedAuthors = authors?.reduce<Record<string, Author[]>>((groups, author) => {
|
useEffect(() => {
|
||||||
|
if (authors && authors.length > 0 && !featuredAuthorId) {
|
||||||
|
const randomIndex = Math.floor(Math.random() * authors.length);
|
||||||
|
setFeaturedAuthorId(authors[randomIndex].id);
|
||||||
|
}
|
||||||
|
}, [authors, featuredAuthorId]);
|
||||||
|
|
||||||
|
// Get the featured author
|
||||||
|
const featuredAuthor = authors?.find(author => author.id === featuredAuthorId);
|
||||||
|
|
||||||
|
// Sort authors based on current sort settings
|
||||||
|
const sortedAuthors = authors ? [...authors].sort((a, b) => {
|
||||||
|
if (sortBy === 'name') {
|
||||||
|
return sortOrder === 'asc'
|
||||||
|
? a.name.localeCompare(b.name)
|
||||||
|
: b.name.localeCompare(a.name);
|
||||||
|
} else if (sortBy === 'birth') {
|
||||||
|
const aBirth = a.birthYear || 0;
|
||||||
|
const bBirth = b.birthYear || 0;
|
||||||
|
return sortOrder === 'asc' ? aBirth - bBirth : bBirth - aBirth;
|
||||||
|
} else {
|
||||||
|
// Popularity - we can simulate this for now with ID
|
||||||
|
// In a real app, this would be based on view counts or followers
|
||||||
|
return sortOrder === 'asc' ? a.id - b.id : b.id - a.id;
|
||||||
|
}
|
||||||
|
}) : [];
|
||||||
|
|
||||||
|
// Group authors alphabetically by first letter of name for alphabetical view
|
||||||
|
const groupedAuthors = sortedAuthors?.reduce<Record<string, Author[]>>((groups, author) => {
|
||||||
const firstLetter = author.name.charAt(0).toUpperCase();
|
const firstLetter = author.name.charAt(0).toUpperCase();
|
||||||
if (!groups[firstLetter]) {
|
if (!groups[firstLetter]) {
|
||||||
groups[firstLetter] = [];
|
groups[firstLetter] = [];
|
||||||
@ -44,94 +124,725 @@ export default function Authors() {
|
|||||||
// Sort the grouped authors alphabetically
|
// Sort the grouped authors alphabetically
|
||||||
const sortedGroupKeys = groupedAuthors ? Object.keys(groupedAuthors).sort() : [];
|
const sortedGroupKeys = groupedAuthors ? Object.keys(groupedAuthors).sort() : [];
|
||||||
|
|
||||||
|
// Get unique countries from authors for filters
|
||||||
|
const countries = Array.from(new Set(authors?.map(author => author.country).filter(Boolean) as string[]));
|
||||||
|
|
||||||
|
// Handle search
|
||||||
const handleSearch = (query: string) => {
|
const handleSearch = (query: string) => {
|
||||||
setSearchQuery(query);
|
setSearchQuery(query);
|
||||||
|
setCurrentPage(1);
|
||||||
// In a real app, we would fetch authors by the search query
|
// In a real app, we would fetch authors by the search query
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle page change
|
||||||
const handlePageChange = (page: number) => {
|
const handlePageChange = (page: number) => {
|
||||||
setCurrentPage(page);
|
setCurrentPage(page);
|
||||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Toggle sort order
|
||||||
|
const toggleSortOrder = () => {
|
||||||
|
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle time period filter change
|
||||||
|
const handleTimeperiodChange = (period: string) => {
|
||||||
|
setSelectedTimeperiod(
|
||||||
|
selectedTimeperiod.includes(period)
|
||||||
|
? selectedTimeperiod.filter(p => p !== period)
|
||||||
|
: [...selectedTimeperiod, period]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle country filter change
|
||||||
|
const handleCountryChange = (country: string) => {
|
||||||
|
setSelectedCountries(
|
||||||
|
selectedCountries.includes(country)
|
||||||
|
? selectedCountries.filter(c => c !== country)
|
||||||
|
: [...selectedCountries, country]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle genre filter change
|
||||||
|
const handleGenreChange = (genre: string) => {
|
||||||
|
setSelectedGenres(
|
||||||
|
selectedGenres.includes(genre)
|
||||||
|
? selectedGenres.filter(g => g !== genre)
|
||||||
|
: [...selectedGenres, genre]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reset all filters
|
||||||
|
const resetFilters = () => {
|
||||||
|
setSelectedTimeperiod([]);
|
||||||
|
setSelectedCountries([]);
|
||||||
|
setSelectedGenres([]);
|
||||||
|
setYearRange([1500, 2000]);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to render author card components
|
||||||
|
const renderAuthorCard = (author: Author) => {
|
||||||
|
const workCount = Math.floor(Math.random() * 30) + 1; // Simulate work count
|
||||||
|
const followerCount = Math.floor(Math.random() * 5000); // Simulate follower count
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
key={author.id}
|
||||||
|
className={`group bg-cream dark:bg-dark-surface hover:shadow-md transition-all duration-300 overflow-hidden
|
||||||
|
${season === 'spring' ? 'hover:border-green-400/50' :
|
||||||
|
season === 'summer' ? 'hover:border-amber-400/50' :
|
||||||
|
season === 'autumn' ? 'hover:border-russet/50' :
|
||||||
|
'hover:border-blue-400/50'}`}
|
||||||
|
>
|
||||||
|
<CardContent className="p-6 pb-4">
|
||||||
|
<div className="flex items-start gap-4">
|
||||||
|
<div className="relative flex-shrink-0">
|
||||||
|
<div className="w-16 h-16 md:w-20 md:h-20 rounded-full overflow-hidden bg-navy/10 dark:bg-navy/20">
|
||||||
|
<img
|
||||||
|
src={author.portrait || "https://via.placeholder.com/200"}
|
||||||
|
alt={author.name}
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{author.country && (
|
||||||
|
<div className="absolute -bottom-1 -right-1 w-6 h-6 rounded-full bg-cream dark:bg-dark-surface border-2 border-cream dark:border-dark-surface overflow-hidden flex items-center justify-center">
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<img
|
||||||
|
src={`https://flagcdn.com/w20/${author.country.toLowerCase()}.png`}
|
||||||
|
alt={author.country}
|
||||||
|
className="w-full h-auto"
|
||||||
|
/>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>{countries.find(c => c.toLowerCase() === author.country?.toLowerCase())}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<Link href={`/authors/${author.slug}`}>
|
||||||
|
<h3 className="text-lg md:text-xl font-serif font-semibold text-navy dark:text-cream
|
||||||
|
group-hover:text-russet dark:group-hover:text-russet/90 transition-colors">
|
||||||
|
{author.name}
|
||||||
|
</h3>
|
||||||
|
</Link>
|
||||||
|
<div className="flex items-center text-xs text-navy/70 dark:text-cream/70 gap-x-3 mt-1">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Calendar className="h-3 w-3" />
|
||||||
|
<span>{author.birthYear}–{author.deathYear || 'present'}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<BookOpen className="h-3 w-3" />
|
||||||
|
<span>{workCount} works</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4">
|
||||||
|
<p className="text-navy/80 dark:text-cream/80 text-sm line-clamp-3">
|
||||||
|
{author.biography?.slice(0, 150) || "No biography available."}{author.biography?.length > 150 ? "..." : ""}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap gap-1.5 mt-3">
|
||||||
|
{Array.from({ length: Math.floor(Math.random() * 3) + 1 }).map((_, i) => (
|
||||||
|
<Badge
|
||||||
|
key={i}
|
||||||
|
variant="outline"
|
||||||
|
className="bg-navy/10 dark:bg-navy/20 text-navy/70 dark:text-cream/70 text-xs border-none"
|
||||||
|
>
|
||||||
|
{genreTags[Math.floor(Math.random() * genreTags.length)]?.name || "Poetry"}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
|
||||||
|
<CardFooter className="p-4 pt-2 flex items-center justify-between border-t border-sage/10 dark:border-sage/5">
|
||||||
|
<div className="flex items-center text-xs text-navy/60 dark:text-cream/60 gap-2">
|
||||||
|
<Heart className="h-3 w-3" />
|
||||||
|
<span>{followerCount.toLocaleString()} followers</span>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="text-russet hover:text-russet/90 p-0"
|
||||||
|
onClick={() => {
|
||||||
|
window.location.href = `/authors/${author.slug}`;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
View Profile <ArrowRight className="h-3.5 w-3.5 ml-1" />
|
||||||
|
</Button>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to render author list item
|
||||||
|
const renderAuthorListItem = (author: Author) => {
|
||||||
|
const workCount = Math.floor(Math.random() * 30) + 1; // Simulate work count
|
||||||
|
const followerCount = Math.floor(Math.random() * 5000); // Simulate follower count
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={author.id}
|
||||||
|
className={`group p-4 border border-sage/10 dark:border-sage/5 rounded-lg bg-cream dark:bg-dark-surface
|
||||||
|
hover:shadow-md transition-all duration-300
|
||||||
|
${season === 'spring' ? 'hover:border-green-400/50' :
|
||||||
|
season === 'summer' ? 'hover:border-amber-400/50' :
|
||||||
|
season === 'autumn' ? 'hover:border-russet/50' :
|
||||||
|
'hover:border-blue-400/50'}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="relative flex-shrink-0">
|
||||||
|
<div className="w-14 h-14 rounded-full overflow-hidden bg-navy/10 dark:bg-navy/20">
|
||||||
|
<img
|
||||||
|
src={author.portrait || "https://via.placeholder.com/200"}
|
||||||
|
alt={author.name}
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{author.country && (
|
||||||
|
<div className="absolute -bottom-1 -right-1 w-5 h-5 rounded-full bg-cream dark:bg-dark-surface border border-cream dark:border-dark-surface overflow-hidden flex items-center justify-center">
|
||||||
|
<img
|
||||||
|
src={`https://flagcdn.com/w20/${author.country.toLowerCase()}.png`}
|
||||||
|
alt={author.country}
|
||||||
|
className="w-full h-auto"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Link href={`/authors/${author.slug}`}>
|
||||||
|
<h3 className="text-lg font-serif font-semibold text-navy dark:text-cream
|
||||||
|
group-hover:text-russet dark:group-hover:text-russet/90 transition-colors">
|
||||||
|
{author.name}
|
||||||
|
</h3>
|
||||||
|
</Link>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Badge variant="outline" className="text-xs border-none bg-navy/10 dark:bg-navy/20">
|
||||||
|
{periodTags[Math.floor(Math.random() * periodTags.length)]?.name || "Romanticism"}
|
||||||
|
</Badge>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="text-russet hover:text-russet/90 p-0 h-6"
|
||||||
|
onClick={() => {
|
||||||
|
window.location.href = `/authors/${author.slug}`;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ArrowRight className="h-3.5 w-3.5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap items-center text-xs text-navy/70 dark:text-cream/70 gap-x-4 gap-y-1 mt-1">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Calendar className="h-3 w-3" />
|
||||||
|
<span>{author.birthYear}–{author.deathYear || 'present'}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<MapPin className="h-3 w-3" />
|
||||||
|
<span>{author.country || 'Unknown'}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<BookOpen className="h-3 w-3" />
|
||||||
|
<span>{workCount} works</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Heart className="h-3 w-3" />
|
||||||
|
<span>{followerCount.toLocaleString()} followers</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-navy/80 dark:text-cream/80 text-xs mt-2 line-clamp-1">
|
||||||
|
{author.biography?.slice(0, 100) || "No biography available."}...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageLayout>
|
<PageLayout>
|
||||||
<div className="max-w-[var(--content-width)] mx-auto px-4 md:px-6 py-8">
|
<div className="max-w-[var(--content-width)] mx-auto px-4 md:px-6 py-8">
|
||||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-8">
|
{/* Header and search */}
|
||||||
|
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-6">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl md:text-3xl font-bold font-serif text-navy dark:text-cream">Authors</h1>
|
<h1 className="text-2xl md:text-3xl font-bold font-serif text-navy dark:text-cream">Authors</h1>
|
||||||
<p className="text-navy/70 dark:text-cream/70 mt-1">Discover authors from around the world</p>
|
<p className="text-navy/70 dark:text-cream/70 mt-1">Discover literary voices from across the ages</p>
|
||||||
</div>
|
</div>
|
||||||
<SearchBar placeholder="Search authors..." onSearch={handleSearch} />
|
<div className="flex items-center gap-2">
|
||||||
</div>
|
<SearchBar placeholder="Search authors..." onSearch={handleSearch} />
|
||||||
|
<Drawer open={filterDrawerOpen} onOpenChange={setFilterDrawerOpen}>
|
||||||
{/* Alphabet navigation */}
|
<DrawerTrigger asChild>
|
||||||
<div className="mb-8 flex flex-wrap gap-1 justify-center">
|
<Button variant="outline" size="icon" className="shrink-0">
|
||||||
{[..."ABCDEFGHIJKLMNOPQRSTUVWXYZ"].map(letter => {
|
<Filter className="h-4 w-4" />
|
||||||
const hasAuthors = sortedGroupKeys.includes(letter);
|
</Button>
|
||||||
return (
|
</DrawerTrigger>
|
||||||
<a
|
<DrawerContent>
|
||||||
key={letter}
|
<div className="max-w-md mx-auto p-4">
|
||||||
href={hasAuthors ? `#${letter}` : undefined}
|
<DrawerHeader>
|
||||||
className={`w-8 h-8 flex items-center justify-center rounded-full text-sm font-medium transition-colors ${
|
<DrawerTitle>Filter Authors</DrawerTitle>
|
||||||
hasAuthors
|
<DrawerDescription>Narrow down the authors by specific criteria</DrawerDescription>
|
||||||
? 'bg-navy/10 hover:bg-navy/20 dark:bg-navy/20 dark:hover:bg-navy/30 text-navy dark:text-cream cursor-pointer'
|
</DrawerHeader>
|
||||||
: 'bg-navy/5 dark:bg-navy/10 text-navy/40 dark:text-cream/40 cursor-default'
|
|
||||||
}`}
|
<div className="space-y-6 py-4">
|
||||||
>
|
{/* Time period filter */}
|
||||||
{letter}
|
<div>
|
||||||
</a>
|
<h3 className="text-sm font-medium mb-2">Literary Periods</h3>
|
||||||
);
|
<div className="flex flex-wrap gap-2">
|
||||||
})}
|
{periodTags.map(tag => (
|
||||||
</div>
|
<Badge
|
||||||
|
key={tag.id}
|
||||||
{isLoading ? (
|
variant={selectedTimeperiod.includes(tag.name) ? "default" : "outline"}
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
className="cursor-pointer"
|
||||||
{Array.from({ length: 12 }).map((_, i) => (
|
onClick={() => handleTimeperiodChange(tag.name)}
|
||||||
<Card key={i} className="bg-cream dark:bg-dark-surface">
|
>
|
||||||
<CardContent className="p-6">
|
{tag.name}
|
||||||
<div className="flex items-center gap-4 mb-4">
|
</Badge>
|
||||||
<div className="w-16 h-16 rounded-full bg-navy/10 dark:bg-navy/20 animate-pulse"></div>
|
))}
|
||||||
<div className="flex-1">
|
</div>
|
||||||
<div className="h-5 bg-navy/10 dark:bg-navy/20 rounded-md w-3/4 mb-2 animate-pulse"></div>
|
</div>
|
||||||
<div className="h-4 bg-navy/10 dark:bg-navy/20 rounded-md w-1/2 animate-pulse"></div>
|
|
||||||
|
{/* Year range filter */}
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-medium mb-2">Year Range</h3>
|
||||||
|
<div className="px-1">
|
||||||
|
<Slider
|
||||||
|
defaultValue={yearRange}
|
||||||
|
max={2023}
|
||||||
|
min={1400}
|
||||||
|
step={10}
|
||||||
|
onValueChange={(value) => setYearRange(value as [number, number])}
|
||||||
|
/>
|
||||||
|
<div className="flex justify-between text-xs text-navy/70 dark:text-cream/70 mt-1">
|
||||||
|
<span>{yearRange[0]}</span>
|
||||||
|
<span>{yearRange[1]}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Countries filter */}
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-medium mb-2">Countries</h3>
|
||||||
|
<ScrollArea className="h-40 border rounded-md p-2">
|
||||||
|
<div className="space-y-1">
|
||||||
|
{countries.map(country => (
|
||||||
|
<div key={country} className="flex items-center space-x-2">
|
||||||
|
<Checkbox
|
||||||
|
id={`country-${country}`}
|
||||||
|
checked={selectedCountries.includes(country)}
|
||||||
|
onCheckedChange={() => handleCountryChange(country)}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor={`country-${country}`}
|
||||||
|
className="text-sm flex items-center gap-1.5 cursor-pointer"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={`https://flagcdn.com/w20/${country.toLowerCase()}.png`}
|
||||||
|
alt={country}
|
||||||
|
className="w-4 h-auto"
|
||||||
|
/>
|
||||||
|
{country}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Genres filter */}
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-medium mb-2">Primary Genres</h3>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{genreTags.map(tag => (
|
||||||
|
<Badge
|
||||||
|
key={tag.id}
|
||||||
|
variant={selectedGenres.includes(tag.name) ? "default" : "outline"}
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={() => handleGenreChange(tag.name)}
|
||||||
|
>
|
||||||
|
{tag.name}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-20 bg-navy/10 dark:bg-navy/20 rounded-md animate-pulse"></div>
|
|
||||||
</CardContent>
|
<DrawerFooter>
|
||||||
</Card>
|
<div className="flex items-center justify-between">
|
||||||
))}
|
<Button variant="outline" onClick={resetFilters}>
|
||||||
|
Reset All
|
||||||
|
</Button>
|
||||||
|
<DrawerClose asChild>
|
||||||
|
<Button>Apply Filters</Button>
|
||||||
|
</DrawerClose>
|
||||||
|
</div>
|
||||||
|
</DrawerFooter>
|
||||||
|
</div>
|
||||||
|
</DrawerContent>
|
||||||
|
</Drawer>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
</div>
|
||||||
<div className="space-y-8">
|
|
||||||
{sortedGroupKeys.map(letter => (
|
{/* Featured author section */}
|
||||||
<div key={letter} id={letter} className="scroll-mt-20">
|
{featuredAuthor && (
|
||||||
<h2 className="text-xl font-serif font-semibold mb-4 text-navy dark:text-cream">{letter}</h2>
|
<div className={`mb-8 rounded-xl p-5 md:p-6 overflow-hidden relative
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
${season === 'spring' ? 'bg-gradient-to-br from-green-50 to-blue-50 dark:from-green-950/30 dark:to-blue-950/30' :
|
||||||
{groupedAuthors![letter].map(author => (
|
season === 'summer' ? 'bg-gradient-to-br from-amber-50 to-orange-50 dark:from-amber-950/30 dark:to-orange-950/30' :
|
||||||
<Card key={author.id} className="bg-cream dark:bg-dark-surface hover:shadow-md transition-shadow">
|
season === 'autumn' ? 'bg-gradient-to-br from-russet/10 to-amber-50 dark:from-russet/20 dark:to-amber-950/30' :
|
||||||
<CardContent className="p-6">
|
'bg-gradient-to-br from-blue-50 to-indigo-50 dark:from-blue-950/30 dark:to-indigo-950/30'}`}
|
||||||
<AuthorChip author={author} size="lg" withLifeDates className="mb-4" />
|
>
|
||||||
<p className="text-navy/80 dark:text-cream/80 text-sm line-clamp-4 mb-4">
|
<div className="absolute inset-0 overflow-hidden opacity-10">
|
||||||
{author.biography?.slice(0, 150) || "No biography available."}{author.biography?.length > 150 ? "..." : ""}
|
<div className="absolute -right-10 -top-10 w-40 h-40 rounded-full bg-navy/10 dark:bg-cream/10"></div>
|
||||||
</p>
|
<div className="absolute right-20 top-1/2 w-20 h-20 rounded-full bg-navy/10 dark:bg-cream/10"></div>
|
||||||
<Link href={`/authors/${author.slug}`}>
|
<div className="absolute right-1/3 bottom-5 w-16 h-16 rounded-full bg-navy/10 dark:bg-cream/10"></div>
|
||||||
<Button
|
</div>
|
||||||
variant="outline"
|
|
||||||
className="w-full flex items-center justify-center gap-1 text-russet"
|
<div className="relative">
|
||||||
>
|
<div className="flex flex-col md:flex-row items-start gap-6">
|
||||||
View Profile <ArrowRight className="h-4 w-4" />
|
<div className="flex-shrink-0 w-24 h-24 md:w-32 md:h-32 rounded-lg overflow-hidden">
|
||||||
</Button>
|
<img
|
||||||
</Link>
|
src={featuredAuthor.portrait || "https://via.placeholder.com/300"}
|
||||||
</CardContent>
|
alt={featuredAuthor.name}
|
||||||
</Card>
|
className="w-full h-full object-cover"
|
||||||
))}
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<Badge variant="outline" className="bg-navy/5 dark:bg-cream/5 text-navy/70 dark:text-cream/70 border-none">
|
||||||
|
Featured Author
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 className="text-2xl md:text-3xl font-serif font-bold text-navy dark:text-cream mb-2">
|
||||||
|
{featuredAuthor.name}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="flex items-center text-sm text-navy/70 dark:text-cream/70 gap-x-4 mb-3">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Calendar className="h-4 w-4" />
|
||||||
|
<span>{featuredAuthor.birthYear}–{featuredAuthor.deathYear || 'present'}</span>
|
||||||
|
</div>
|
||||||
|
{featuredAuthor.country && (
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<MapPin className="h-4 w-4" />
|
||||||
|
<span>{featuredAuthor.country}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-navy/80 dark:text-cream/80 line-clamp-3 mb-4">
|
||||||
|
{featuredAuthor.biography}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<Link href={`/authors/${featuredAuthor.slug}`}>
|
||||||
|
<Button
|
||||||
|
className={`text-white
|
||||||
|
${season === 'spring' ? 'bg-green-500 hover:bg-green-600 dark:bg-green-700 dark:hover:bg-green-800' :
|
||||||
|
season === 'summer' ? 'bg-amber-500 hover:bg-amber-600 dark:bg-amber-700 dark:hover:bg-amber-800' :
|
||||||
|
season === 'autumn' ? 'bg-russet hover:bg-russet/90 dark:bg-russet/90 dark:hover:bg-russet' :
|
||||||
|
'bg-blue-500 hover:bg-blue-600 dark:bg-blue-700 dark:hover:bg-blue-800'}`}
|
||||||
|
>
|
||||||
|
Explore Works
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
<Button variant="outline">Follow</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* View controls bar */}
|
||||||
|
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 mb-6">
|
||||||
|
<Tabs defaultValue="all" className="w-full sm:w-auto">
|
||||||
|
<TabsList className="grid w-full sm:w-auto grid-cols-3">
|
||||||
|
<TabsTrigger value="all" className="text-sm">All Authors</TabsTrigger>
|
||||||
|
<TabsTrigger value="alphabetical" className="text-sm">Alphabetical</TabsTrigger>
|
||||||
|
<TabsTrigger value="chronological" className="text-sm">Chronological</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3 w-full sm:w-auto">
|
||||||
|
<div className="flex items-center gap-1 text-sm text-navy/70 dark:text-cream/70">
|
||||||
|
<Label htmlFor="sort-by" className="sr-only">Sort by</Label>
|
||||||
|
<Select
|
||||||
|
value={sortBy}
|
||||||
|
onValueChange={(value) => setSortBy(value as 'name' | 'birth' | 'popularity')}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="sort-by" className="w-[140px] h-9 text-xs">
|
||||||
|
<SelectValue placeholder="Sort by" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="name">Name</SelectItem>
|
||||||
|
<SelectItem value="birth">Birth Year</SelectItem>
|
||||||
|
<SelectItem value="popularity">Popularity</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-9 w-9"
|
||||||
|
onClick={toggleSortOrder}
|
||||||
|
>
|
||||||
|
{sortOrder === 'asc' ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator orientation="vertical" className="h-8 hidden sm:block" />
|
||||||
|
|
||||||
|
<div className="flex items-center">
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant={viewMode === 'list' ? 'default' : 'ghost'}
|
||||||
|
size="icon"
|
||||||
|
className="h-9 w-9"
|
||||||
|
onClick={() => setViewMode('list')}
|
||||||
|
>
|
||||||
|
<LayoutList className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>List view</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant={viewMode === 'grid' ? 'default' : 'ghost'}
|
||||||
|
size="icon"
|
||||||
|
className="h-9 w-9"
|
||||||
|
onClick={() => setViewMode('grid')}
|
||||||
|
>
|
||||||
|
<LayoutGrid className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Grid view</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Active filters */}
|
||||||
|
{(selectedTimeperiod.length > 0 || selectedCountries.length > 0 || selectedGenres.length > 0) && (
|
||||||
|
<div className="flex flex-wrap items-center gap-2 mb-6 p-3 bg-navy/5 dark:bg-navy/10 rounded-md">
|
||||||
|
<span className="text-sm font-medium text-navy/70 dark:text-cream/70">
|
||||||
|
Active filters:
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{selectedTimeperiod.map(period => (
|
||||||
|
<Badge
|
||||||
|
key={`period-${period}`}
|
||||||
|
variant="secondary"
|
||||||
|
className="flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<History className="h-3 w-3" />
|
||||||
|
{period}
|
||||||
|
<button
|
||||||
|
className="ml-1 hover:text-navy dark:hover:text-cream"
|
||||||
|
onClick={() => handleTimeperiodChange(period)}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{selectedCountries.map(country => (
|
||||||
|
<Badge
|
||||||
|
key={`country-${country}`}
|
||||||
|
variant="secondary"
|
||||||
|
className="flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<Globe className="h-3 w-3" />
|
||||||
|
{country}
|
||||||
|
<button
|
||||||
|
className="ml-1 hover:text-navy dark:hover:text-cream"
|
||||||
|
onClick={() => handleCountryChange(country)}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{selectedGenres.map(genre => (
|
||||||
|
<Badge
|
||||||
|
key={`genre-${genre}`}
|
||||||
|
variant="secondary"
|
||||||
|
className="flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<BookOpen className="h-3 w-3" />
|
||||||
|
{genre}
|
||||||
|
<button
|
||||||
|
className="ml-1 hover:text-navy dark:hover:text-cream"
|
||||||
|
onClick={() => handleGenreChange(genre)}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
size="sm"
|
||||||
|
className="text-russet hover:text-russet/90 h-6 p-0 ml-auto"
|
||||||
|
onClick={resetFilters}
|
||||||
|
>
|
||||||
|
Clear all
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Author grid or list */}
|
||||||
|
<TabsContent value="all" className="mt-0">
|
||||||
|
{isLoading ? (
|
||||||
|
<div className={`grid gap-6 ${viewMode === 'grid' ? 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4' : 'grid-cols-1'}`}>
|
||||||
|
{Array.from({ length: PAGE_SIZE }).map((_, i) => (
|
||||||
|
<Card key={i} className="bg-cream dark:bg-dark-surface">
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<div className="flex items-center gap-4 mb-4">
|
||||||
|
<div className="w-16 h-16 rounded-full bg-navy/10 dark:bg-navy/20 animate-pulse"></div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="h-5 bg-navy/10 dark:bg-navy/20 rounded-md w-3/4 mb-2 animate-pulse"></div>
|
||||||
|
<div className="h-4 bg-navy/10 dark:bg-navy/20 rounded-md w-1/2 animate-pulse"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="h-20 bg-navy/10 dark:bg-navy/20 rounded-md animate-pulse"></div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{viewMode === 'grid' ? (
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||||
|
{sortedAuthors.map(author => renderAuthorCard(author))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{sortedAuthors.map(author => renderAuthorListItem(author))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="alphabetical" className="mt-0">
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="animate-pulse space-y-6">
|
||||||
|
{Array.from({ length: 3 }).map((_, i) => (
|
||||||
|
<div key={i}>
|
||||||
|
<div className="h-6 bg-navy/10 dark:bg-navy/20 rounded-md w-12 mb-4"></div>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||||
|
{Array.from({ length: 4 }).map((_, j) => (
|
||||||
|
<div key={j} className="h-48 bg-navy/5 dark:bg-navy/10 rounded-md"></div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{/* Alphabet navigation */}
|
||||||
|
<div className="sticky top-16 z-10 bg-paper dark:bg-dark-paper py-3 mb-6 border-b border-sage/10 dark:border-sage/5">
|
||||||
|
<div className="flex flex-wrap gap-1 justify-center">
|
||||||
|
{[..."ABCDEFGHIJKLMNOPQRSTUVWXYZ"].map(letter => {
|
||||||
|
const hasAuthors = sortedGroupKeys.includes(letter);
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
key={letter}
|
||||||
|
href={hasAuthors ? `#${letter}` : undefined}
|
||||||
|
className={`w-8 h-8 flex items-center justify-center rounded-full text-sm font-medium transition-colors ${
|
||||||
|
hasAuthors
|
||||||
|
? 'bg-navy/10 hover:bg-navy/20 dark:bg-navy/20 dark:hover:bg-navy/30 text-navy dark:text-cream cursor-pointer'
|
||||||
|
: 'bg-navy/5 dark:bg-navy/10 text-navy/40 dark:text-cream/40 cursor-default'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{letter}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-10">
|
||||||
|
{sortedGroupKeys.map(letter => (
|
||||||
|
<div key={letter} id={letter} className="scroll-mt-32">
|
||||||
|
<h2 className="text-2xl font-serif font-semibold mb-5 text-navy dark:text-cream border-b border-sage/10 dark:border-sage/5 pb-2">
|
||||||
|
{letter}
|
||||||
|
</h2>
|
||||||
|
<div className={`grid gap-6 ${viewMode === 'grid' ? 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4' : 'grid-cols-1'}`}>
|
||||||
|
{groupedAuthors![letter].map(author => (
|
||||||
|
viewMode === 'grid' ? renderAuthorCard(author) : renderAuthorListItem(author)
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="chronological" className="mt-0">
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="animate-pulse space-y-6">
|
||||||
|
{Array.from({ length: 4 }).map((_, i) => (
|
||||||
|
<div key={i}>
|
||||||
|
<div className="h-6 bg-navy/10 dark:bg-navy/20 rounded-md w-24 mb-4"></div>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{Array.from({ length: 3 }).map((_, j) => (
|
||||||
|
<div key={j} className="h-48 bg-navy/5 dark:bg-navy/10 rounded-md"></div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-10">
|
||||||
|
{/* Group authors by century */}
|
||||||
|
{Array.from(new Set(authors?.map(a => Math.floor((a.birthYear || 1800) / 100) * 100))).sort().map(century => {
|
||||||
|
const centuryLabel = `${century}s`;
|
||||||
|
const centuryAuthors = authors?.filter(a =>
|
||||||
|
Math.floor((a.birthYear || 1800) / 100) * 100 === century
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={century}>
|
||||||
|
<h2 className="text-2xl font-serif font-semibold mb-5 text-navy dark:text-cream border-b border-sage/10 dark:border-sage/5 pb-2">
|
||||||
|
{centuryLabel}
|
||||||
|
</h2>
|
||||||
|
<div className={`grid gap-6 ${viewMode === 'grid' ? 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4' : 'grid-cols-1'}`}>
|
||||||
|
{centuryAuthors?.map(author => (
|
||||||
|
viewMode === 'grid' ? renderAuthorCard(author) : renderAuthorListItem(author)
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
{/* Pagination */}
|
{/* Pagination */}
|
||||||
{totalPages > 1 && (
|
{totalPages > 1 && (
|
||||||
<div className="mt-8 flex justify-center">
|
<div className="mt-8 flex justify-center">
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user