refactor: replace remaining emojis with Lucide icons

Replace all raw emoji characters in dashboard cards, empty states,
and onboarding wizard with LucideIcon components for visual consistency.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-15 18:47:50 +01:00
parent 407fa45280
commit 7c3740fc72
7 changed files with 853 additions and 818 deletions

View File

@@ -2,315 +2,320 @@ import { useState } from "react";
import { useCreateCategory } from "../hooks/useCategories";
import { useCreateItem } from "../hooks/useItems";
import { useUpdateSetting } from "../hooks/useSettings";
import { LucideIcon } from "../lib/iconData";
import { IconPicker } from "./IconPicker";
interface OnboardingWizardProps {
onComplete: () => void;
onComplete: () => void;
}
export function OnboardingWizard({ onComplete }: OnboardingWizardProps) {
const [step, setStep] = useState(1);
const [step, setStep] = useState(1);
// Step 2 state
const [categoryName, setCategoryName] = useState("");
const [categoryIcon, setCategoryIcon] = useState("");
const [categoryError, setCategoryError] = useState("");
const [createdCategoryId, setCreatedCategoryId] = useState<number | null>(null);
// Step 2 state
const [categoryName, setCategoryName] = useState("");
const [categoryIcon, setCategoryIcon] = useState("");
const [categoryError, setCategoryError] = useState("");
const [createdCategoryId, setCreatedCategoryId] = useState<number | null>(
null,
);
// Step 3 state
const [itemName, setItemName] = useState("");
const [itemWeight, setItemWeight] = useState("");
const [itemPrice, setItemPrice] = useState("");
const [itemError, setItemError] = useState("");
// Step 3 state
const [itemName, setItemName] = useState("");
const [itemWeight, setItemWeight] = useState("");
const [itemPrice, setItemPrice] = useState("");
const [itemError, setItemError] = useState("");
const createCategory = useCreateCategory();
const createItem = useCreateItem();
const updateSetting = useUpdateSetting();
const createCategory = useCreateCategory();
const createItem = useCreateItem();
const updateSetting = useUpdateSetting();
function handleSkip() {
updateSetting.mutate(
{ key: "onboardingComplete", value: "true" },
{ onSuccess: onComplete },
);
}
function handleSkip() {
updateSetting.mutate(
{ key: "onboardingComplete", value: "true" },
{ onSuccess: onComplete },
);
}
function handleCreateCategory() {
const name = categoryName.trim();
if (!name) {
setCategoryError("Please enter a category name");
return;
}
setCategoryError("");
createCategory.mutate(
{ name, icon: categoryIcon.trim() || undefined },
{
onSuccess: (created) => {
setCreatedCategoryId(created.id);
setStep(3);
},
onError: (err) => {
setCategoryError(err.message || "Failed to create category");
},
},
);
}
function handleCreateCategory() {
const name = categoryName.trim();
if (!name) {
setCategoryError("Please enter a category name");
return;
}
setCategoryError("");
createCategory.mutate(
{ name, icon: categoryIcon.trim() || undefined },
{
onSuccess: (created) => {
setCreatedCategoryId(created.id);
setStep(3);
},
onError: (err) => {
setCategoryError(err.message || "Failed to create category");
},
},
);
}
function handleCreateItem() {
const name = itemName.trim();
if (!name) {
setItemError("Please enter an item name");
return;
}
if (!createdCategoryId) return;
function handleCreateItem() {
const name = itemName.trim();
if (!name) {
setItemError("Please enter an item name");
return;
}
if (!createdCategoryId) return;
setItemError("");
const payload: any = {
name,
categoryId: createdCategoryId,
};
if (itemWeight) payload.weightGrams = Number(itemWeight);
if (itemPrice) payload.priceCents = Math.round(Number(itemPrice) * 100);
setItemError("");
const payload: any = {
name,
categoryId: createdCategoryId,
};
if (itemWeight) payload.weightGrams = Number(itemWeight);
if (itemPrice) payload.priceCents = Math.round(Number(itemPrice) * 100);
createItem.mutate(payload, {
onSuccess: () => setStep(4),
onError: (err) => {
setItemError(err.message || "Failed to add item");
},
});
}
createItem.mutate(payload, {
onSuccess: () => setStep(4),
onError: (err) => {
setItemError(err.message || "Failed to add item");
},
});
}
function handleDone() {
updateSetting.mutate(
{ key: "onboardingComplete", value: "true" },
{ onSuccess: onComplete },
);
}
function handleDone() {
updateSetting.mutate(
{ key: "onboardingComplete", value: "true" },
{ onSuccess: onComplete },
);
}
return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
{/* Backdrop */}
<div className="absolute inset-0 bg-black/30 backdrop-blur-sm" />
return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
{/* Backdrop */}
<div className="absolute inset-0 bg-black/30 backdrop-blur-sm" />
{/* Card */}
<div className="relative z-10 w-full max-w-md mx-4 bg-white rounded-2xl shadow-2xl p-8">
{/* Step indicator */}
<div className="flex items-center justify-center gap-2 mb-6">
{[1, 2, 3].map((s) => (
<div
key={s}
className={`h-1.5 rounded-full transition-all ${
s <= Math.min(step, 3)
? "bg-blue-600 w-8"
: "bg-gray-200 w-6"
}`}
/>
))}
</div>
{/* Card */}
<div className="relative z-10 w-full max-w-md mx-4 bg-white rounded-2xl shadow-2xl p-8">
{/* Step indicator */}
<div className="flex items-center justify-center gap-2 mb-6">
{[1, 2, 3].map((s) => (
<div
key={s}
className={`h-1.5 rounded-full transition-all ${
s <= Math.min(step, 3) ? "bg-blue-600 w-8" : "bg-gray-200 w-6"
}`}
/>
))}
</div>
{/* Step 1: Welcome */}
{step === 1 && (
<div className="text-center">
<h2 className="text-2xl font-semibold text-gray-900 mb-2">
Welcome to GearBox!
</h2>
<p className="text-gray-500 mb-8 leading-relaxed">
Track your gear, compare weights, and plan smarter purchases.
Let&apos;s set up your first category and item.
</p>
<button
type="button"
onClick={() => setStep(2)}
className="w-full py-3 px-4 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors"
>
Get Started
</button>
<button
type="button"
onClick={handleSkip}
className="mt-3 text-sm text-gray-400 hover:text-gray-600 transition-colors"
>
Skip setup
</button>
</div>
)}
{/* Step 1: Welcome */}
{step === 1 && (
<div className="text-center">
<h2 className="text-2xl font-semibold text-gray-900 mb-2">
Welcome to GearBox!
</h2>
<p className="text-gray-500 mb-8 leading-relaxed">
Track your gear, compare weights, and plan smarter purchases.
Let&apos;s set up your first category and item.
</p>
<button
type="button"
onClick={() => setStep(2)}
className="w-full py-3 px-4 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors"
>
Get Started
</button>
<button
type="button"
onClick={handleSkip}
className="mt-3 text-sm text-gray-400 hover:text-gray-600 transition-colors"
>
Skip setup
</button>
</div>
)}
{/* Step 2: Create category */}
{step === 2 && (
<div>
<h2 className="text-xl font-semibold text-gray-900 mb-1">
Create a category
</h2>
<p className="text-sm text-gray-500 mb-6">
Categories help you organize your gear (e.g. Shelter, Cooking,
Clothing).
</p>
{/* Step 2: Create category */}
{step === 2 && (
<div>
<h2 className="text-xl font-semibold text-gray-900 mb-1">
Create a category
</h2>
<p className="text-sm text-gray-500 mb-6">
Categories help you organize your gear (e.g. Shelter, Cooking,
Clothing).
</p>
<div className="space-y-4">
<div>
<label
htmlFor="onboard-cat-name"
className="block text-sm font-medium text-gray-700 mb-1"
>
Category name *
</label>
<input
id="onboard-cat-name"
type="text"
value={categoryName}
onChange={(e) => setCategoryName(e.target.value)}
className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="e.g. Shelter"
autoFocus
/>
</div>
<div className="space-y-4">
<div>
<label
htmlFor="onboard-cat-name"
className="block text-sm font-medium text-gray-700 mb-1"
>
Category name *
</label>
<input
id="onboard-cat-name"
type="text"
value={categoryName}
onChange={(e) => setCategoryName(e.target.value)}
className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="e.g. Shelter"
autoFocus
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Icon (optional)
</label>
<IconPicker
value={categoryIcon}
onChange={setCategoryIcon}
size="md"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Icon (optional)
</label>
<IconPicker
value={categoryIcon}
onChange={setCategoryIcon}
size="md"
/>
</div>
{categoryError && (
<p className="text-xs text-red-500">{categoryError}</p>
)}
</div>
{categoryError && (
<p className="text-xs text-red-500">{categoryError}</p>
)}
</div>
<button
type="button"
onClick={handleCreateCategory}
disabled={createCategory.isPending}
className="mt-6 w-full py-3 px-4 bg-blue-600 hover:bg-blue-700 disabled:opacity-50 text-white font-medium rounded-lg transition-colors"
>
{createCategory.isPending ? "Creating..." : "Create Category"}
</button>
<button
type="button"
onClick={handleSkip}
className="mt-3 w-full text-sm text-gray-400 hover:text-gray-600 transition-colors"
>
Skip setup
</button>
</div>
)}
<button
type="button"
onClick={handleCreateCategory}
disabled={createCategory.isPending}
className="mt-6 w-full py-3 px-4 bg-blue-600 hover:bg-blue-700 disabled:opacity-50 text-white font-medium rounded-lg transition-colors"
>
{createCategory.isPending ? "Creating..." : "Create Category"}
</button>
<button
type="button"
onClick={handleSkip}
className="mt-3 w-full text-sm text-gray-400 hover:text-gray-600 transition-colors"
>
Skip setup
</button>
</div>
)}
{/* Step 3: Add item */}
{step === 3 && (
<div>
<h2 className="text-xl font-semibold text-gray-900 mb-1">
Add your first item
</h2>
<p className="text-sm text-gray-500 mb-6">
Add a piece of gear to your collection.
</p>
{/* Step 3: Add item */}
{step === 3 && (
<div>
<h2 className="text-xl font-semibold text-gray-900 mb-1">
Add your first item
</h2>
<p className="text-sm text-gray-500 mb-6">
Add a piece of gear to your collection.
</p>
<div className="space-y-4">
<div>
<label
htmlFor="onboard-item-name"
className="block text-sm font-medium text-gray-700 mb-1"
>
Item name *
</label>
<input
id="onboard-item-name"
type="text"
value={itemName}
onChange={(e) => setItemName(e.target.value)}
className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="e.g. Big Agnes Copper Spur"
autoFocus
/>
</div>
<div className="space-y-4">
<div>
<label
htmlFor="onboard-item-name"
className="block text-sm font-medium text-gray-700 mb-1"
>
Item name *
</label>
<input
id="onboard-item-name"
type="text"
value={itemName}
onChange={(e) => setItemName(e.target.value)}
className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="e.g. Big Agnes Copper Spur"
autoFocus
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label
htmlFor="onboard-item-weight"
className="block text-sm font-medium text-gray-700 mb-1"
>
Weight (g)
</label>
<input
id="onboard-item-weight"
type="number"
min="0"
step="any"
value={itemWeight}
onChange={(e) => setItemWeight(e.target.value)}
className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="e.g. 1200"
/>
</div>
<div>
<label
htmlFor="onboard-item-price"
className="block text-sm font-medium text-gray-700 mb-1"
>
Price ($)
</label>
<input
id="onboard-item-price"
type="number"
min="0"
step="0.01"
value={itemPrice}
onChange={(e) => setItemPrice(e.target.value)}
className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="e.g. 349.99"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label
htmlFor="onboard-item-weight"
className="block text-sm font-medium text-gray-700 mb-1"
>
Weight (g)
</label>
<input
id="onboard-item-weight"
type="number"
min="0"
step="any"
value={itemWeight}
onChange={(e) => setItemWeight(e.target.value)}
className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="e.g. 1200"
/>
</div>
<div>
<label
htmlFor="onboard-item-price"
className="block text-sm font-medium text-gray-700 mb-1"
>
Price ($)
</label>
<input
id="onboard-item-price"
type="number"
min="0"
step="0.01"
value={itemPrice}
onChange={(e) => setItemPrice(e.target.value)}
className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="e.g. 349.99"
/>
</div>
</div>
{itemError && (
<p className="text-xs text-red-500">{itemError}</p>
)}
</div>
{itemError && <p className="text-xs text-red-500">{itemError}</p>}
</div>
<button
type="button"
onClick={handleCreateItem}
disabled={createItem.isPending}
className="mt-6 w-full py-3 px-4 bg-blue-600 hover:bg-blue-700 disabled:opacity-50 text-white font-medium rounded-lg transition-colors"
>
{createItem.isPending ? "Adding..." : "Add Item"}
</button>
<button
type="button"
onClick={handleSkip}
className="mt-3 w-full text-sm text-gray-400 hover:text-gray-600 transition-colors"
>
Skip setup
</button>
</div>
)}
<button
type="button"
onClick={handleCreateItem}
disabled={createItem.isPending}
className="mt-6 w-full py-3 px-4 bg-blue-600 hover:bg-blue-700 disabled:opacity-50 text-white font-medium rounded-lg transition-colors"
>
{createItem.isPending ? "Adding..." : "Add Item"}
</button>
<button
type="button"
onClick={handleSkip}
className="mt-3 w-full text-sm text-gray-400 hover:text-gray-600 transition-colors"
>
Skip setup
</button>
</div>
)}
{/* Step 4: Done */}
{step === 4 && (
<div className="text-center">
<div className="text-4xl mb-4">&#127881;</div>
<h2 className="text-xl font-semibold text-gray-900 mb-2">
You&apos;re all set!
</h2>
<p className="text-sm text-gray-500 mb-8">
Your first item has been added. You can now browse your collection,
add more gear, and track your setup.
</p>
<button
type="button"
onClick={handleDone}
disabled={updateSetting.isPending}
className="w-full py-3 px-4 bg-blue-600 hover:bg-blue-700 disabled:opacity-50 text-white font-medium rounded-lg transition-colors"
>
{updateSetting.isPending ? "Finishing..." : "Done"}
</button>
</div>
)}
</div>
</div>
);
{/* Step 4: Done */}
{step === 4 && (
<div className="text-center">
<div className="mb-4">
<LucideIcon
name="party-popper"
size={48}
className="text-gray-400 mx-auto"
/>
</div>
<h2 className="text-xl font-semibold text-gray-900 mb-2">
You&apos;re all set!
</h2>
<p className="text-sm text-gray-500 mb-8">
Your first item has been added. You can now browse your
collection, add more gear, and track your setup.
</p>
<button
type="button"
onClick={handleDone}
disabled={updateSetting.isPending}
className="w-full py-3 px-4 bg-blue-600 hover:bg-blue-700 disabled:opacity-50 text-white font-medium rounded-lg transition-colors"
>
{updateSetting.isPending ? "Finishing..." : "Done"}
</button>
</div>
)}
</div>
</div>
);
}