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
452 changes: 196 additions & 256 deletions client/src/components/new-recipe/RecipeStep1.tsx

Large diffs are not rendered by default.

331 changes: 90 additions & 241 deletions client/src/components/new-recipe/RecipeStep4.tsx

Large diffs are not rendered by default.

188 changes: 104 additions & 84 deletions client/src/components/new-recipe/TagDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,10 @@ type TagDialogProps = {
}

const allTags = Object.values(tagGroups).flat()
const categories = ['Alle', ...Object.keys(tagGroups)]
const categories = Object.keys(tagGroups)

// Utseende for alle pillene (kategorier, valgte tags og tags i listen)
const pill = (selected: boolean, disabled = false) =>
`rounded-full border font-semibold transition-colors ${
const tagPill = (selected: boolean, disabled = false) =>
`rounded-full border px-3 py-1.5 text-sm font-semibold transition-colors ${
selected
? 'cursor-pointer border-primary bg-primary text-white hover:opacity-90'
: disabled
Expand All @@ -25,24 +24,26 @@ const pill = (selected: boolean, disabled = false) =>

export default function TagDialog({ open, selectedTags, onChange, onClose }: TagDialogProps) {
const [search, setSearch] = useState('')
const [category, setCategory] = useState('Alle')

const [category, setCategory] = useState<string>(categories[0] ?? '')
const titleId = useId()
const dialogRef = useRef<HTMLDivElement>(null)
const closeButtonRef = useRef<HTMLButtonElement>(null)

// Lukker med Escape, holder fokus i dialogen og låser scrolling bak
// Hold tastaturfokus inne i dialogen og gjenopprett det ved lukking.
useEffect(() => {
if (!open) return

const previousFocus = document.activeElement as HTMLElement | null
const previousOverflow = document.body.style.overflow

document.body.style.overflow = 'hidden'
closeButtonRef.current?.focus()

const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') return onClose()
if (event.key === 'Escape') {
event.preventDefault()
onClose()
return
}
if (event.key !== 'Tab') return

const items = dialogRef.current?.querySelectorAll<HTMLElement>(
Expand All @@ -52,7 +53,6 @@ export default function TagDialog({ open, selectedTags, onChange, onClose }: Tag

const first = items[0]
const last = items[items.length - 1]

if (event.shiftKey && document.activeElement === first) {
event.preventDefault()
last.focus()
Expand All @@ -63,7 +63,6 @@ export default function TagDialog({ open, selectedTags, onChange, onClose }: Tag
}

window.addEventListener('keydown', handleKeyDown)

return () => {
window.removeEventListener('keydown', handleKeyDown)
document.body.style.overflow = previousOverflow
Expand All @@ -73,42 +72,46 @@ export default function TagDialog({ open, selectedTags, onChange, onClose }: Tag

if (!open) return null

// Filtrerer tags etter kategori og søketekst
const filteredTags = (category === 'Alle' ? allTags : tagGroups[category]).filter((tag) =>
tag.toLowerCase().includes(search.trim().toLowerCase()),
// Søk i alle tags, uavhengig av valgt kategori.
const query = search.trim().toLowerCase()
const categoryTags = tagGroups[category as keyof typeof tagGroups] ?? []
const filteredTags = (query ? allTags : categoryTags).filter((tag) =>
tag.toLowerCase().includes(query),
)

const isFull = selectedTags.length >= MAX_TAGS

// Legger til eller fjerner en tag (maks MAX_TAGS totalt)
const toggleTag = (tag: string) => {
if (selectedTags.includes(tag)) onChange(selectedTags.filter((item) => item !== tag))
else if (!isFull) onChange([...selectedTags, tag])
if (selectedTags.includes(tag)) {
onChange(selectedTags.filter((item) => item !== tag))
} else if (!isFull) {
onChange([...selectedTags, tag])
}
}

return (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4"
onMouseDown={(event) => event.target === event.currentTarget && onClose()}
onMouseDown={(event) => {
if (event.target === event.currentTarget) onClose()
}}
>
<div
ref={dialogRef}
role="dialog"
aria-modal="true"
aria-labelledby={titleId}
className="flex max-h-[90vh] w-full max-w-3xl flex-col gap-4 overflow-hidden rounded-medium border border-peach bg-background p-5 shadow-xl sm:p-6"
className="flex h-[700px] max-h-[90vh] w-full max-w-3xl flex-col gap-4 overflow-hidden rounded-medium border border-peach bg-background p-5 shadow-xl sm:p-6"
>
{/* OVERSKRIFT */}
{/* Overskrift */}
<div className="flex shrink-0 items-start justify-between gap-3">
<div>
<h2 id={titleId} className="font-heading text-2xl text-text">
Velg flere tags
</h2>
<p className="mt-1 text-sm text-olive">
Finn tags som passer oppskriften din. Du kan velge opptil {MAX_TAGS}.
Velg opptil {MAX_TAGS} tags som passer oppskriften din.
</p>
</div>

<button
ref={closeButtonRef}
type="button"
Expand All @@ -120,7 +123,7 @@ export default function TagDialog({ open, selectedTags, onChange, onClose }: Tag
</button>
</div>

{/* SØKEFELT */}
{/* Søkefelt */}
<div className="flex shrink-0 items-center gap-2 rounded-medium border border-peach px-3 py-2">
<SearchIcon fontSize="small" className="text-olive" />
<input
Expand All @@ -133,82 +136,99 @@ export default function TagDialog({ open, selectedTags, onChange, onClose }: Tag
/>
</div>

{/* KATEGORIER */}
<div className="flex shrink-0 flex-wrap gap-2">
{categories.map((item) => (
<button
key={item}
type="button"
onClick={() => setCategory(item)}
aria-pressed={category === item}
className={`${pill(category === item)} px-3 py-1.5 text-xs`}
>
{item}
</button>
))}
{/* Kategorifiltre */}
<div className="shrink-0">
<p className="mb-3 text-xs font-semibold uppercase text-olive">
Filtrer etter kategori
</p>
<div className="flex flex-wrap gap-2">
{categories.map((item) => (
<button
key={item}
type="button"
onClick={() => setCategory(item)}
aria-pressed={category === item}
className={`cursor-pointer rounded-medium border px-3 py-1.5 text-xs font-semibold transition-colors ${
category === item
? 'border-primary bg-primary text-white hover:opacity-90'
: 'border-peach text-text hover:bg-peach/30'
}`}
>
{item}
</button>
))}
</div>
</div>

{/* VALGTE TAGS – KOMPAKT VISNING */}
{selectedTags.length > 0 && (
<div className="flex shrink-0 items-center gap-3 border-b border-peach pb-3">
<span className="shrink-0 text-xs font-semibold text-olive">Valgt:</span>

<div className="custom-scrollbar flex min-w-0 flex-1 gap-2 overflow-x-auto pb-1">
{selectedTags.map((tag) => (
{/* Alltid synlig. Fast høyde hindrer at resten av dialogen hopper. */}
<div className="shrink-0">
<div className="mb-2 flex items-center justify-between">
<span className="text-xs font-semibold uppercase text-olive">
Valgte tags
</span>

<span className="text-xs font-semibold text-olive">
{selectedTags.length} / {MAX_TAGS} valgt
</span>
</div>
{isFull && (
<p className="mb-[4px] text-xs text-olive">
Fjern en tag for å velge en annen.
</p>
)}
<div className="custom-scrollbar flex h-[35px] flex-wrap content-start items-start gap-2 overflow-y-auto">
{selectedTags.length === 0 ? (
<span className="py-1 text-sm text-olive/70">Ingen tags valgt ennå</span>
) : (
selectedTags.map((tag) => (
<button
key={tag}
type="button"
onClick={() => toggleTag(tag)}
aria-label={`Fjern ${tag}`}
className={`${pill(true)} shrink-0 px-2.5 py-1 text-xs`}
className="cursor-pointer rounded-full border border-peach bg-peach/40 px-3 py-1.5 text-xs font-semibold text-text transition-colors hover:bg-peach/70"
>
{tag} ×
</button>
))}
</div>
</div>
)}

{/* ALLE TAGS – SCROLLBART OMRÅDE */}
<div className="custom-scrollbar min-h-0 flex-1 overflow-y-auto overscroll-contain pr-2">
<div className="flex flex-wrap gap-2">
{filteredTags.map((tag) => {
const selected = selectedTags.includes(tag)
const disabled = !selected && isFull

return (
<button
key={tag}
type="button"
disabled={disabled}
aria-pressed={selected}
onClick={() => toggleTag(tag)}
className={`${pill(selected, disabled)} px-3 py-1.5 text-sm`}
>
{selected && '✓ '}
{tag}
</button>
)
})}

{filteredTags.length === 0 && (
<p className="py-4 text-sm text-olive">Ingen tags funnet. Prøv et annet søkeord.</p>
))
)}
</div>
</div>

{/* BUNN */}
<div className="flex shrink-0 items-center justify-between gap-3 border-t border-peach pt-4">
<div>
<p className="text-sm font-semibold text-olive">
{selectedTags.length} / {MAX_TAGS} valgt
</p>
{isFull && <p className="mt-1 text-xs text-olive">Fjern en tag for å velge en annen.</p>}
{/* Valgbare tags */}
<div className="flex min-h-0 flex-1 flex-col border-t border-peach pt-4">
<div className="mb-2 flex shrink-0 items-center justify-between">
<h3 className="text-sm font-semibold text-text">
{query ? 'Søkeresultater' : category}
</h3>
</div>
<div className="custom-scrollbar min-h-0 flex-1 overflow-y-auto overscroll-contain pr-2">
<div className="flex flex-wrap gap-2">
{filteredTags.map((tag) => {
const selected = selectedTags.includes(tag)
const disabled = !selected && isFull
return (
<button key={tag} type="button" disabled={disabled}
aria-pressed={selected}
onClick={() => toggleTag(tag)}
className={tagPill(selected, disabled)}
>
{tag}
</button>
)
})}
{filteredTags.length === 0 && (
<p className="py-4 text-sm text-olive">
Ingen tags funnet. Prøv et annet søkeord.
</p>
)}
</div>
</div>
</div>

<button
type="button"
onClick={onClose}
{/* Bunn */}
<div className="flex shrink-0 items-center justify-between gap-3 border-t border-peach pt-4">
<button type="button" onClick={onClose}
className="cursor-pointer rounded-medium bg-primary px-5 py-2 text-sm font-semibold text-white hover:opacity-90"
>
Ferdig
Expand Down
23 changes: 8 additions & 15 deletions client/src/components/ui/Tag.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,32 @@
import type { ButtonHTMLAttributes, ReactNode } from 'react'

// Props for Tag-komponenten
type TagProps = ButtonHTMLAttributes<HTMLButtonElement> & {
children: ReactNode
selected?: boolean
}

export function Tag({
children,
selected = false,
className = '',
...props
selected = false, // Ikke valgt som standard
className = '', // Ekstra styling
...props // Andre button-attributter
}: TagProps) {
return (
<button
type="button"
aria-pressed={selected}
aria-pressed={selected} // Valgt-status for skjermleser
className={`
relative
inline-flex items-center justify-center
min-w-[100px]
min-w-[80px]
rounded-full
px-5 py-2
px-4 py-2
font-body text-sm font-medium
transition-colors cursor-pointer
${
// Styling for valgt / ikke valgt
selected
? 'bg-primary text-background'
: 'bg-background text-text border border-peach hover:bg-secondary'
Expand All @@ -41,15 +43,6 @@ export function Tag({
`}
{...props}
>
{selected && (
<span
aria-hidden="true"
className="absolute left-3"
>
✓
</span>
)}

{children}
</button>
)
Expand Down
6 changes: 3 additions & 3 deletions client/src/lib/tags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,18 @@ export const tagGroups: Record<string, string[]> = {
'Røkt', 'Frisk', 'Mettende', 'Lett', 'Saftig', 'Umami',
],
Matkultur: [
'Norsk', 'Nordisk', 'Italiensk', 'Meksikansk', 'Tex-Mex', 'Amerikansk',
'Norsk', 'Nordisk', 'Italiensk', 'Meksikansk', 'Amerikansk',
'Indisk', 'Japansk', 'Koreansk', 'Thai', 'Kinesisk', 'Vietnamesisk',
'Gresk', 'Fransk', 'Spansk', 'Tyrkisk', 'Libanesisk', 'Marokkansk',
'Middelhavsmat',
],
Anledning: [
'Studentvennlig', 'Budsjettvennlig', 'Barnevennlig', 'Festmat', 'Hverdagsmat', 'Helgekos',
'Fredagskos', 'Matpakke', 'Selskap', 'Gjester', 'Date night', 'Bursdag',
'Matpakke', 'Selskap','Date night', 'Bursdag',
'Brunsj', 'Piknik', 'Restemat', 'Turmat',
],
Sesong: [
'Vårmat', 'Sommermat', 'Høstmat', 'Vintermat', 'Julemat', 'Påskemat',
'Nyttårsaften', '17. mai', 'Sankthans', 'Halloween', 'Grillsesong', 'Sesongvarer',
'Nyttårsaften', '17. mai', 'Halloween', 'Grillsesong',
],
}
Loading