luxe-frontend/src/pages/Companions.tsx

171 lines
8.6 KiB
TypeScript

import { useState, useRef } from "react";
import { motion, useInView } from "framer-motion";
import { Search, Star, Heart } from "lucide-react";
import { COMPANIONS, CITIES, INTERESTS } from "@/lib/mock-data";
import { Link } from "wouter";
import InquiryModal from "@/components/InquiryModal";
function FadeIn({ children, delay = 0, className = "" }: { children: React.ReactNode; delay?: number; className?: string }) {
const ref = useRef(null);
const inView = useInView(ref, { once: true, margin: "-40px" });
return (
<motion.div
ref={ref}
initial={{ opacity: 0, y: 24 }}
animate={inView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.65, delay, ease: [0.22, 1, 0.36, 1] }}
className={className}
>
{children}
</motion.div>
);
}
export default function Companions() {
const [cityFilter, setCityFilter] = useState("All");
const [interestFilter, setInterestFilter] = useState("All");
const [search, setSearch] = useState("");
const [favorites, setFavorites] = useState<string[]>([]);
const [modalOpen, setModalOpen] = useState(false);
const [selectedName, setSelectedName] = useState("");
const filtered = COMPANIONS.filter(c => {
const cityOk = cityFilter === "All" || c.city === cityFilter;
const intOk = interestFilter === "All" || c.interests.includes(interestFilter);
const searchOk = !search || c.name.toLowerCase().includes(search.toLowerCase()) || c.interests.some(i => i.toLowerCase().includes(search.toLowerCase()));
return cityOk && intOk && searchOk;
});
const toggleFav = (id: string) => setFavorites(v => v.includes(id) ? v.filter(x => x !== id) : [...v, id]);
const gradients = [
"from-amber-900/60 to-stone-900",
"from-rose-900/40 to-stone-900",
"from-emerald-900/40 to-stone-900",
"from-indigo-900/40 to-stone-900",
"from-amber-800/50 to-stone-900",
"from-purple-900/40 to-stone-900",
"from-teal-900/40 to-stone-900",
"from-orange-900/40 to-stone-900",
];
return (
<div className="pt-20 min-h-screen">
<InquiryModal open={modalOpen} onClose={() => setModalOpen(false)} companionName={selectedName} />
{/* Header */}
<div className="py-20 text-center border-b border-border bg-card">
<FadeIn>
<span className="text-xs tracking-[0.35em] uppercase text-primary mb-3 block">Our Network</span>
<h1 className="font-serif text-5xl sm:text-6xl text-foreground mb-4">Our Companions</h1>
<p className="text-muted-foreground text-base max-w-xl mx-auto">
Each companion is personally selected for their poise, intellect, and social grace. Find the perfect match for your occasion.
</p>
</FadeIn>
</div>
{/* Filters */}
<div className="sticky top-20 z-30 bg-background/95 border-b border-border py-4 px-6 backdrop-blur-sm">
<div className="max-w-7xl mx-auto flex flex-col sm:flex-row gap-4 items-start sm:items-center">
<div className="relative flex-1 max-w-xs">
<Search size={14} className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" />
<input
value={search}
onChange={e => setSearch(e.target.value)}
placeholder="Search companions..."
className="w-full pl-9 pr-4 py-2.5 bg-card border border-border text-sm text-foreground focus:outline-none focus:border-primary/40 placeholder:text-muted-foreground/50 transition-colors"
/>
</div>
<div className="flex flex-wrap gap-2">
<select
value={cityFilter}
onChange={e => setCityFilter(e.target.value)}
className="bg-card border border-border text-sm text-muted-foreground px-3 py-2.5 focus:outline-none focus:border-primary/40 [color-scheme:dark]"
>
<option value="All">All Cities</option>
{CITIES.map(c => <option key={c} value={c}>{c}</option>)}
</select>
<select
value={interestFilter}
onChange={e => setInterestFilter(e.target.value)}
className="bg-card border border-border text-sm text-muted-foreground px-3 py-2.5 focus:outline-none focus:border-primary/40 [color-scheme:dark]"
>
<option value="All">All Interests</option>
{INTERESTS.map(i => <option key={i} value={i}>{i}</option>)}
</select>
</div>
<p className="text-xs text-muted-foreground ml-auto">{filtered.length} companions</p>
</div>
</div>
{/* Grid */}
<div className="max-w-7xl mx-auto px-6 py-16">
{filtered.length === 0 ? (
<div className="py-32 text-center">
<p className="font-serif text-2xl text-foreground mb-2">No companions found</p>
<p className="text-sm text-muted-foreground">Try adjusting your filters</p>
</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{filtered.map((c, i) => {
const grad = gradients[parseInt(c.id) % gradients.length];
const isFav = favorites.includes(c.id);
return (
<FadeIn key={c.id} delay={i * 0.05}>
<motion.div whileHover={{ y: -4 }} transition={{ duration: 0.3 }} className="bg-card border border-border overflow-hidden group">
<div className={`relative h-60 bg-gradient-to-b ${grad} flex items-center justify-center`}>
<div className="w-20 h-20 rounded-full bg-primary/10 border border-primary/20 flex items-center justify-center">
<span className="font-serif text-2xl text-primary/60">{c.name.slice(0, 2).toUpperCase()}</span>
</div>
<button
onClick={() => toggleFav(c.id)}
className="absolute top-3 right-3 p-1.5"
>
<Heart size={15} fill={isFav ? "hsl(var(--primary))" : "none"} className="text-primary" />
</button>
<div className="absolute bottom-3 left-3 flex items-center gap-1.5">
<span className="w-1.5 h-1.5 rounded-full" style={{ backgroundColor: c.availability === "Online" ? "#4ade80" : "#6b7280" }} />
<span className="text-xs text-white/60">{c.availability}</span>
</div>
</div>
<div className="p-5">
<div className="flex items-center justify-between mb-1">
<h3 className="font-serif text-xl">{c.name}</h3>
<div className="flex items-center gap-1">
<Star size={11} fill="hsl(var(--primary))" className="text-primary" />
<span className="text-xs text-muted-foreground">{c.rating}</span>
</div>
</div>
<p className="text-xs tracking-wider text-muted-foreground uppercase mb-2">{c.city} · {c.age} yrs</p>
<p className="text-sm text-muted-foreground mb-3 line-clamp-2">{c.snippet}</p>
<div className="flex flex-wrap gap-1 mb-4">
{c.interests.slice(0, 2).map(i => (
<span key={i} className="text-[9px] tracking-widest uppercase px-2 py-0.5 border border-primary/20 text-primary/60">{i}</span>
))}
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-primary">{c.pricing}</span>
<div className="flex gap-2">
<Link href={`/companions/${c.id}`}>
<span className="text-[10px] tracking-wider uppercase px-3 py-1.5 border border-border text-muted-foreground hover:text-foreground transition-colors cursor-pointer">View</span>
</Link>
<button
onClick={() => { setSelectedName(c.name); setModalOpen(true); }}
className="text-[10px] tracking-wider uppercase px-3 py-1.5 border border-primary/40 text-primary hover:bg-primary hover:text-background transition-all duration-300"
>
Inquire
</button>
</div>
</div>
</div>
</motion.div>
</FadeIn>
);
})}
</div>
)}
</div>
</div>
);
}