- Add (marketing) route group: /, /pricing, /privacy, /terms - Add (app) route group: moves all authenticated pages, app home → /home - Root / is now a static marketing page (zero DB imports, zero auth) - NavAuthButton client component: shows "Open Tia →" if logged in, else "Continue with Google" - Plausible analytics hook in marketing layout - Auto-generated OG image via opengraph-image.tsx - Middleware updated to allowlist marketing routes - All /-redirects updated to /home (login, onboarding, invite, circle join) - BottomNav home tab updated: / → /home Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
146 lines
5.7 KiB
TypeScript
146 lines
5.7 KiB
TypeScript
"use client";
|
||
|
||
import { useState, useEffect } from "react";
|
||
import { useRouter } from "next/navigation";
|
||
import Link from "next/link";
|
||
import { useFamily } from "@/app/FamilyProvider";
|
||
|
||
interface SavedOutfit {
|
||
id: string;
|
||
name: string;
|
||
garment_ids: string[];
|
||
occasion_tags: string[];
|
||
created_at: string;
|
||
}
|
||
|
||
interface GarmentThumb {
|
||
id: string;
|
||
thumbUrl: string;
|
||
name: string | null;
|
||
category: string;
|
||
}
|
||
|
||
export default function SavedOutfitsPage() {
|
||
const { childId } = useFamily();
|
||
const router = useRouter();
|
||
|
||
const [outfits, setOutfits] = useState<SavedOutfit[]>([]);
|
||
const [thumbs, setThumbs] = useState<Record<string, GarmentThumb>>({});
|
||
const [loading, setLoading] = useState(true);
|
||
|
||
useEffect(() => {
|
||
if (!childId) return;
|
||
(async () => {
|
||
try {
|
||
const res = await fetch(`/api/garments/outfits?childId=${childId}`);
|
||
const data = await res.json();
|
||
setOutfits(data.items || []);
|
||
|
||
// Fetch thumbnails for garment IDs referenced by outfits
|
||
const allIds = [...new Set((data.items || []).flatMap((o: SavedOutfit) => o.garment_ids))] as string[];
|
||
if (allIds.length > 0) {
|
||
const garmentRes = await fetch(`/api/garments?childId=${childId}&status=active`);
|
||
const garmentData = await garmentRes.json();
|
||
const map: Record<string, GarmentThumb> = {};
|
||
for (const g of garmentData.items || []) {
|
||
map[g.id] = { id: g.id, thumbUrl: g.thumbUrl, name: g.name, category: g.category };
|
||
}
|
||
setThumbs(map);
|
||
}
|
||
} catch {}
|
||
setLoading(false);
|
||
})();
|
||
}, [childId]);
|
||
|
||
const deleteOutfit = async (id: string) => {
|
||
try {
|
||
const res = await fetch(`/api/garments/outfits/${id}`, { method: "DELETE" });
|
||
if (res.ok) setOutfits(prev => prev.filter(o => o.id !== id));
|
||
} catch {}
|
||
};
|
||
|
||
return (
|
||
<div className="min-h-screen bg-gradient-to-br from-indigo-50 via-purple-50 to-pink-50 dark:from-gray-900 dark:to-gray-800 pb-24">
|
||
<div className="flex items-center gap-3 p-4">
|
||
<button onClick={() => router.back()} className="p-2 rounded-xl bg-white dark:bg-gray-800 shadow-sm text-xl">←</button>
|
||
<h1 className="text-xl font-bold">💾 Saved Outfits</h1>
|
||
<Link href="/wardrobe/outfit" className="ml-auto text-sm px-3 py-1.5 bg-rose-400 text-white rounded-xl font-medium shadow-sm">
|
||
+ New
|
||
</Link>
|
||
</div>
|
||
|
||
{loading ? (
|
||
<div className="flex justify-center py-16">
|
||
<div className="flex gap-1">
|
||
{[0, 150, 300].map(d => (
|
||
<span key={d} className="w-2.5 h-2.5 bg-rose-400 rounded-full animate-bounce" style={{ animationDelay: `${d}ms` }} />
|
||
))}
|
||
</div>
|
||
</div>
|
||
) : outfits.length === 0 ? (
|
||
<div className="flex flex-col items-center justify-center py-20 px-8 text-center">
|
||
<span className="text-5xl mb-4">👗</span>
|
||
<p className="font-semibold text-gray-600 dark:text-gray-300">No saved outfits yet</p>
|
||
<p className="text-sm text-gray-400 mt-1">Save combinations from the outfit suggestion screen</p>
|
||
<Link href="/wardrobe/outfit" className="mt-4 px-5 py-2 bg-rose-400 text-white rounded-xl text-sm font-medium">
|
||
Get suggestions
|
||
</Link>
|
||
</div>
|
||
) : (
|
||
<div className="mx-4 space-y-3">
|
||
{outfits.map(outfit => (
|
||
<div key={outfit.id} className="bg-white dark:bg-gray-800 rounded-2xl shadow-sm p-4">
|
||
<div className="flex items-start justify-between mb-3">
|
||
<div>
|
||
<p className="font-semibold text-gray-700 dark:text-gray-200">{outfit.name}</p>
|
||
{outfit.occasion_tags?.length > 0 && (
|
||
<div className="flex gap-1 mt-1">
|
||
{outfit.occasion_tags.map(t => (
|
||
<span key={t} className="text-xs px-2 py-0.5 bg-purple-100 text-purple-700 dark:bg-purple-900/20 dark:text-purple-300 rounded-full">
|
||
{t}
|
||
</span>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
<button
|
||
onClick={() => deleteOutfit(outfit.id)}
|
||
className="text-gray-300 hover:text-red-400 text-lg p-1 transition-colors"
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
|
||
<div className="flex gap-2">
|
||
{outfit.garment_ids.slice(0, 4).map(gid => {
|
||
const g = thumbs[gid];
|
||
if (!g) return (
|
||
<div key={gid} className="w-16 h-16 rounded-xl bg-gray-100 dark:bg-gray-700 flex items-center justify-center text-gray-300 text-xl">
|
||
👗
|
||
</div>
|
||
);
|
||
return (
|
||
<div key={gid} className="flex flex-col items-center gap-1">
|
||
<div className="w-16 h-16 rounded-xl overflow-hidden">
|
||
<img src={g.thumbUrl} alt={g.name || g.category} className="w-full h-full object-cover" loading="lazy" />
|
||
</div>
|
||
</div>
|
||
);
|
||
})}
|
||
{outfit.garment_ids.length > 4 && (
|
||
<div className="w-16 h-16 rounded-xl bg-gray-100 dark:bg-gray-700 flex items-center justify-center text-sm text-gray-500">
|
||
+{outfit.garment_ids.length - 4}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<p className="text-xs text-gray-400 mt-2">
|
||
{new Date(outfit.created_at).toLocaleDateString("en-IN", { day: "numeric", month: "short", year: "numeric" })}
|
||
</p>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|