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>
);
}
+18 -1
View File
@@ -463,5 +463,22 @@
"barcode.info.noNutrition": "Ingen näringsinfo finns för den här produkten ännu.",
"barcode.info.scanAgain": "Skanna igen",
"barcode.info.sourceOwn": "Källa: egen produktdatabas",
"barcode.info.sourceOff": "Källa: Open Food Facts"
"barcode.info.sourceOff": "Källa: Open Food Facts",
"submit.title": "Skicka in eget recept",
"submit.subtitle": "Fyll i mallen så finskriver och publicerar vår AI receptet märkt med ditt namn.",
"submit.titleLabel": "Receptets namn",
"submit.titlePlaceholder": "t.ex. Mormors köttbullar",
"submit.ingredientsLabel": "Ingredienser (en per rad, med mängd)",
"submit.ingredientsPlaceholder": "2 dl vetemjöl\n500 g köttfärs\n1 gul lök\n1 dl mjölk\n1 tsk salt",
"submit.stepsLabel": "Gör så här",
"submit.stepsPlaceholder": "1. Hacka och fräs löken mjuk.\n2. Blanda färs, ägg, mjölk och kryddor.\n3. Rulla bollar och stek gyllene.",
"submit.button": "Skicka in receptet",
"submit.sending": "Skickar …",
"submit.reviewNote": "Vi jämför mot befintliga recept, putsar språket till samma standard och publicerar automatiskt. Dubbletter läggs inte ut.",
"submit.successTitle": "Tack receptet är inskickat!",
"submit.successBody": "Vår AI läser igenom, granskar och finskriver det. Det publiceras strax, märkt med ditt namn, och dyker upp bland recepten.",
"submit.premiumTitle": "Egna recept ingår i Premium",
"submit.premiumBody": "Att skicka in och publicera egna recept ingår i Premium. Starta din provperiod eller uppgradera så kör vi.",
"submit.seePlans": "Se planer",
"submit.backHome": "Tillbaka"
}