Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 30 additions & 7 deletions client/src/lib/tags.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,50 @@
// Maks antall tags en oppskrift kan ha
export const MAX_TAGS = 6

// Alle tags, fordelt på kategorier
// Alle tags, fordelt på kategorier.
// Rekkefølgen her er rekkefølgen på kategoriknappene i dialogen.
// Hver tag må ha et unikt navn på tvers av ALLE kategorier.
export const tagGroups: Record<string, string[]> = {
Kosthold: [
'Vegetar', 'Vegansk', 'Pescetar', 'Glutenfri', 'Laktosefri',
'Melkefri', 'Eggfri', 'Nøttefri', 'Soyafri', 'Uten tilsatt sukker',
'Lavkarbo', 'Keto', 'Paleo', 'Halal', 'Kosher',
'Plantebasert', 'Hvetefri', 'Fettfattig', 'Lavt saltinnhold',
],
Hovedingrediens: [
'Kylling', 'Storfe', 'Svinekjøtt', 'Lam', 'Fisk', 'Laks',
'Sjømat', 'Egg', 'Ost', 'Pasta', 'Ris', 'Poteter',
'Bønner og linser', 'Tofu', 'Sopp', 'Grønnsaker', 'Mel og deig',
],
'Type rett': [
'Suppe', 'Salat', 'Gryte', 'Wok', 'Pai', 'Pizza',
'Burger', 'Tacos', 'Smoothie', 'Brød', 'Kake', 'Kjeks',
'Saus', 'Dip', 'Tilbehør',
],
'Tid og tilberedning': [
'Under 15 min', 'Under 30 min', 'Under 60 min', 'Én gryte', 'Airfryer', 'Ovnsbakt',
'Ingen steking', 'Ingen koking', 'Meal prep', 'Frysevennlig', 'Grilling', 'Slow cooker',
'Under 15 min', 'Under 30 min', 'Under 45 min', 'Under 60 min', 'Langtidskoking',
'Én gryte', 'Én form', 'Lite oppvask', 'Stekepanne', 'Airfryer', 'Ovnsbakt',
'Mikrobølgeovn', 'Ingen steking', 'Ingen koking', 'Meal prep', 'Frysevennlig',
'Grilling', 'Slow cooker',
],
Matpreferanser: [
'Sterk mat', 'Mild', 'Proteinrik', 'Fiberrik', 'Grønnsaksrik', 'Sukkerfri',
'Søt', 'Salt', 'Syrlig', 'Sprø', 'Kremet', 'Comfort food',
'Røkt', 'Frisk', 'Mettende', 'Lett', 'Saftig', 'Umami',
],
Matkultur: [
'Norsk', 'Nordisk', 'Italiensk', 'Meksikansk', 'Tex-Mex', 'Amerikansk',
'Indisk', 'Japansk', 'Koreansk', 'Thai', 'Kinesisk', 'Vietnamesisk',
'Gresk', 'Fransk', 'Spansk', 'Tyrkisk', 'Libanesisk', 'Marokkansk',
'Middelhavsmat',
],
Anledning: [
'Studentvennlig', 'Budsjettvennlig', 'Barnevennlig', 'Festmat', 'Hverdagsmat', 'Helgekos',
'Matpakke', 'Selskap', 'Date night', 'Sommermat', 'Vintermat', 'Turmat',
'Fredagskos', 'Matpakke', 'Selskap', 'Gjester', 'Date night', 'Bursdag',
'Brunsj', 'Piknik', 'Restemat', 'Turmat',
],
Matkultur: [
'Italiensk', 'Meksikansk', 'Indisk', 'Japansk', 'Koreansk', 'Thai',
'Kinesisk', 'Gresk', 'Fransk', 'Spansk', 'Middelhavsmat', 'Nordisk',
Sesong: [
'Vårmat', 'Sommermat', 'Høstmat', 'Vintermat', 'Julemat', 'Påskemat',
'Nyttårsaften', '17. mai', 'Sankthans', 'Halloween', 'Grillsesong', 'Sesongvarer',
],
}
117 changes: 40 additions & 77 deletions client/src/routes/NewRecipePage.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@

import { useState } from 'react'
import { useNavigate } from 'react-router'
import { useMutation } from '@apollo/client/react'
import { CREATE_RECIPE } from '../graphql/mutations'

Expand All @@ -8,23 +8,19 @@ import RecipeStep2 from '../components/new-recipe/RecipeStep2'
import RecipeStep3 from '../components/new-recipe/RecipeStep3'
import RecipeStep4 from '../components/new-recipe/RecipeStep4'

// Definerer hvordan en ingrediens skal se ut
export type Ingredient = {
id: number
name: string
amount: string
unit: string
}

// Definerer hvordan en instruksjon skal se ut
export type Instruction = {
id: number
text: string
}

// All informasjon som lagres i oppskriftsskjemaet
export type RecipeFormData = {
// Step 1 - Generell informasjon
title: string
description: string
videoUrl: string
Expand All @@ -34,30 +30,26 @@ export type RecipeFormData = {
category: string
tags: string[]
rating: number | null

// Step 2 - Ingredienser
measurementSystem: 'metric' | 'imperial'
ingredients: Ingredient[]
ingredientNote: string

// Step 3 - Fremgangsmåte
instructionMode: 'steps' | 'text'
instructionText: string
steps: { id: number; text: string; minutes: string }[]
}

// Deler samlet tekst opp i separate steg
type CreateRecipeData = {
createRecipe: {
id: string
title: string
status: string
} | null
}

function parseInstructionText(text: string): string[] {
const trimmedText = text.trim()
if (!trimmedText) return []

if (!trimmedText) {
return []
}

// Finner nummererte steg som 1., 2., 3. osv.
// Fungerer både med og uten mellomrom etter punktum.
// Nummereringen må starte på en ny linje eller etter
// et mellomrom.
const parts = trimmedText.split(
/(?:^|\s)(?=\d+\.\s*[A-Za-zÆØÅæøå])/,
)
Expand All @@ -67,7 +59,6 @@ function parseInstructionText(text: string): string[] {
.filter(Boolean)
}

// Props som styrer hvilket steg brukeren er på
type NewRecipePageProps = {
currentStep: number
setCurrentStep: (step: number) => void
Expand All @@ -77,9 +68,9 @@ export default function NewRecipePage({
currentStep,
setCurrentStep,
}: NewRecipePageProps) {
// Lagrer informasjonen brukeren fyller inn
const navigate = useNavigate()

const [recipeData, setRecipeData] = useState<RecipeFormData>({
// Step 1
title: '',
description: '',
videoUrl: '',
Expand All @@ -89,8 +80,6 @@ export default function NewRecipePage({
category: 'Middag',
tags: [],
rating: null,

// Step 2
measurementSystem: 'metric',
ingredients: [
{
Expand All @@ -101,8 +90,6 @@ export default function NewRecipePage({
},
],
ingredientNote: '',

// Step 3
instructionMode: 'steps',
instructionText: '',
steps: [
Expand All @@ -114,95 +101,69 @@ export default function NewRecipePage({
],
})

// Kobler frontend til GraphQL-mutasjonen
const [createRecipe, { loading, error }] =
useMutation(CREATE_RECIPE)
useMutation<CreateRecipeData>(CREATE_RECIPE)

// Holder styr på om bildet lastes opp
const [uploading, setUploading] = useState(false)
const [publishError, setPublishError] = useState<string | null>(
null,
)
const [publishError, setPublishError] = useState<string | null>(null)

// Funksjonen som kjøres når man publiserer oppskriften
const handlePublish = async () => {
if (loading || uploading) return

setPublishError(null)
setUploading(true)

// Gjør ingrediensene om til formatet backend forventer
const ingredients = recipeData.ingredients.map(
(ingredient, index) => ({
name: ingredient.name,
amount: Number(ingredient.amount),
unit: ingredient.unit,
order: index + 1,
}),
)
const ingredients = recipeData.ingredients.map((ingredient, index) => ({
name: ingredient.name,
amount: Number(ingredient.amount),
unit: ingredient.unit,
order: index + 1,
}))

// Gjør fremgangsmåten om til formatet backend forventer
let steps: { order: number; instruction: string }[] = []

if (recipeData.instructionMode === 'steps') {
// Brukeren har skrevet ett steg i hvert felt
steps = recipeData.steps
.filter((step) => step.text.trim() !== '')
.map((step, index) => ({
order: index + 1,
instruction: step.text.trim(),
}))
} else {
// Brukeren har skrevet hele fremgangsmåten i ett tekstfelt
const parsedSteps = parseInstructionText(
recipeData.instructionText,
)

const parsedSteps = parseInstructionText(recipeData.instructionText)
steps = parsedSteps.map((instruction, index) => ({
order: index + 1,
instruction,
}))
}

// Sørger for at oppskriften har minst ett steg
if (steps.length === 0) {
setPublishError(
'Du må legge til minst ett steg i fremgangsmåten.',
)
setPublishError('Du må legge til minst ett steg i fremgangsmåten.')
setUploading(false)
return
}

try {
// 1. Laster opp bildet til backend hvis brukeren har valgt et
let imageUrl: string | null = null

if (recipeData.imageFile) {
const formData = new FormData()
formData.append('image', recipeData.imageFile)

const uploadResponse = await fetch(
'http://localhost:3001/upload',
{
method: 'POST',
body: formData,
},
)
const uploadResponse = await fetch('http://localhost:3001/upload', {
method: 'POST',
body: formData,
})

if (!uploadResponse.ok) {
throw new Error('Kunne ikke laste opp bildet')
}

// Backend returnerer adressen til bildet
const uploadResult: { imageUrl: string } =
await uploadResponse.json()

const uploadResult: { imageUrl: string } = await uploadResponse.json()
imageUrl = uploadResult.imageUrl

console.log('Bilde lastet opp:', imageUrl)
}

// 2. Sender oppskriften og bilde-URL-en til GraphQL
const result = await createRecipe({
variables: {
input: {
Expand All @@ -222,14 +183,21 @@ export default function NewRecipePage({
console.log('Oppskrift publisert:', result.data)
console.log('Antall steg lagret:', steps.length)

alert('Oppskriften ble publisert!')
const recipeId = result.data?.createRecipe?.id
if (!recipeId) {
setPublishError(
'Oppskriften ble lagret, men kunne ikke åpnes automatisk. Sjekk oppskriftslisten før du prøver igjen.',
)
return
}

// Navigerer til detaljsiden for oppskriften som nettopp ble opprettet.
// Forutsetter at appens rute er /recipes/:id.
navigate(`/recipes/${recipeId}`, { replace: true })
} catch (err) {
console.error('Kunne ikke publisere oppskrift:', err)

setPublishError(
err instanceof Error
? err.message
: 'Noe gikk galt under publiseringen.',
err instanceof Error ? err.message : 'Noe gikk galt under publiseringen.',
)
} finally {
setUploading(false)
Expand All @@ -238,7 +206,6 @@ export default function NewRecipePage({

return (
<div className="w-full">
{/* STEP 1 - Generell informasjon */}
{currentStep === 1 && (
<RecipeStep1
data={recipeData}
Expand All @@ -247,7 +214,6 @@ export default function NewRecipePage({
/>
)}

{/* STEP 2 - Ingredienser */}
{currentStep === 2 && (
<RecipeStep2
data={recipeData}
Expand All @@ -257,7 +223,6 @@ export default function NewRecipePage({
/>
)}

{/* STEP 3 - Fremgangsmåte */}
{currentStep === 3 && (
<RecipeStep3
data={recipeData}
Expand All @@ -267,7 +232,6 @@ export default function NewRecipePage({
/>
)}

{/* STEP 4 - Forhåndsvisning og publisering */}
{currentStep === 4 && (
<>
<RecipeStep4
Expand All @@ -284,8 +248,7 @@ export default function NewRecipePage({

{(publishError || error) && (
<p role="alert" className="mt-3 text-primary">
Kunne ikke publisere oppskriften:{' '}
{publishError || error?.message}
Kunne ikke publisere oppskriften: {publishError || error?.message}
</p>
)}
</>
Expand Down
Loading