Files
GearBox/src/client/components/onboarding/OnboardingReview.tsx
Jean-Luc Makiola 5c18a3cd6c feat(30-02): build full-screen catalog-driven onboarding flow UI
Implements 5-step onboarding: Welcome, Hobby Picker, Item Browser,
Review, and Done. Includes hobby card selection, popular item grid
with check/uncheck, review list with remove, CSS step transitions,
and responsive grid layout.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 20:46:55 +02:00

129 lines
3.5 KiB
TypeScript

import { LucideIcon } from "../../lib/iconData";
interface ReviewItem {
id: number;
brand: string | null;
model: string;
imageUrl: string | null;
category: string | null;
}
interface OnboardingReviewProps {
items: ReviewItem[];
onRemoveItem: (itemId: number) => void;
onConfirm: () => void;
onSkip: () => void;
isSubmitting: boolean;
}
export function OnboardingReview({
items,
onRemoveItem,
onConfirm,
onSkip,
isSubmitting,
}: OnboardingReviewProps) {
// Group by category
const grouped = new Map<string, ReviewItem[]>();
for (const item of items) {
const cat = item.category || "Uncategorized";
if (!grouped.has(cat)) grouped.set(cat, []);
grouped.get(cat)!.push(item);
}
return (
<div className="flex flex-col items-center justify-center min-h-screen px-8">
<div className="max-w-2xl w-full text-center">
<h1 className="text-3xl font-bold text-gray-900 mb-2">
Your starting collection
</h1>
<p className="text-base text-gray-500 mb-8">
{items.length > 0
? `${items.length} ${items.length === 1 ? "item" : "items"} ready to add`
: "No items selected — you can always add gear later from the catalog."}
</p>
{items.length > 0 && (
<div className="text-left mb-8">
{[...grouped.entries()].map(([category, catItems]) => (
<div key={category} className="mb-4">
<div className="text-xs font-medium text-gray-400 uppercase tracking-wide mb-2">
{category}
</div>
{catItems.map((item) => (
<div
key={item.id}
className="flex items-center gap-3 py-2 border-b border-gray-50"
>
<div className="w-10 h-10 rounded-lg overflow-hidden bg-gray-50 shrink-0">
{item.imageUrl ? (
<img
src={item.imageUrl}
alt={item.model}
className="w-full h-full object-cover"
/>
) : (
<div className="w-full h-full flex items-center justify-center">
<LucideIcon
name="package"
size={16}
className="text-gray-300"
/>
</div>
)}
</div>
<div className="flex-1 min-w-0">
<div className="text-sm text-gray-900 truncate">
{item.brand
? `${item.brand} ${item.model}`
: item.model}
</div>
</div>
<button
type="button"
onClick={() => onRemoveItem(item.id)}
className="text-gray-300 hover:text-red-500 transition-colors shrink-0"
>
<LucideIcon name="x" size={16} />
</button>
</div>
))}
</div>
))}
</div>
)}
<div className="flex flex-col items-center gap-3">
{items.length > 0 ? (
<button
type="button"
onClick={onConfirm}
disabled={isSubmitting}
className="px-8 py-3 bg-gray-700 hover:bg-gray-800 disabled:opacity-50 text-white font-medium rounded-lg transition-colors"
>
{isSubmitting ? "Adding..." : "Add to my collection"}
</button>
) : (
<button
type="button"
onClick={onSkip}
className="px-8 py-3 bg-gray-700 hover:bg-gray-800 text-white font-medium rounded-lg transition-colors"
>
Continue
</button>
)}
{items.length > 0 && (
<button
type="button"
onClick={onSkip}
className="text-sm text-gray-400 hover:text-gray-600 transition-colors"
>
Skip this step
</button>
)}
</div>
</div>
</div>
);
}