Files
coop/frontend/src/app/dashboard/components/SearchRequestModal.tsx
T

190 lines
6.8 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { Egg, Film, Loader2, PlusCircle, Search, Tv } from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { api } from "@/lib/api";
export function SearchRequestModal({
open,
onOpenChange,
costs,
}: {
open: boolean;
onOpenChange: (open: boolean) => void;
costs: { movie: number; tv: number; scarcityMultiplier?: number };
}) {
const [query, setQuery] = useState("");
const [results, setResults] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const getTVCost = (item: any) => {
const base = costs.tv;
const eps = item.numberOfEpisodes || 0;
if (eps <= 50) return base;
if (eps <= 100) return Math.round(base * 1.3);
if (eps <= 200) return Math.round(base * 1.6);
if (eps <= 400) return Math.round(base * 2.0);
return Math.round(base * 2.5);
};
const search = async () => {
if (!query) return;
setLoading(true);
try {
const res = await api.get(
`/overseer/search?query=${encodeURIComponent(query)}`,
);
setResults(res.data?.results || res.data || []);
} catch {
toast.error("Chickens lost the trail! Search failed.");
} finally {
setLoading(false);
}
};
const request = async (item: any, mediaType: string) => {
try {
const cost = mediaType === "movie" ? costs.movie : getTVCost(item);
await api.post("/overseer/request", {
mediaType,
mediaId: item.id || item.tmdbId,
title: item.title || item.name,
seasons: item.seasons,
numberOfEpisodes: item.numberOfEpisodes || 0,
});
toast.success(`Golden egg sent! ${cost} $COOP deducted.`);
onOpenChange(false);
} catch (e: any) {
toast.error(e?.response?.data?.error || "Request flew the coop!");
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl rounded-[2.5rem] border-4 border-foreground bg-background p-0 overflow-hidden shadow-[15px_15px_0px_0px_rgba(0,0,0,1)]">
<DialogHeader className="p-6 bg-primary border-b-4 border-foreground text-primary-foreground">
<div className="flex items-center justify-between">
<DialogTitle className="font-display text-3xl uppercase italic tracking-tighter flex items-center gap-2">
<Search className="h-6 w-6" /> PECK FOR FEED
</DialogTitle>
</div>
<DialogDescription className="text-primary-foreground/90 font-bold italic">
{costs.scarcityMultiplier && costs.scarcityMultiplier > 1
? `Market scarcity: x${costs.scarcityMultiplier}`
: ""}
</DialogDescription>
</DialogHeader>
<div className="p-6 space-y-6 font-sans">
<div className="flex gap-2">
<div className="relative flex-1">
<Input
value={query}
onChange={(e) => setQuery(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && search()}
placeholder="Type movie or show name..."
className="rounded-full border-4 border-foreground h-14 pl-6 pr-12 font-bold text-lg focus-visible:ring-primary shadow-sm"
/>
<Search className="absolute right-4 top-1/2 -translate-y-1/2 h-6 w-6 text-muted-foreground" />
</div>
<Button
onClick={search}
disabled={loading || !query}
className="rounded-full h-14 px-8 border-4 border-foreground shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] hover:shadow-none hover:translate-x-[2px] hover:translate-y-[2px] transition-all font-black text-lg"
>
{loading ? <Loader2 className="animate-spin" /> : "SEARCH"}
</Button>
</div>
<div className="max-h-[50vh] overflow-y-auto pr-2 space-y-4 scrollbar-hide">
{results.map((r) => {
const itemCost =
r.mediaType === "movie" ? costs.movie : getTVCost(r);
return (
<div
key={r.id || r.tmdbId}
className="group flex gap-4 border-4 border-foreground bg-card p-3 rounded-3xl hover:bg-accent/5 transition-all shadow-[4px_4px_0px_0px_rgba(0,0,0,0.1)] hover:shadow-[6px_6px_0px_0px_rgba(0,0,0,0.1)]"
>
<div className="relative h-28 w-20 flex-shrink-0 overflow-hidden rounded-xl border-2 border-foreground bg-muted shadow-sm">
{r.posterPath ? (
<img
src={`https://image.tmdb.org/t/p/w200${r.posterPath}`}
alt={r.title || r.name}
className="h-full w-full object-cover group-hover:scale-110 transition-transform duration-300"
/>
) : (
<div className="flex h-full w-full items-center justify-center bg-secondary/20">
<Film className="h-8 w-8 text-muted-foreground/30" />
</div>
)}
</div>
<div className="flex flex-1 flex-col justify-between py-1">
<div>
<div className="font-black text-xl leading-tight line-clamp-1">
{r.title || r.name}
</div>
<div className="flex items-center gap-2 mt-1 flex-wrap">
<span className="inline-flex items-center gap-1 rounded-full bg-secondary/30 px-2 py-0.5 text-[10px] font-black uppercase text-secondary-foreground border border-foreground/10">
{r.mediaType === "movie" ? (
<Film className="h-3 w-3" />
) : (
<Tv className="h-3 w-3" />
)}
{r.mediaType || "unknown"}
</span>
{r.releaseDate && (
<span className="text-[10px] font-bold text-muted-foreground uppercase tracking-widest">
{new Date(r.releaseDate).getFullYear()}
</span>
)}
{r.numberOfEpisodes > 0 && (
<span className="text-[10px] font-bold text-muted-foreground">
{r.numberOfEpisodes} eps
</span>
)}
</div>
</div>
<div className="flex items-center justify-between gap-4 mt-2">
<div className="text-xs font-bold text-muted-foreground italic flex items-center gap-1">
<Egg className="h-3 w-3" /> {itemCost} $COOP
</div>
<Button
size="sm"
onClick={() => request(r, r.mediaType || "movie")}
className="rounded-full border-2 border-foreground font-black text-xs hover:bg-primary hover:text-white transition-colors"
>
REQUEST <PlusCircle className="ml-1 h-3 w-3" />
</Button>
</div>
</div>
</div>
);
})}
{results.length === 0 && !loading && query && (
<div className="text-center py-10 opacity-40">
<div className="text-6xl mb-4">🏜</div>
<div className="font-black uppercase tracking-widest">
Nothing found in the yard.
</div>
</div>
)}
</div>
</div>
</DialogContent>
</Dialog>
);
}