From c1f3654b19520556c844569e54dda2b043ec6935 Mon Sep 17 00:00:00 2001 From: Isabella Hauan-Zamora Date: Wed, 14 Oct 2026 17:35:30 +0200 Subject: [PATCH 1/2] Fikset bildeopplasting og tags etter konflikt --- .../src/components/new-recipe/RecipeStep1.tsx | 123 +++++++++++++++--- client/src/routes/NewRecipePage.tsx | 77 ++++++++--- 2 files changed, 168 insertions(+), 32 deletions(-) diff --git a/client/src/components/new-recipe/RecipeStep1.tsx b/client/src/components/new-recipe/RecipeStep1.tsx index ed69a9e..8ce566c 100644 --- a/client/src/components/new-recipe/RecipeStep1.tsx +++ b/client/src/components/new-recipe/RecipeStep1.tsx @@ -2,12 +2,13 @@ import { useState } from 'react' import type { Dispatch, SetStateAction } from 'react' import Rating from '@mui/material/Rating' + import { Button } from '../ui/Button' import { Field } from '../ui/Field' import { Input, Textarea } from '../ui/Input' import { Tag } from '../ui/Tag' -import { ImageUpload } from '../ui/ImageUpload' import TagDialog from './TagDialog' + import type { RecipeFormData } from '../../routes/NewRecipePage' // Sånn at informasjonen blir lagret mellom stegene @@ -49,7 +50,10 @@ export default function RecipeStep1({ key: K, value: RecipeFormData[K], ) => { - setData((prev) => ({ ...prev, [key]: value })) + setData((prev) => ({ + ...prev, + [key]: value, + })) } // Legger til eller fjerner en tag (maks 6 totalt) @@ -69,6 +73,35 @@ export default function RecipeStep1({ (tag) => !dietaryTags.includes(tag), ) + // Når brukeren velger et bilde + const handleImageChange = ( + event: React.ChangeEvent, + ) => { + const file = event.target.files?.[0] + + if (!file) return + + const allowedTypes = [ + 'image/jpeg', + 'image/png', + 'image/webp', + ] + + if (!allowedTypes.includes(file.type)) { + alert('Du kan bare laste opp JPG, PNG eller WebP.') + event.target.value = '' + return + } + + if (file.size > 5 * 1024 * 1024) { + alert('Bildet kan ikke være større enn 5 MB.') + event.target.value = '' + return + } + + update('imageFile', file) + } + return (

@@ -78,13 +111,50 @@ export default function RecipeStep1({

{/* VENSTRE SIDE */}
+ {/* BILDEOPPLASTING */} - update('imageFile', file)} - /> +
+ + + {/* Viser valgt bilde */} + {data.imageFile && ( +
+ Forhåndsvisning av oppskriftsbilde + + +
+ )} +
+ {/* BESKRIVELSE */} + {/* VURDERING */} update('rating', value)} + onChange={(_, value) => + update('rating', value) + } /> - {/* Lenke til Instagram Reel eller TikTok */} + {/* VIDEO */} + {/* OPPSKRIFTSTITTEL */} - {/* Tid og porsjoner */} + {/* TID OG PORSJONER */}
- update('prepTime', Number(e.target.value)) + update( + 'prepTime', + Number(e.target.value), + ) } /> + min @@ -193,7 +271,10 @@ export default function RecipeStep1({ className="stepper-button" aria-label="Øk antall porsjoner" onClick={() => - update('servings', data.servings + 1) + update( + 'servings', + data.servings + 1, + ) } > + @@ -202,6 +283,7 @@ export default function RecipeStep1({
+ {/* KATEGORI */}
{categories.map((category) => ( @@ -235,7 +317,9 @@ export default function RecipeStep1({ {/* Åpner dialogen med flere tags */} setTagDialogOpen(true)} + onClick={() => + setTagDialogOpen(true) + } > Annet + @@ -256,7 +340,7 @@ export default function RecipeStep1({
)} - {/* Viser hvor mange tags som er valgt */} + {/* Antall valgte tags */}

{data.tags.length} av {MAX_TAGS} tags valgt

@@ -266,7 +350,10 @@ export default function RecipeStep1({ {/* KNAPPER */}
- @@ -283,8 +370,12 @@ export default function RecipeStep1({ update('tags', tags)} - onClose={() => setTagDialogOpen(false)} + onChange={(tags) => + update('tags', tags) + } + onClose={() => + setTagDialogOpen(false) + } />
) diff --git a/client/src/routes/NewRecipePage.tsx b/client/src/routes/NewRecipePage.tsx index 94305f6..7c477b1 100644 --- a/client/src/routes/NewRecipePage.tsx +++ b/client/src/routes/NewRecipePage.tsx @@ -1,9 +1,8 @@ -//For at frontend og backend kobler seg + import { useState } from 'react' import { useMutation } from '@apollo/client/react' import { CREATE_RECIPE } from '../graphql/mutations' -//Daten fra steps dukker opp her import RecipeStep1 from '../components/new-recipe/RecipeStep1' import RecipeStep2 from '../components/new-recipe/RecipeStep2' import RecipeStep3 from '../components/new-recipe/RecipeStep3' @@ -23,7 +22,7 @@ export type Instruction = { text: string } -// Definerer all informasjonen som lagres i oppskriftsskjemaet +// All informasjon som lagres i oppskriftsskjemaet export type RecipeFormData = { // Step 1 - Generell informasjon title: string @@ -57,7 +56,7 @@ export default function NewRecipePage({ currentStep, setCurrentStep, }: NewRecipePageProps) { - // Lagrer all informasjonen brukeren fyller inn + // Lagrer informasjonen brukeren fyller inn const [recipeData, setRecipeData] = useState({ // Step 1 title: '', @@ -94,11 +93,23 @@ export default function NewRecipePage({ ], }) - // Kobler komponenten til GraphQL-mutasjonen CREATE_RECIPE - const [createRecipe, { loading, error }] = useMutation(CREATE_RECIPE) + // Kobler frontend til GraphQL-mutasjonen + const [createRecipe, { loading, error }] = + useMutation(CREATE_RECIPE) + + // Holder styr på om bildet lastes opp + const [uploading, setUploading] = useState(false) + const [publishError, setPublishError] = useState( + null, + ) - // Funksjonen som kjøres når brukeren publiserer oppskriften + // 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) => ({ @@ -106,7 +117,7 @@ export default function NewRecipePage({ amount: Number(ingredient.amount), unit: ingredient.unit, order: index + 1, - }) + }), ) // Gjør fremgangsmåten om til formatet backend forventer @@ -124,7 +135,35 @@ export default function NewRecipePage({ ] try { - // Sender oppskriftsinformasjonen til GraphQL + // 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, + }, + ) + + if (!uploadResponse.ok) { + throw new Error('Kunne ikke laste opp bildet') + } + + // Backend returnerer adressen til bildet + 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: { @@ -133,6 +172,7 @@ export default function NewRecipePage({ prepTimeMinutes: recipeData.prepTime, servings: recipeData.servings, category: recipeData.category, + imageUrl, tagNames: recipeData.tags, ingredients, steps, @@ -140,13 +180,19 @@ export default function NewRecipePage({ }, }) - // Viser resultatet hvis oppskriften ble lagret console.log('Oppskrift publisert:', result.data) alert('Oppskriften ble publisert!') } catch (err) { - // Skriver ut feilen hvis publiseringen mislykkes console.error('Kunne ikke publisere oppskrift:', err) + + setPublishError( + err instanceof Error + ? err.message + : 'Noe gikk galt under publiseringen.', + ) + } finally { + setUploading(false) } } @@ -190,17 +236,16 @@ export default function NewRecipePage({ onPublish={handlePublish} /> - {/* Viser en melding mens oppskriften lagres */} - {loading && ( + {(uploading || loading) && (

Publiserer oppskriften...

)} - {/* Viser en feilmelding hvis publiseringen mislykkes */} - {error && ( + {(publishError || error) && (

- Kunne ikke publisere oppskriften: {error.message} + Kunne ikke publisere oppskriften:{' '} + {publishError || error?.message}

)} From 7e4f84fbcb3581c334303ff2abb61bfe3d737423 Mon Sep 17 00:00:00 2001 From: Isabella Hauan-Zamora Date: Thu, 15 Oct 2026 01:28:56 +0200 Subject: [PATCH 2/2] =?UTF-8?q?M=C3=A5tte=20l=C3=B8se=20konflikter=20:/?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/routes/RecipeListPage.tsx | 62 +++++++--------------------- 1 file changed, 16 insertions(+), 46 deletions(-) diff --git a/client/src/routes/RecipeListPage.tsx b/client/src/routes/RecipeListPage.tsx index ce1aad3..b82ea84 100644 --- a/client/src/routes/RecipeListPage.tsx +++ b/client/src/routes/RecipeListPage.tsx @@ -3,10 +3,6 @@ import { useState } from 'react' import { useSearchParams } from 'react-router' import { useQuery } from '@apollo/client/react' -<<<<<<< Updated upstream -// Listen over alle oppskrifter eller søksresultatet -// Selve søkefeltet ligger i headeren -======= import { RecipeList } from '../components/recipe/RecipeList' import { GET_RECIPES } from '../graphql/recipes' @@ -31,49 +27,39 @@ function getImageUrl(imageUrl: string | null | undefined) { } // Listen over alle oppskrifter eller søkeresultater ->>>>>>> Stashed changes export default function RecipeListPage() { const [searchParams] = useSearchParams() const query = searchParams.get('q') ?? '' const { data, loading, error, fetchMore } = useQuery(GET_RECIPES, { -<<<<<<< Updated upstream - variables: { search: query || undefined, page: 1, pageSize: PAGE_SIZE }, - }); -======= variables: { + search: query || undefined, page: 1, pageSize: PAGE_SIZE, }, }) ->>>>>>> Stashed changes const [hasMore, setHasMore] = useState(true) const [loadingMore, setLoadingMore] = useState(false) - // nullstiller når man søker på noe nytt - const [prevQuery, setPrevQuery] = useState(query); + // Nullstiller paginering når man søker på noe nytt + const [prevQuery, setPrevQuery] = useState(query) + if (query !== prevQuery) { - setPrevQuery(query); - setHasMore(true); + setPrevQuery(query) + setHasMore(true) } -<<<<<<< Updated upstream if (loading && !data) { - return

Laster oppskrifter...

; + return

Laster oppskrifter...

} - if (error) { - return ( -

Kunne ikke hente oppskriftene. Prøv igjen senere.

- ); -======= + if (error) { return (

Kunne ikke hente oppskriftene. Prøv igjen senere.

) ->>>>>>> Stashed changes } // Henter oppskriftene fra GraphQL @@ -87,29 +73,16 @@ export default function RecipeListPage() { // Henter flere oppskrifter når brukeren trykker "Last flere" async function handleLoadMore() { -<<<<<<< Updated upstream - // Regner ut hvilke side som er neste. - const nextPage = Math.floor(recipes.length / PAGE_SIZE) + 1; - - // Henter neste side - const result = await fetchMore({ - variables: { page: nextPage }, - // Legger de nye oppskriftene etter de gamle - updateQuery: (previous, { fetchMoreResult }) => ({ - recipes: [...previous.recipes, ...fetchMoreResult.recipes], - }), - }); -======= if (loadingMore || !hasMore) return setLoadingMore(true) ->>>>>>> Stashed changes try { const nextPage = Math.floor(recipes.length / PAGE_SIZE) + 1 const result = await fetchMore({ variables: { + search: query || undefined, page: nextPage, pageSize: PAGE_SIZE, }, @@ -141,21 +114,18 @@ export default function RecipeListPage() { Oppskrifter -<<<<<<< Updated upstream - {query && recipes.length > 0 &&

Alt med '{query}'

} + {query && recipes.length > 0 && ( +

+ Alt med «{query}» +

+ )} - {/* melding hvis søket ikke finner noe */} + {/* Melding hvis søket ikke finner noe */} {query && recipes.length === 0 && ( -

Fant ingen oppskrifter med '{query}'.

- )} - -======= - {query && (

- Alt med «{query}» + Fant ingen oppskrifter med «{query}».

)} ->>>>>>> Stashed changes {/* Viser oppskriftene med riktige bildeadresser */}