feat(egna-recept): knapp i Hem + inmatningsmall for egna recept

Ny knapp 'Skicka in eget recept' i Hem, direkt under 'Dina sparade
recept' (samma kort-stil). Ny skarm submit-recipe: enkel mall (titel +
ingredienser med mangd/gram/kilo + tillagning). Skickas som fritext ->
AAMOS strukturerar -> workern AI-granskar, dubblettkollar, finskriver
och auto-publicerar, markt med anvandarens namn.

Hanterar 402/PREMIUM_REQUIRED snyggt (egna recept ingar i Premium/trial;
gratisnivan ser uppgraderingsvy i stallet for ratt fel). Backend fanns
redan (POST /v1/recipes + STRUCTURE_RECIPE_TEXT).
This commit is contained in:
Claude
2026-08-19 21:54:28 +00:00
parent f45eb5724b
commit 0e057bc25d
3 changed files with 165 additions and 1 deletions
+4
View File
@@ -135,6 +135,10 @@ export default function HomeScreen() {
<Body> Dina sparade recept</Body>
</Card>
<Card onPress={() => router.push("/submit-recipe")}>
<Body> Skicka in eget recept</Body>
</Card>
{/* Matlager städat, öppnas per plats eller som helhet (spec §8). */}
<Card>
<Heading>🧺 {t("home.inventory")}</Heading>
+143
View File
@@ -0,0 +1,143 @@
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()}`,
"",
"Ingredienser:",
ingredients.trim(),
"",
"Gör så här:",
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>
);
}