2d59a3e636
- 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>
141 lines
4.1 KiB
TypeScript
141 lines
4.1 KiB
TypeScript
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>
|
||
);
|
||
}
|