- Add useTranslation() to TopNav, BottomTabBar, FabMenu, UserMenu - Internationalize ConfirmDialog, AuthPromptModal, ExternalLinkDialog - Extract all onboarding flow strings (Welcome, HobbyPicker, ItemBrowser, Review, Done) - Internationalize settings page (weight unit, currency, API keys, import/export) - Internationalize login page and root error boundary - All dialogs in __root.tsx use t() for UI chrome Phase 34, Plan 02 (core navigation and global UI)
131 lines
3.6 KiB
TypeScript
131 lines
3.6 KiB
TypeScript
import { useTranslation } from "react-i18next";
|
|
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) {
|
|
const { t } = useTranslation("onboarding");
|
|
// 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">
|
|
{t("review.title")}
|
|
</h1>
|
|
<p className="text-base text-gray-500 mb-8">
|
|
{items.length > 0
|
|
? t("review.itemsReady", { count: items.length })
|
|
: t("review.noItemsSelected")}
|
|
</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 ? t("review.adding") : t("review.addToCollection")}
|
|
</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"
|
|
>
|
|
{t("common:actions.continue")}
|
|
</button>
|
|
)}
|
|
{items.length > 0 && (
|
|
<button
|
|
type="button"
|
|
onClick={onSkip}
|
|
className="text-sm text-gray-400 hover:text-gray-600 transition-colors"
|
|
>
|
|
{t("common:actions.skipStep")}
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|