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
7 changes: 3 additions & 4 deletions client/src/components/new-recipe/RecipeStep1.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ type RecipeStep1Props = {
onNext: () => void
}


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

const dietaryTags = ['Vegetar', 'Vegansk', 'Pescetar', 'Glutenfri', 'Laktosefri', 'Annet +']
Expand Down Expand Up @@ -102,10 +103,8 @@ export default function RecipeStep1({ data, setData, onNext }: RecipeStep1Props)
</Field>

<Field label="Porsjoner" required>
<div className="flex h-[38px] items-center gap-3">
<button
type="button"
className="stepper-button"
<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"
aria-label="Reduser antall porsjoner"
onClick={() => update('servings', Math.max(1, data.servings - 1))}
>
Expand Down
8 changes: 4 additions & 4 deletions client/src/components/new-recipe/RecipeStep2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import type { Dispatch, SetStateAction } from 'react'
import type { RecipeFormData } from '../../routes/NewRecipePage'
import { Button } from '../ui/Button'
import { Field } from '../ui/Field'
import { Input, Select, Textarea } from '../ui/Input'
import { Input, Select} from '../ui/Input'
import ArrowBackIcon from '@mui/icons-material/ArrowBack'
import DeleteIcon from '@mui/icons-material/Delete'
import DeleteIcon from '@mui/icons-material/DeleteOutlined'
import AddIcon from '@mui/icons-material/Add'
import DragIcon from '@mui/icons-material/DragIndicator'
import InfoIcon from '@mui/icons-material/InfoOutlined'
Expand Down Expand Up @@ -191,7 +191,7 @@ export default function RecipeStep2({ data, setData, onBack, onNext }: RecipeSte
<button
type="button"
aria-label="Reduser antall porsjoner"
className={`h-8 w-8 rounded-small bg-peach/40 hover:bg-peach ${focusRing}`}
className="stepper-button"
onClick={() => setData((prev) => ({ ...prev, servings: Math.max(1, prev.servings - 1) }))}
>
−
Expand All @@ -200,7 +200,7 @@ export default function RecipeStep2({ data, setData, onBack, onNext }: RecipeSte
<button
type="button"
aria-label="Øk antall porsjoner"
className={`h-8 w-8 rounded-small bg-peach/40 hover:bg-peach ${focusRing}`}
className="stepper-button"
onClick={() => setData((prev) => ({ ...prev, servings: prev.servings + 1 }))}
>
+
Expand Down
233 changes: 203 additions & 30 deletions client/src/components/new-recipe/RecipeStep4.tsx
Original file line number Diff line number Diff line change
@@ -1,49 +1,222 @@
import { useState } from 'react'
import type { Dispatch, ReactNode, SetStateAction } from 'react'
import Rating from '@mui/material/Rating'
import type { RecipeFormData } from '../../routes/NewRecipePage'
import { Button } from '../ui/Button'
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import ArrowBackIcon from '@mui/icons-material/ArrowBack'
import AccessTimeIcon from '@mui/icons-material/AccessTime'
import CheckIcon from '@mui/icons-material/Check'
import NorthEastIcon from '@mui/icons-material/NorthEast'
import RestaurantIcon from '@mui/icons-material/Restaurant'

type RecipeStep4Props = {
data: RecipeFormData
onBack: () => void
onPublish: () => void
}

export default function RecipeStep4({
onBack,
}: RecipeStep4Props) {
// Felles utseende for de fire boksene
const card = 'rounded-medium border border-peach bg-background p-5'

// Liten avrundet merkelapp (kategori, kosthold, "Under 30 min")
function Pill({ children, className }: { children: ReactNode; className: string }) {
return (
<span className={`rounded-full px-2.5 py-0.5 text-xs font-semibold ${className}`}>
{children}
</span>
)
}

// Legger til id-en i listen hvis den ikke er der, fjerner den hvis den er der
const toggleId = (setter: Dispatch<SetStateAction<number[]>>, id: number) =>
setter((prev) => (prev.includes(id) ? prev.filter((i) => i !== id) : [...prev, id]))

export default function RecipeStep4({ data, onBack, onPublish }: RecipeStep4Props) {
// Kun for forhåndsvisningen: porsjoner og avkrysninger lagres ikke i oppskriften
const [servings, setServings] = useState(data.servings)
const [checkedIngredients, setCheckedIngredients] = useState<number[]>([])
const [doneSteps, setDoneSteps] = useState<number[]>([])

// Regner om mengden når man endrer porsjoner i forhåndsvisningen
const scale = (amount: string) => {
const value = Number(amount)
if (!amount || Number.isNaN(value)) return amount
return String(Math.round(((value * servings) / data.servings) * 10) / 10)
}

const ingredients = data.ingredients.filter((item) => item.name.trim())

// Gjør begge måter å skrive fremgangsmåte på om til samme liste
const instructions =
data.instructionMode === 'steps'
? data.steps
.filter((step) => step.text.trim())
.map((step) => ({ id: step.id, text: step.text, minutes: step.minutes }))
: data.instructionText
.split('\n')
.map((line) => line.replace(/^\s*\d+[.)]\s*/, '').trim()) // fjerner "1." foran
.filter(Boolean)
.map((text, index) => ({ id: index, text, minutes: '' }))

return (
<div className="w-full">
<div className="grid grid-cols-1 gap-4 lg:grid-cols-[2fr_3fr]">
{/* OPPSUMMERING */}
<section className={card}>
<div className="flex items-start justify-between gap-3">
<div className="flex flex-wrap gap-2">
{data.category && <Pill className="bg-olive/20 text-text">{data.category}</Pill>}
{data.tags.map((tag) => (
<Pill key={tag} className="bg-pink-light text-text">
{tag}
</Pill>
))}
{data.prepTime <= 30 && (
<Pill className="border border-peach text-text">Under 30 min</Pill>
)}
</div>

{data.rating ? (
<div className="flex shrink-0 items-center gap-1 text-xs font-semibold text-olive">
<Rating value={data.rating} readOnly size="small" sx={{ color: 'inherit' }} />
{data.rating.toFixed(1)}
</div>
) : null}
</div>

<h3 className="mt-4 font-heading text-2xl text-text">{data.title || 'Uten tittel'}</h3>
<p className="mt-2 text-sm text-olive">{data.description}</p>

<div className="mt-4 flex items-center justify-between text-sm">
<span className="flex items-center gap-2 font-semibold">
<AccessTimeIcon fontSize="small" />
{data.prepTime} min · {data.servings} porsjoner
</span>

{/* Midlertidig innhold */}
<div className="min-h-80">
<h2 className="font-heading text-3xl text-text">
Publiser
</h2>
{data.videoUrl && (
<a
href={data.videoUrl}
target="_blank"
rel="noreferrer"
className="flex items-center gap-1 font-semibold text-primary hover:underline"
>
Se video <NorthEastIcon sx={{ fontSize: 14 }} />
</a>
)}
</div>
</section>

<p className="mt-2 text-sm text-olive">
Step 4 – forhåndsvisning av oppskriften kommer her.
</p>
{/* BILDE */}
<section className="flex min-h-48 flex-col items-center justify-center gap-3 rounded-medium bg-peach/60">
<span className="flex h-12 w-12 items-center justify-center rounded-full bg-background">
<RestaurantIcon />
</span>
<span className="text-sm font-semibold">Bildeforhåndsvisning</span>
</section>

{/* INGREDIENSER */}
<section className={card}>
<div className="flex items-center justify-between">
<h3 className="font-heading text-xl text-text">Ingredienser</h3>

<div className="inline-flex items-center gap-3 rounded-medium border border-peach p-1 text-sm">
<button
type="button"
aria-label="Reduser antall porsjoner"
className="stepper-button"
onClick={() => setServings((value) => Math.max(1, value - 1))}
>
−
</button>
<span className="min-w-4 text-center font-semibold">{servings}</span>
<button
type="button"
aria-label="Øk antall porsjoner"
className="stepper-button"
onClick={() => setServings((value) => value + 1)}
>
+
</button>
</div>
</div>

<ul className="mt-4 max-h-44 space-y-2 overflow-y-auto pr-2 text-sm">
{ingredients.length === 0 && <li className="text-olive">Ingen ingredienser lagt til.</li>}
{ingredients.map((item) => (
<li key={item.id}>
<label className="flex cursor-pointer items-center gap-3">
<input
type="checkbox"
className="accent-primary"
checked={checkedIngredients.includes(item.id)}
onChange={() => toggleId(setCheckedIngredients, item.id)}
/>
<span className={checkedIngredients.includes(item.id) ? 'text-olive line-through' : ''}>
{item.amount && `${scale(item.amount)} ${item.unit} `}
{item.name}
</span>
</label>
</li>
))}
</ul>

{/* TODO: koble til handlelisten når den finnes */}
<div className="mt-4 border-t border-peach pt-3">
<button type="button" className="cursor-pointer text-xs font-semibold text-primary hover:underline">
Legg til i handleliste +
</button>
</div>
</section>

{/* FREMGANGSMÅTE */}
<section className={card}>
<h3 className="font-heading text-xl text-text">Fremgangsmåte</h3>

<ol className="mt-4 max-h-56 space-y-3 overflow-y-auto pr-2 text-sm">
{instructions.length === 0 && <li className="text-olive">Ingen fremgangsmåte lagt til.</li>}
{instructions.map((step, index) => (
<li key={step.id} className="flex items-start gap-3">
<span className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-peach/50 text-xs font-semibold">
{index + 1}
</span>

<p className={`flex-1 ${doneSteps.includes(step.id) ? 'text-olive line-through' : ''}`}>
{step.text}
{step.minutes && <span className="ml-2 text-xs text-olive">({step.minutes} min)</span>}
</p>

<label className="flex shrink-0 cursor-pointer items-center gap-1 text-xs text-olive">
<input
type="checkbox"
className="accent-primary"
checked={doneSteps.includes(step.id)}
onChange={() => toggleId(setDoneSteps, step.id)}
/>
Fullført
</label>
</li>
))}
</ol>
</section>
</div>

{/* Knapper */}
<div className="mt-4 flex items-center justify-end gap-5 border-t border-peach pt-4">
<Button
variant="ghost" size="small" onClick={onBack}
className="mr-auto"
>
<ArrowBackIcon fontSize="small" />
<div className="mt-5 flex flex-wrap items-center justify-between gap-3 border-t border-peach pt-4">
<Button variant="ghost" size="small" onClick={onBack}>
<ArrowBackIcon fontSize="small" />
Tilbake
</Button>

<Button variant="ghost" size="small">
Lagre utkast
</Button>

<Button
variant="primary"
size="small"
>
Publiser oppskrift
</Button>
<div className="flex gap-3">
<Button variant="ghost" size="small">
Lagre utkast
</Button>
<Button variant="primary" size="small" onClick={onPublish}>
Publiser oppskrift
<CheckIcon fontSize="small" />
</Button>
</div>
</div>

</div>
)
}
}
17 changes: 10 additions & 7 deletions client/src/components/ui/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@ type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
size?: ButtonSize
}

// Felles styling
// Felles styling for alle knappene
const baseStyles = [
'font-body',
'rounded-medium',
'transition-colors',
'cursor-pointer',
].join(' ')

// Fokus og deaktivert tilstand
// Felles styling for tastaturfokus og deaktiverte knapper
const sharedStyles = [
'focus-visible:outline-2',
'focus-visible:outline-offset-2',
Expand Down Expand Up @@ -63,15 +63,17 @@ const sizeStyles: Record<ButtonSize, string> = {
large: 'px-6 py-4 text-lg',
}

// Gjenbrukbar knapp som kan tilpasses med variant, størrelse og andre props
export function Button({
children,
variant = 'primary',
size = 'medium',
className = '',
type = 'button',
variant = 'primary', // Bruker primary hvis ingen variant er valgt
size = 'medium', // Bruker medium hvis ingen størrelse er valgt
className = '', // Gjør det mulig å legge til ekstra styling
type = 'button', // Hindrer at knappen automatisk sender inn et skjema
...props
}: ButtonProps) {
// Kombinerer styling

// Kombinerer felles styling, variant, størrelse og eventuell ekstra styling
const classes = [
baseStyles,
sharedStyles,
Expand All @@ -83,6 +85,7 @@ export function Button({
.join(' ')

return (
// Sender videre vanlige HTML-egenskaper som onClick, disabled og aria-label
<button
type={type}
className={classes}
Expand Down
Loading