diff --git a/src/components/ui/checkbox.tsx b/src/components/ui/checkbox.tsx new file mode 100644 index 0000000..98ac83e --- /dev/null +++ b/src/components/ui/checkbox.tsx @@ -0,0 +1,30 @@ +import * as React from "react" +import { CheckIcon } from "lucide-react" +import { Checkbox as CheckboxPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" + +function Checkbox({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + + + ) +} + +export { Checkbox } diff --git a/src/hooks/useWizardState.ts b/src/hooks/useWizardState.ts new file mode 100644 index 0000000..141c8b5 --- /dev/null +++ b/src/hooks/useWizardState.ts @@ -0,0 +1,85 @@ +import { useState, useEffect } from "react" +import { PRESETS } from "@/data/presets" + +export interface WizardState { + currentStep: 1 | 2 | 3 + income: number + selectedItems: Record +} + +const STORAGE_KEY = (userId: string) => `setup-wizard-${userId}` + +function getDefaultState(): WizardState { + const selectedItems: Record = {} + for (const preset of PRESETS) { + selectedItems[preset.slug] = { + checked: preset.type === "bill" || preset.type === "variable_expense", + amount: preset.defaultAmount, + } + } + return { currentStep: 1, income: 3000, selectedItems } +} + +export function useWizardState(userId: string) { + const [state, setState] = useState(() => { + try { + const saved = localStorage.getItem(STORAGE_KEY(userId)) + if (saved) { + const parsed = JSON.parse(saved) + // Validate shape (T-07-01 mitigation: validate JSON on load) + if ( + parsed && + typeof parsed.currentStep === "number" && + parsed.currentStep >= 1 && + parsed.currentStep <= 3 && + typeof parsed.income === "number" && + parsed.income >= 0 && + parsed.selectedItems && + typeof parsed.selectedItems === "object" + ) { + return parsed as WizardState + } + } + } catch { + /* ignore corrupt data */ + } + return getDefaultState() + }) + + useEffect(() => { + localStorage.setItem(STORAGE_KEY(userId), JSON.stringify(state)) + }, [state, userId]) + + const setStep = (step: 1 | 2 | 3) => + setState((s) => ({ ...s, currentStep: step })) + + const setIncome = (income: number) => + setState((s) => ({ ...s, income })) + + const toggleItem = (slug: string) => + setState((s) => ({ + ...s, + selectedItems: { + ...s.selectedItems, + [slug]: { + ...s.selectedItems[slug], + checked: !s.selectedItems[slug].checked, + }, + }, + })) + + const setItemAmount = (slug: string, amount: number) => + setState((s) => ({ + ...s, + selectedItems: { + ...s.selectedItems, + [slug]: { ...s.selectedItems[slug], amount }, + }, + })) + + const clearState = () => { + localStorage.removeItem(STORAGE_KEY(userId)) + } + + return { state, setStep, setIncome, toggleItem, setItemAmount, clearState } +} diff --git a/src/i18n/de.json b/src/i18n/de.json index cf7e00f..3352cd1 100644 --- a/src/i18n/de.json +++ b/src/i18n/de.json @@ -121,6 +121,44 @@ "error": "Etwas ist schiefgelaufen", "confirm": "Bestätigen" }, + "setup": { + "title": "Budget einrichten", + "step1": { + "title": "Monatliches Einkommen", + "description": "Wie viel verdienst du pro Monat?", + "incomeLabel": "Monatliches Nettoeinkommen", + "helper": "Gib dein monatliches Nettoeinkommen ein", + "validation": "Bitte gib einen positiven Betrag ein" + }, + "step2": { + "title": "Wiederkehrende Posten", + "description": "Wähle deine regelmäßigen monatlichen Ausgaben", + "remaining": "Verbleibend zu verteilen" + }, + "step3": { + "title": "Übersicht", + "description": "Bestätige deine Budgetvorlage", + "incomeLabel": "Monatliches Einkommen", + "totalLabel": "Gesamtausgaben", + "remainingLabel": "Verbleibend", + "empty": "Keine Posten ausgewählt. Du kannst später Posten zu deiner Vorlage hinzufügen." + }, + "steps": { + "1": "Einkommen", + "2": "Posten", + "3": "Übersicht" + }, + "next": "Nächster Schritt", + "back": "Zurück", + "skip": "Schritt überspringen", + "skipSetup": "Einrichtung überspringen", + "complete": "Einrichtung abschließen", + "toast": { + "success": "Vorlage erstellt! Dein erstes Budget wird automatisch erscheinen.", + "error": "Vorlage konnte nicht gespeichert werden. Bitte versuche es erneut.", + "partialError": "Einige Posten konnten nicht gespeichert werden. Prüfe deine Vorlagenseite." + } + }, "presets": { "income": { "salary": "Gehalt", diff --git a/src/i18n/en.json b/src/i18n/en.json index a38a475..5531496 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -121,6 +121,44 @@ "error": "Something went wrong", "confirm": "Confirm" }, + "setup": { + "title": "Set up your budget", + "step1": { + "title": "Monthly Income", + "description": "How much do you earn each month?", + "incomeLabel": "Monthly net income", + "helper": "Enter your total monthly take-home pay", + "validation": "Please enter a positive income amount" + }, + "step2": { + "title": "Recurring Items", + "description": "Select your regular monthly expenses", + "remaining": "Remaining to allocate" + }, + "step3": { + "title": "Review", + "description": "Confirm your budget template", + "incomeLabel": "Monthly income", + "totalLabel": "Total expenses", + "remainingLabel": "Remaining", + "empty": "No items selected. You can add items to your template later." + }, + "steps": { + "1": "Income", + "2": "Items", + "3": "Review" + }, + "next": "Next Step", + "back": "Go Back", + "skip": "Skip Step", + "skipSetup": "Skip setup", + "complete": "Complete Setup", + "toast": { + "success": "Template created! Your first budget will appear automatically.", + "error": "Could not save your template. Please try again.", + "partialError": "Some items could not be saved. Check your template page." + } + }, "presets": { "income": { "salary": "Salary",