tia/src/app/(app)/wardrobe/saved-outfits/page.tsx
Mannu 2a09c027fa feat(marketing): public homepage replacing / → /login redirect
- 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>
2026-05-25 23:26:26 +05:30

146 lines
5.7 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.

"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>
);
}