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:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user