Files
memefast/resources/js/pages/memes/index.tsx
2025-07-16 23:33:43 +08:00

210 lines
9.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Badge } from '@/components/ui/badge';
import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator } from '@/components/ui/breadcrumb';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { KeywordBadge } from '@/components/ui/keyword-badge';
import { MemeCard } from '@/components/custom/meme-card';
import Footer from '@/pages/home/partials/Footer';
import { Link, router } from '@inertiajs/react';
import { Search } from 'lucide-react';
import { useState } from 'react';
import { route } from 'ziggy-js';
import BrandLogo from '../home/partials/BrandLogo';
interface MemeMedia {
ids: string;
name: string;
description: string;
keywords: string[];
action_keywords: string[];
emotion_keywords: string[];
misc_keywords: string[];
mov_url: string;
webm_url: string;
gif_url: string;
webp_url: string;
slug: string;
}
interface CursorPaginatedMemes {
data: MemeMedia[];
next_cursor: string | null;
prev_cursor: string | null;
next_page_url: string | null;
prev_page_url: string | null;
per_page: number;
path: string;
}
interface Props {
memes: CursorPaginatedMemes;
types: string[];
popularKeywords: string[];
filters: {
search?: string;
};
}
export default function MemesIndex({ memes, popularKeywords, filters }: Props) {
const [search, setSearch] = useState(filters.search || '');
const handleSearch = (e: React.FormEvent) => {
e.preventDefault();
navigateToSearch(search);
};
const handleKeywordClick = (keyword: string) => {
setSearch(keyword);
navigateToSearch(keyword);
};
const navigateToSearch = (searchTerm: string) => {
if (!searchTerm.trim()) {
router.get(route('memes.index'));
return;
}
const trimmedSearch = searchTerm.trim();
// Convert spaces to + for URL segment
const urlSegment = trimmedSearch.replace(/\s+/g, '+');
router.get(route('memes.search', urlSegment));
};
return (
<>
<div className="min-h-screen bg-neutral-50 pb-10 dark:bg-black">
<div className="container mx-auto px-4 pt-8 pb-0">
<BrandLogo className="py-3"></BrandLogo>
{/* Breadcrumbs */}
<Breadcrumb className="mb-6">
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link href={route('home')}>Home</Link>
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>
{filters.search ? `Search: ${filters.search}` : 'Meme Library'}
</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
{/* Header */}
<div className="mb-8 text-center">
<h1 className="text-foreground mb-4 text-4xl font-bold">Meme Library</h1>
<p className="text-muted-foreground mx-auto max-w-2xl text-xl">
Thousands of memes ready for TikTok, Reels, Threads and YouTube Shorts. No signup needed - click any meme to start
creating!
</p>
</div>
{/* Search and Filters */}
<Card className="mb-8">
<CardContent className="p-6">
<form onSubmit={handleSearch} className="mb-4 flex flex-col gap-4 md:flex-row">
<div className="flex-1">
<div className="relative">
<Search className="text-muted-foreground absolute top-4 left-4 h-5 w-5" />
<Input
type="text"
placeholder="Search memes..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="h-12 pl-12 text-lg"
/>
</div>
</div>
<Button
type="submit"
size="lg"
className="h-12 bg-gradient-to-r from-purple-600 to-pink-600 text-lg text-white hover:from-purple-700 hover:to-pink-700"
>
Search
</Button>
</form>
{/* Popular Keywords */}
<div className="mt-4">
<h3 className="text-foreground mb-3 text-sm font-medium">Popular Keywords</h3>
<div className="flex flex-wrap gap-2">
{popularKeywords.map((keyword) => (
<KeywordBadge key={keyword} keyword={keyword} size="lg" />
))}
</div>
</div>
{/* Active Search */}
{filters.search && (
<div className="mt-4 flex flex-wrap gap-2">
<Badge variant="secondary" className="flex items-center gap-1">
Search: {filters.search}
<button
onClick={() => {
setSearch('');
router.get(route('memes.index'));
}}
className="ml-1 hover:text-red-500"
>
×
</button>
</Badge>
</div>
)}
</CardContent>
</Card>
{/* Memes Grid */}
<div className="xs:grid-cols-2 mb-8 grid grid-cols-1 gap-6 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6">
{memes.data.map((meme) => (
<MemeCard key={meme.ids} meme={meme} />
))}
</div>
{/* Cursor Pagination */}
{(memes.next_page_url || memes.prev_page_url) && (
<div className="flex flex-col items-center gap-4">
<div className="flex items-center justify-center gap-4">
<div>
{memes.prev_page_url && (
<Link
href={memes.prev_page_url}
className="text-muted-foreground border-input bg-background hover:bg-accent hover:text-accent-foreground inline-flex w-32 items-center justify-center gap-2 rounded-md border px-4 py-2 text-sm font-medium transition-colors"
>
Previous
</Link>
)}
</div>
<div>
{memes.next_page_url && (
<Link
href={memes.next_page_url}
className="text-muted-foreground border-input bg-background hover:bg-accent hover:text-accent-foreground inline-flex w-32 items-center justify-center gap-2 rounded-md border px-4 py-2 text-sm font-medium transition-colors"
>
Next
</Link>
)}
</div>
</div>
{memes.prev_page_url && (
<Link
href={route('memes.index', { ...(filters.search && { search: filters.search }) })}
className="text-muted-foreground hover:text-foreground text-sm transition-colors"
>
Back to first page
</Link>
)}
</div>
)}
</div>
<Footer />
</div>
</>
);
}