New import in version 0.1
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
# Teknisk beskrivning av Recipe App
|
||||
|
||||
> Se [README.md](README.md) för användarinformation och kom-igång-guide.
|
||||
|
||||
## Översikt
|
||||
|
||||
Recipe App är en fullstack-applikation för hantering av hemmavaror, recept och inköpsplanering. Systemet är byggt med Next.js (frontend), NestJS (backend), Prisma ORM och MariaDB. Applikationen är containeriserad med Docker och använder Caddy som reverse proxy.
|
||||
@@ -114,6 +116,7 @@ Recipe App är en fullstack-applikation för hantering av hemmavaror, recept och
|
||||
- Skapa, redigera, ta bort recept
|
||||
- Jämför mot hemmavaror (räcker/saknas/enhetskonflikt)
|
||||
- Visar instruktioner och saknade ingredienser
|
||||
- **Importera recept från Markdown** (se nedan)
|
||||
|
||||
### Produkter (Admin)
|
||||
- Sök, sortera, redigera canonical name
|
||||
@@ -135,12 +138,180 @@ Recipe App är en fullstack-applikation för hantering av hemmavaror, recept och
|
||||
- `POST /api/recipes` – Skapa recept
|
||||
- `PATCH /api/recipes/:id` – Uppdatera recept
|
||||
- `DELETE /api/recipes/:id` – Ta bort recept
|
||||
- `POST /api/recipes/parse-markdown` – Tolka Markdown-recept (se nedan)
|
||||
- `GET /api/products` – Lista produkter
|
||||
- `PATCH /api/products/:id` – Uppdatera produkt
|
||||
- `GET /health` – Hälsokontroll
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## Receptimport via Markdown
|
||||
|
||||
### Syfte
|
||||
|
||||
Användaren kan importera ett recept skrivet i ett enkelt Markdown-format istället för att fylla i formularet manuellt. Systemet tolkar texten, föreslår matchande produkter från databasen och låter användaren granska och bekräfta innan receptet sparas.
|
||||
|
||||
### Markdown-format
|
||||
|
||||
```markdown
|
||||
# Receptnamn
|
||||
|
||||
Valfri beskrivning av receptet.
|
||||
|
||||
## Ingredienser
|
||||
- 500 g köttfärs
|
||||
- 1 st lök
|
||||
- 2 msk tomatpuré
|
||||
- 1 dl grädde (vispgrädde)
|
||||
|
||||
## Tillvägagångssätt
|
||||
Stek löken i lite smör. Tillsats köttfärsen...
|
||||
```
|
||||
|
||||
Regler:
|
||||
- Rad med `#` tolkas som receptnamn
|
||||
- Text mellan `#`-rubriken och `## Ingredienser` tolkas som beskrivning
|
||||
- Rader under `## Ingredienser` med mönstret `- ANTAL ENHET NAMN` tolkas som ingredienser
|
||||
- Text i parentes efter ingrediensnamnet (`(vispgrädde)`) sparas som anteckning
|
||||
- Text under `## Tillvägagångssätt` (eller `## Instruktioner`) tolkas som instruktioner
|
||||
|
||||
### Arkitektur
|
||||
|
||||
```
|
||||
Användaren Frontend Backend Bibliotek
|
||||
(klistrar in MD) → /recipes/import → POST /api/ → recipe-document-
|
||||
ImportRecipePage recipes/ converter/
|
||||
parse-markdown parseRecipeMarkdown()
|
||||
↑
|
||||
Granskar förslag
|
||||
Väljer produkter
|
||||
↓
|
||||
POST /api/recipes (befintlig endpoint)
|
||||
```
|
||||
|
||||
### Komponenterna
|
||||
|
||||
#### `recipe-document-converter/` (fristående TypeScript-bibliotek)
|
||||
|
||||
Ett eget npm-paket som inte har externa beroenden. Det enda som exporteras är:
|
||||
|
||||
```typescript
|
||||
parseRecipeMarkdown(markdown: string): ParsedRecipe
|
||||
```
|
||||
|
||||
Returnerar:
|
||||
```typescript
|
||||
type ParsedRecipe = {
|
||||
name: string;
|
||||
description?: string;
|
||||
instructions?: string;
|
||||
ingredients: Array<{
|
||||
rawName: string; // fråntext, t.ex. "köttfärs"
|
||||
quantity: number; // t.ex. 500
|
||||
unit: string; // t.ex. "g"
|
||||
note?: string; // text i parentes, t.ex. "nötfärs"
|
||||
}>;
|
||||
};
|
||||
```
|
||||
|
||||
Biblioteket kompileras i ett separat Docker-byggsteg och länkas till backend via `"recipe-document-converter": "file:../recipe-document-converter"` i `backend/package.json`.
|
||||
|
||||
#### Backend — `POST /api/recipes/parse-markdown`
|
||||
|
||||
Endpoint som tar emot `{ markdown: string }` och returnerar det tolkade receptet åtsamman med produktmatchförslag för varje ingrediens.
|
||||
|
||||
Matchningslogik:
|
||||
1. Anropar `parseRecipeMarkdown()` från biblioteket
|
||||
2. Hämtar alla aktiva produkter ur databasen
|
||||
3. Jämför varje ingrediensnamn mot `product.canonicalName` / `product.normalizedName` med tre metoder i ordning:
|
||||
- **Exakt match** (efter normalisering) → 100 poäng
|
||||
- **Delsträngsmatch** → 70 poäng
|
||||
- **Levenshtein-likhet** → 0–100 poäng (filtreras under 40)
|
||||
4. Returnerar upp till 5 förslag per ingrediens, sorterade efter poäng
|
||||
|
||||
Svar:
|
||||
```json
|
||||
{
|
||||
"name": "Köttfärssås",
|
||||
"description": "En klassisk...",
|
||||
"instructions": "Stek löken...",
|
||||
"ingredients": [
|
||||
{
|
||||
"rawName": "köttfärs",
|
||||
"quantity": 500,
|
||||
"unit": "g",
|
||||
"suggestions": [
|
||||
{ "productId": 12, "productName": "Köttfärs", "score": 100 },
|
||||
{ "productId": 34, "productName": "Blandfärs", "score": 55 }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### Frontend — `/recipes/import`
|
||||
|
||||
En 3-stegsvy (client component):
|
||||
|
||||
| Steg | Innehåll |
|
||||
|------|----------|
|
||||
| 1. Klistra in | Textarea för Markdown + "Tolka recept"-knapp |
|
||||
| 2. Granska | Redigerbara fält för namn/beskrivning/instruktioner; varje ingrediens har en dropdown med föreslagna produkter överst, sedan alla produkter |
|
||||
| 3. Spara | Knapp som POSTar till befintlig `POST /api/recipes` |
|
||||
|
||||
Ingrediensräder med ingen matchning markeras visuellt (gul ram) så att användaren ser att de behöver väljas manuellt. Receptet sparas inte förrän minst en ingrediens har en vald produkt.
|
||||
|
||||
Flöde i Next.js:
|
||||
```
|
||||
/recipes/import
|
||||
└─ ImportRecipePage.tsx (client component, 3-stegsflödet)
|
||||
|
||||
/api/parse-markdown-proxy
|
||||
└─ route.ts (POST-proxy till backend, omgår CORS)
|
||||
```
|
||||
|
||||
### Docker-bygget
|
||||
|
||||
Backend-Dockerfilen använder nu projektets rot (`.`) som byggkontext. Converter-biblioteket kompileras i en separat stage:
|
||||
|
||||
```dockerfile
|
||||
# Stage 1: Bygg converter-biblioteket
|
||||
FROM node:22-alpine AS converter-build
|
||||
WORKDIR /converter
|
||||
COPY recipe-document-converter/package.json ./
|
||||
RUN npm install
|
||||
COPY recipe-document-converter/src ./src
|
||||
COPY recipe-document-converter/tsconfig.json ./
|
||||
RUN npm run build
|
||||
|
||||
# Stage 2: Installera backend-beroenden (converter kopieras in)
|
||||
FROM node:22-alpine AS deps
|
||||
WORKDIR /app
|
||||
COPY --from=converter-build /converter /recipe-document-converter
|
||||
...
|
||||
```
|
||||
|
||||
Bygga om backend efter ändringar:
|
||||
```bash
|
||||
docker compose build recipe-api
|
||||
```
|
||||
|
||||
### Relevanta filer
|
||||
|
||||
| Fil | Syfte |
|
||||
|-----|-------|
|
||||
| `recipe-document-converter/src/parser.ts` | Markdown-parser |
|
||||
| `recipe-document-converter/src/index.ts` | Biblioteksexport |
|
||||
| `backend/src/recipes/dto/parse-markdown.dto.ts` | Inkommande DTO |
|
||||
| `backend/src/recipes/recipes.controller.ts` | Nytt endpoint |
|
||||
| `backend/src/recipes/recipes.service.ts` | Matchningslogik |
|
||||
| `frontend/app/recipes/import/ImportRecipePage.tsx` | 3-stegsvy |
|
||||
| `frontend/app/api/parse-markdown-proxy/route.ts` | Proxy-route |
|
||||
|
||||
---
|
||||
|
||||
## Säkerhet
|
||||
|
||||
- Ingen auth i grundutförande (kan enkelt byggas på)
|
||||
|
||||
Reference in New Issue
Block a user