diff --git a/src/components/setup/AllocationBar.tsx b/src/components/setup/AllocationBar.tsx
new file mode 100644
index 0000000..cb6b515
--- /dev/null
+++ b/src/components/setup/AllocationBar.tsx
@@ -0,0 +1,31 @@
+import { useTranslation } from "react-i18next"
+
+interface AllocationBarProps {
+ remaining: number
+ currency: string
+}
+
+export function AllocationBar({ remaining, currency }: AllocationBarProps) {
+ const { t } = useTranslation()
+
+ const formatted = new Intl.NumberFormat(undefined, {
+ style: "currency",
+ currency,
+ minimumFractionDigits: 0,
+ maximumFractionDigits: 0,
+ }).format(remaining)
+
+ return (
+
+ {t("setup.step2.remaining")}
+ = 0 ? "text-on-budget" : "text-destructive"
+ }`}
+ >
+ {formatted}
+
+
+ )
+}
diff --git a/src/components/setup/CategoryGroupHeader.tsx b/src/components/setup/CategoryGroupHeader.tsx
new file mode 100644
index 0000000..18a80d8
--- /dev/null
+++ b/src/components/setup/CategoryGroupHeader.tsx
@@ -0,0 +1,36 @@
+import type { CategoryType } from "@/lib/types"
+import { Separator } from "@/components/ui/separator"
+
+interface CategoryGroupHeaderProps {
+ type: CategoryType
+ label: string
+ count: number
+}
+
+const colorVar = (type: CategoryType): string => {
+ const map: Record = {
+ income: "var(--color-income)",
+ bill: "var(--color-bill)",
+ variable_expense: "var(--color-variable-expense)",
+ debt: "var(--color-debt)",
+ saving: "var(--color-saving)",
+ investment: "var(--color-investment)",
+ }
+ return map[type]
+}
+
+export function CategoryGroupHeader({ type, label, count }: CategoryGroupHeaderProps) {
+ return (
+
+
+
+ {label}
+ ({count} items)
+
+
+
+ )
+}
diff --git a/src/components/setup/IncomeStep.tsx b/src/components/setup/IncomeStep.tsx
new file mode 100644
index 0000000..ab8fb34
--- /dev/null
+++ b/src/components/setup/IncomeStep.tsx
@@ -0,0 +1,45 @@
+import { useTranslation } from "react-i18next"
+import { Input } from "@/components/ui/input"
+import { Label } from "@/components/ui/label"
+
+interface IncomeStepProps {
+ income: number
+ onIncomeChange: (val: number) => void
+ currency: string
+ error: string | null
+}
+
+export function IncomeStep({ income, onIncomeChange, currency, error }: IncomeStepProps) {
+ const { t } = useTranslation()
+
+ const helperId = "income-helper"
+ const errorId = "income-error"
+
+ return (
+
+
+
+ onIncomeChange(Number(e.target.value))}
+ min={0}
+ aria-describedby={`${helperId}${error ? ` ${errorId}` : ""}`}
+ />
+ {currency}
+
+
+ {t("setup.step1.helper")}
+
+ {error && (
+
+ {error}
+
+ )}
+
+ )
+}
diff --git a/src/components/setup/PresetItemRow.tsx b/src/components/setup/PresetItemRow.tsx
new file mode 100644
index 0000000..63387f5
--- /dev/null
+++ b/src/components/setup/PresetItemRow.tsx
@@ -0,0 +1,59 @@
+import { useTranslation } from "react-i18next"
+import type { CategoryType } from "@/lib/types"
+import { Checkbox } from "@/components/ui/checkbox"
+import { Badge } from "@/components/ui/badge"
+import { Input } from "@/components/ui/input"
+
+interface PresetItemRowProps {
+ slug: string
+ type: CategoryType
+ checked: boolean
+ amount: number
+ onToggle: () => void
+ onAmountChange: (val: number) => void
+}
+
+const colorVar = (type: CategoryType): string => {
+ const map: Record = {
+ income: "var(--color-income)",
+ bill: "var(--color-bill)",
+ variable_expense: "var(--color-variable-expense)",
+ debt: "var(--color-debt)",
+ saving: "var(--color-saving)",
+ investment: "var(--color-investment)",
+ }
+ return map[type]
+}
+
+export function PresetItemRow({
+ slug,
+ type,
+ checked,
+ amount,
+ onToggle,
+ onAmountChange,
+}: PresetItemRowProps) {
+ const { t } = useTranslation()
+
+ return (
+
+
+ {t(`presets.${type}.${slug}`)}
+
+ {t(`categories.types.${type}`)}
+
+ onAmountChange(Number(e.target.value))}
+ disabled={!checked}
+ min={0}
+ />
+
+ )
+}
diff --git a/src/components/setup/RecurringItemsStep.tsx b/src/components/setup/RecurringItemsStep.tsx
new file mode 100644
index 0000000..33bf9ac
--- /dev/null
+++ b/src/components/setup/RecurringItemsStep.tsx
@@ -0,0 +1,81 @@
+import { useTranslation } from "react-i18next"
+import { PRESETS } from "@/data/presets"
+import type { CategoryType } from "@/lib/types"
+import { AllocationBar } from "./AllocationBar"
+import { CategoryGroupHeader } from "./CategoryGroupHeader"
+import { PresetItemRow } from "./PresetItemRow"
+
+interface RecurringItemsStepProps {
+ selectedItems: Record
+ income: number
+ currency: string
+ onToggle: (slug: string) => void
+ onAmountChange: (slug: string, amount: number) => void
+}
+
+const GROUP_ORDER: CategoryType[] = [
+ "income",
+ "bill",
+ "variable_expense",
+ "debt",
+ "saving",
+ "investment",
+]
+
+export function RecurringItemsStep({
+ selectedItems,
+ income,
+ currency,
+ onToggle,
+ onAmountChange,
+}: RecurringItemsStepProps) {
+ const { t } = useTranslation()
+
+ // Group presets by type
+ const grouped = GROUP_ORDER.reduce(
+ (acc, type) => {
+ acc[type] = PRESETS.filter((p) => p.type === type)
+ return acc
+ },
+ {} as Record
+ )
+
+ // Compute remaining
+ const totalChecked = Object.values(selectedItems)
+ .filter((i) => i.checked)
+ .reduce((sum, i) => sum + i.amount, 0)
+ const remaining = income - totalChecked
+
+ return (
+
+
+
+ {GROUP_ORDER.map((type) => {
+ const items = grouped[type]
+ if (!items || items.length === 0) return null
+ return (
+
+ )
+ })}
+
+
+ )
+}
diff --git a/src/components/setup/WizardStepper.tsx b/src/components/setup/WizardStepper.tsx
new file mode 100644
index 0000000..55bd17e
--- /dev/null
+++ b/src/components/setup/WizardStepper.tsx
@@ -0,0 +1,62 @@
+import { useTranslation } from "react-i18next"
+import { Check } from "lucide-react"
+
+interface WizardStepperProps {
+ currentStep: 1 | 2 | 3
+ onStepClick: (step: 1 | 2 | 3) => void
+}
+
+const steps = [1, 2, 3] as const
+
+export function WizardStepper({ currentStep, onStepClick }: WizardStepperProps) {
+ const { t } = useTranslation()
+
+ return (
+
+ )
+}
diff --git a/src/pages/SetupPage.tsx b/src/pages/SetupPage.tsx
new file mode 100644
index 0000000..b682a55
--- /dev/null
+++ b/src/pages/SetupPage.tsx
@@ -0,0 +1,179 @@
+import { useState } from "react"
+import { useTranslation } from "react-i18next"
+import { useAuth } from "@/hooks/useAuth"
+import { useWizardState } from "@/hooks/useWizardState"
+import { supabase } from "@/lib/supabase"
+import type { Profile } from "@/lib/types"
+import { WizardStepper } from "@/components/setup/WizardStepper"
+import { IncomeStep } from "@/components/setup/IncomeStep"
+import { RecurringItemsStep } from "@/components/setup/RecurringItemsStep"
+import { Button } from "@/components/ui/button"
+import {
+ Card,
+ CardContent,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card"
+import { Skeleton } from "@/components/ui/skeleton"
+import { useEffect } from "react"
+
+export default function SetupPage() {
+ const { t } = useTranslation()
+ const { user, loading: authLoading } = useAuth()
+ const [profile, setProfile] = useState(null)
+ const [profileLoading, setProfileLoading] = useState(true)
+
+ useEffect(() => {
+ if (!user) return
+ supabase
+ .from("profiles")
+ .select("*")
+ .eq("id", user.id)
+ .single()
+ .then(({ data }) => {
+ if (data) setProfile(data)
+ setProfileLoading(false)
+ })
+ }, [user])
+
+ const userId = user?.id ?? ""
+ const currency = profile?.currency ?? "EUR"
+ const { state, setStep, setIncome, toggleItem, setItemAmount } =
+ useWizardState(userId)
+
+ const [incomeError, setIncomeError] = useState(null)
+
+ const loading = authLoading || profileLoading
+
+ if (loading) {
+ return (
+
+ )
+ }
+
+ const stepTitles: Record<1 | 2 | 3, string> = {
+ 1: t("setup.step1.title"),
+ 2: t("setup.step2.title"),
+ 3: t("setup.step3.title"),
+ }
+
+ const stepDescriptions: Record<1 | 2 | 3, string> = {
+ 1: t("setup.step1.description"),
+ 2: t("setup.step2.description"),
+ 3: t("setup.step3.description"),
+ }
+
+ function handleNext() {
+ if (state.currentStep === 1) {
+ if (!state.income || state.income <= 0) {
+ setIncomeError(t("setup.step1.validation"))
+ return
+ }
+ setIncomeError(null)
+ setStep(2)
+ } else if (state.currentStep === 2) {
+ setStep(3)
+ }
+ }
+
+ function handleBack() {
+ if (state.currentStep === 2) setStep(1)
+ else if (state.currentStep === 3) setStep(2)
+ }
+
+ function handleSkipStep() {
+ if (state.currentStep === 1) setStep(2)
+ else if (state.currentStep === 2) setStep(3)
+ }
+
+ function handleStepClick(step: 1 | 2 | 3) {
+ if (step <= state.currentStep) {
+ setStep(step)
+ }
+ }
+
+ return (
+
+
+
+
+
+
+
+ {stepTitles[state.currentStep]}
+
+
+ {stepDescriptions[state.currentStep]}
+
+
+
+ {state.currentStep === 1 && (
+
+ )}
+
+ {state.currentStep === 2 && (
+
+ )}
+
+ {state.currentStep === 3 && (
+
+
Review step coming in next plan
+
+ )}
+
+ {/* Bottom navigation */}
+
+
+ {state.currentStep > 1 && (
+
+ )}
+ {state.currentStep < 3 && (
+
+ )}
+
+
+ {state.currentStep < 3 && (
+
+ )}
+ {state.currentStep === 3 && (
+
+ )}
+
+
+
+
+
+
+
+
+ )
+}