Files
Cibello-app/apps/mobile/src/app/submit-recipe.tsx
T
Claude 2d59a3e636 i18n(app-svep) + i18n-vakt: hela mobil-UI:t översatt + CI-vakt mot regress
- Svepte 51 hårdkodade strängar -> t() över konto, sök/bläddra, sparade recept,
  kök, planera, skanna, byt måltid, inköp, minne, cooking, api-fel (+ locLabel/
  kategorier via nycklar). 47 nya nycklar, översatta till alla 12 språk.
- NY: apps/mobile/scripts/i18n-check.mjs (i18n:check) – fäller bygget om ett språk
  saknar en nyckel, en använd nyckel saknas, eller det finns hårdkodad UI-text.
  Wire:ad i CI (.github/workflows/ci.yml) efter typecheck. => 'lägg till språk'
  blir: kör vakten, den listar exakt vad som fattas. Aldrig mer leta för hand.
- Prettier-fixade 5 filer från tidigare leveranser så format:check blir grön.
- submit-recipe fri-text-payload + firebase dev-stubs markerade // i18n-ignore.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-21 01:50:08 +00:00

141 lines
4.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from "react";
import { router } from "expo-router";
import { api, ApiError } from "@/lib/api";
import { t } from "@/lib/i18n";
import { Body, Button, Card, Heading, Input, Screen, Small, Spacer, Title } from "@/components/ui";
import { spacing } from "@/lib/theme";
/**
* Skicka in eget recept (spec §35). Enkel mall: titel + ingredienser
* (mängd + gram/kilo) + tillagning. Skickas som fritext → AAMOS
* (STRUCTURE_RECIPE_TEXT) tolkar och strukturerar, workern AI-granskar,
* dubblettkollar, finskriver och auto-publicerar märkt med ditt namn.
*/
type Phase = "form" | "done" | "premium";
export default function SubmitRecipeScreen() {
const [title, setTitle] = useState("");
const [ingredients, setIngredients] = useState("");
const [steps, setSteps] = useState("");
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
const [phase, setPhase] = useState<Phase>("form");
const canSubmit =
title.trim().length >= 3 && ingredients.trim().length >= 3 && steps.trim().length >= 3;
const submit = async () => {
if (!canSubmit || busy) return;
setBusy(true);
setError(null);
// Bygg en tydlig fritext-mall som AI:n tolkar robust.
const text = [
`Titel: ${title.trim()}`, // i18n-ignore (fri-text-payload till receptparsern, ej UI)
"",
"Ingredienser:", // i18n-ignore
ingredients.trim(),
"",
"Gör så här:", // i18n-ignore
steps.trim(),
].join("\n");
try {
await api("/v1/recipes", { method: "POST", body: { mode: "free_text", text } });
setPhase("done");
} catch (err) {
if (err instanceof ApiError && err.status === 402) {
setPhase("premium");
} else {
setError(err instanceof Error ? err.message : t("common.error"));
}
} finally {
setBusy(false);
}
};
if (phase === "done") {
return (
<Screen>
<Card>
<Heading>🎉 {t("submit.successTitle")}</Heading>
<Body>{t("submit.successBody")}</Body>
<Spacer size={spacing.md} />
<Button label={t("submit.backHome")} onPress={() => router.back()} />
</Card>
</Screen>
);
}
if (phase === "premium") {
return (
<Screen>
<Card>
<Heading> {t("submit.premiumTitle")}</Heading>
<Body>{t("submit.premiumBody")}</Body>
<Spacer size={spacing.md} />
<Button label={t("submit.seePlans")} onPress={() => router.replace("/profile")} />
<Spacer size={spacing.xs} />
<Button label={t("submit.backHome")} variant="ghost" onPress={() => router.back()} />
</Card>
</Screen>
);
}
return (
<Screen>
<Title>{t("submit.title")}</Title>
<Small>{t("submit.subtitle")}</Small>
<Spacer size={spacing.md} />
<Small>{t("submit.titleLabel")}</Small>
<Input
placeholder={t("submit.titlePlaceholder")}
value={title}
onChangeText={setTitle}
maxLength={150}
/>
<Spacer size={spacing.sm} />
<Small>{t("submit.ingredientsLabel")}</Small>
<Input
placeholder={t("submit.ingredientsPlaceholder")}
value={ingredients}
onChangeText={setIngredients}
multiline
numberOfLines={6}
style={{ minHeight: 120, textAlignVertical: "top" }}
maxLength={3000}
/>
<Spacer size={spacing.sm} />
<Small>{t("submit.stepsLabel")}</Small>
<Input
placeholder={t("submit.stepsPlaceholder")}
value={steps}
onChangeText={setSteps}
multiline
numberOfLines={8}
style={{ minHeight: 150, textAlignVertical: "top" }}
maxLength={5000}
/>
<Spacer size={spacing.sm} />
{error && (
<>
<Small>{`⚠️ ${error}`}</Small>
<Spacer size={spacing.xs} />
</>
)}
<Button
label={busy ? t("submit.sending") : t("submit.button")}
onPress={() => void submit()}
loading={busy}
disabled={!canSubmit}
/>
<Spacer size={spacing.sm} />
<Small>{t("submit.reviewNote")}</Small>
</Screen>
);
}