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
64 changes: 64 additions & 0 deletions client/src/components/new-recipe/RecipeImageUpload.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@

import { useEffect, useState } from 'react'
import type { ChangeEvent } from 'react'

export function RecipeImageUpload() {
const [image, setImage] = useState<File | null>(null)
const [preview, setPreview] = useState<string | null>(null)

useEffect(() => {
if (!image) {
setPreview(null)
return
}

const url = URL.createObjectURL(image)
setPreview(url)

return () => URL.revokeObjectURL(url)
}, [image])

const handleImageChange = (event: ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0]
if (!file) return

if (!['image/jpeg', 'image/png', 'image/webp'].includes(file.type)) {
alert('Velg et JPG-, PNG- eller WebP-bilde.')
return
}

if (file.size > 5 * 1024 * 1024) {
alert('Bildet kan ikke være større enn 5 MB.')
return
}

setImage(file)
}

return (
<div>
<label
htmlFor="recipe-image"
className="flex min-h-64 cursor-pointer flex-col items-center justify-center overflow-hidden rounded-medium border-2 border-dashed border-peach"
>
{preview ? (
<img
src={preview}
alt="Forhåndsvisning av oppskriftsbilde"
className="h-64 w-full object-cover"
/>
) : (
<span>+ Velg bilde til oppskriften</span>
)}
</label>

<input
id="recipe-image"
type="file"
accept="image/jpeg,image/png,image/webp"
className="sr-only"
onChange={handleImageChange}
/>
</div>
)
}
194 changes: 160 additions & 34 deletions client/src/components/new-recipe/RecipeStep1.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,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
Expand All @@ -13,45 +17,87 @@ type RecipeStep1Props = {
onNext: () => void
}

const MAX_TAGS = 6

const categories = [
'Frokost',
'Lunsj',
'Middag',
'Dessert',
'Snacks',
'Drikke',
]

const categories = ['Frokost', 'Lunsj', 'Middag', 'Dessert', 'Snacks', 'Drikke']
const dietaryTags = [
'Vegetar',
'Vegansk',
'Pescetar',
'Glutenfri',
'Laktosefri',
'Under 30 min',
]

const dietaryTags = ['Vegetar', 'Vegansk', 'Pescetar', 'Glutenfri', 'Laktosefri', 'Annet +']
export default function RecipeStep1({
data,
setData,
onNext,
}: RecipeStep1Props) {
const [tagDialogOpen, setTagDialogOpen] = useState(false)

export default function RecipeStep1({ data, setData, onNext }: RecipeStep1Props) {
// Oppdaterer ETT felt i oppskriften, f.eks. update('title', 'Pasta').
// Erstatter alle setData({ ...data, felt: verdi }) og sørger for at resten beholdes.
const update = <K extends keyof RecipeFormData>(key: K, value: RecipeFormData[K]) =>
// Oppdaterer ett felt i oppskriften og beholder resten
const update = <K extends keyof RecipeFormData>(
key: K,
value: RecipeFormData[K],
) => {
setData((prev) => ({ ...prev, [key]: value }))
}

// Klikk på en valgt tag fjerner den, klikk på en ikke-valgt tag legger den til
const toggleTag = (tag: string) =>
update(
'tags',
data.tags.includes(tag) ? data.tags.filter((item) => item !== tag) : [...data.tags, tag],
)
// Legger til eller fjerner en tag (maks 6 totalt)
const toggleTag = (tag: string) => {
if (data.tags.includes(tag)) {
update(
'tags',
data.tags.filter((item) => item !== tag),
)
} else if (data.tags.length < MAX_TAGS) {
update('tags', [...data.tags, tag])
}
}

// Tags som er valgt fra dialogen
const extraTags = data.tags.filter(
(tag) => !dietaryTags.includes(tag),
)

return (
<div className="w-full">
<p className="mb-3 text-sm text-olive">Lag din egen oppskrift og del den med andre.</p>
<p className="mb-3 text-sm text-olive">
Lag din egen oppskrift og del den med andre.
</p>

<div className="grid gap-4 md:grid-cols-2">
{/* VENSTRE SIDE */}
<div className="space-y-2">
<Field label="Bilde">
<button type="button" className="upload-button">
<span className="text-sm font-semibold text-primary">Last opp bilde</span>
<span className="mt-1 text-xs text-olive">PNG, JPG eller WebP</span>
</button>
<ImageUpload
value={data.imageFile}
onChange={(file) => update('imageFile', file)}
/>
</Field>

<Field label="Kort beskrivelse" htmlFor="description" required>
<Field
label="Kort beskrivelse"
htmlFor="description"
required
>
<Textarea
id="description"
rows={2}
placeholder="Skriv noen setninger om oppskriften..."
value={data.description}
onChange={(e) => update('description', e.target.value)}
onChange={(e) =>
update('description', e.target.value)
}
/>
</Field>

Expand All @@ -65,59 +111,90 @@ export default function RecipeStep1({ data, setData, onNext }: RecipeStep1Props)
/>
</Field>

<Field label="Instagram Reel / TikTok (valgfritt)" htmlFor="video-url">
{/* Lenke til Instagram Reel eller TikTok */}
<Field
label="Instagram Reel / TikTok (valgfritt)"
htmlFor="video-url"
>
<Input
id="video-url"
type="url"
placeholder="Lim inn lenke til video..."
value={data.videoUrl}
onChange={(e) => update('videoUrl', e.target.value)}
onChange={(e) =>
update('videoUrl', e.target.value)
}
/>
</Field>
</div>

{/* HØYRE SIDE */}
<div className="space-y-3">
<Field label="Navn på oppskrift" htmlFor="recipe-title" required>
<Field
label="Navn på oppskrift"
htmlFor="recipe-title"
required
>
<Input
id="recipe-title"
placeholder="F.eks. kremet pasta med sopp"
value={data.title}
onChange={(e) => update('title', e.target.value)}
onChange={(e) =>
update('title', e.target.value)
}
/>
</Field>

{/* Tid og porsjoner */}
<div className="grid grid-cols-2 gap-4">
<Field label="Tilberedningstid" htmlFor="prep-time" required>
<Field
label="Tilberedningstid"
htmlFor="prep-time"
required
>
<div className="flex items-center gap-2">
<Input
id="prep-time"
type="number"
min={1}
value={data.prepTime}
onChange={(e) => update('prepTime', Number(e.target.value))}
onChange={(e) =>
update('prepTime', Number(e.target.value))
}
/>
<span className="text-sm text-olive">min</span>
<span className="text-sm text-olive">
min
</span>
</div>
</Field>

<Field label="Porsjoner" required>
<div className="flex h-[38px] items-center inline-flex items-center gap-4 rounded-medium border border-peach p-1">
<button type="button" className="stepper-button"
<div className="inline-flex h-[38px] items-center gap-4 rounded-medium border border-peach p-1">
<button
type="button"
className="stepper-button"
aria-label="Reduser antall porsjoner"
onClick={() => update('servings', Math.max(1, data.servings - 1))}
onClick={() =>
update(
'servings',
Math.max(1, data.servings - 1),
)
}
>
−
</button>

<span className="min-w-5 text-center font-semibold">{data.servings}</span>
<span className="min-w-5 text-center font-semibold">
{data.servings}
</span>

<button
type="button"
className="stepper-button"
aria-label="Øk antall porsjoner"
onClick={() => update('servings', data.servings + 1)}
onClick={() =>
update('servings', data.servings + 1)
}
>
+
</button>
Expand All @@ -131,22 +208,58 @@ export default function RecipeStep1({ data, setData, onNext }: RecipeStep1Props)
<Tag
key={category}
selected={data.category === category}
onClick={() => update('category', category)}
onClick={() =>
update('category', category)
}
>
{category}
</Tag>
))}
</div>
</Field>

{/* KOSTHOLD OG TAGS */}
<Field label="Kosthold">
<div className="flex flex-wrap gap-2">
{/* Vanlige tags */}
{dietaryTags.map((tag) => (
<Tag key={tag} selected={data.tags.includes(tag)} onClick={() => toggleTag(tag)}>
<Tag
key={tag}
selected={data.tags.includes(tag)}
onClick={() => toggleTag(tag)}
>
{tag}
</Tag>
))}

{/* Åpner dialogen med flere tags */}
<Tag
selected={false}
onClick={() => setTagDialogOpen(true)}
>
Annet +
</Tag>
</div>

{/* Ekstra tags valgt i dialogen */}
{extraTags.length > 0 && (
<div className="mt-3 flex flex-wrap gap-2">
{extraTags.map((tag) => (
<Tag
key={tag}
selected
onClick={() => toggleTag(tag)}
>
{tag} ×
</Tag>
))}
</div>
)}

{/* Viser hvor mange tags som er valgt */}
<p className="mt-2 text-xs text-olive">
{data.tags.length} av {MAX_TAGS} tags valgt
</p>
</Field>
</div>
</div>
Expand All @@ -156,10 +269,23 @@ export default function RecipeStep1({ data, setData, onNext }: RecipeStep1Props)
<Button variant="ghost" size="small">
Lagre utkast
</Button>
<Button variant="primary" size="small" onClick={onNext}>

<Button
variant="primary"
size="small"
onClick={onNext}
>
Neste: Ingredienser
</Button>
</div>

{/* DIALOG MED FLERE TAGS */}
<TagDialog
open={tagDialogOpen}
selectedTags={data.tags}
onChange={(tags) => update('tags', tags)}
onClose={() => setTagDialogOpen(false)}
/>
</div>
)
}
Loading