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
24 changes: 24 additions & 0 deletions frontend/src/components/button/Button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { ButtonHTMLAttributes } from "react";
import "./Button.css";

// Fra Figma: Components → Button. Alle vanlige button-props sendes videre.
type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
variant?: "primary" | "secondary" | "dark" | "tinted";
size?: "s" | "m" | "l";
};

export default function Button({
variant = "primary",
size = "m",
type = "button",
className = "",
...rest
}: ButtonProps) {
return (
<button
type={type}
className={`button button--${variant} button--${size} ${className}`.trim()}
{...rest}
/>
);
}
42 changes: 42 additions & 0 deletions frontend/src/components/categoryCard/CategoryCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import "./CategoryCard.css";

// Fra Figma: Components → CategoryCard. Klikkbart kort for en kategori.
type CategoryCardProps = {
number: string; // f.eks. "01"
meta: string; // f.eks. "1 234 varer" eller "Laster..."
name: string;
description: string;
onClick: () => void;
};

export default function CategoryCard({
number,
meta,
name,
description,
onClick,
}: CategoryCardProps) {
return (
<button
type="button"
className="category-card"
onClick={onClick}
aria-label={`Vis produkter i ${name}`}
>
<span className="category-card__top">
<span>{number}</span>
<span>{meta}</span>
</span>

<span className="category-card__bottom">
<span>
<span className="category-card__name">{name}</span>
<span className="category-card__description">{description}</span>
</span>
<span className="category-card__arrow" aria-hidden="true">
→
</span>
</span>
</button>
);
}
24 changes: 24 additions & 0 deletions frontend/src/components/checkbox/Checkbox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { InputHTMLAttributes } from "react";
import "./Checkbox.css";

// En ekte <input type="checkbox">, så den virker med tastatur og skjermleser.
// Alle vanlige input-props (checked, onChange, id, aria-label ...) sendes rett videre.
type CheckboxProps = Omit<InputHTMLAttributes<HTMLInputElement>, "type" | "size"> & {
size?: "normal" | "large";
};

export default function Checkbox({
size = "normal",
className = "",
...rest
}: CheckboxProps) {
const sizeClass = size === "large" ? "checkbox--large" : "";

return (
<input
type="checkbox"
className={`checkbox ${sizeClass} ${className}`.trim()}
{...rest}
/>
);
}
51 changes: 0 additions & 51 deletions frontend/src/components/header/Header.css
Original file line number Diff line number Diff line change
Expand Up @@ -66,58 +66,7 @@
/* 3. Søkefelt – øvrige sider */

.header__search {
display: flex;
align-items: center;
gap: 12px;

width: 100%;
max-width: 610px;
height: 40px;
padding: 0 14px;
box-sizing: border-box;

justify-self: center;

background: var(--bg-surface);
border: 1px solid transparent;
border-radius: 8px;
}

.header__search:focus-within {
border-color: var(--text-accent);
}

.header__search-icon {
display: flex;
width: 18px;
height: 18px;
flex-shrink: 0;
color: var(--text-muted);
}

.header__search-icon svg {
width: 100%;
height: 100%;
}

.header__search-input {
flex: 1;
min-width: 0;
width: 100%;
border: none;
outline: none;
background: transparent;

color: var(--text-primary);
font: 400 13px var(--font-ui);
}

.header__search-input::placeholder {
color: var(--text-muted);
}

.header__search-input:disabled {
cursor: not-allowed;
}

/* 4. Handlinger */
Expand Down
65 changes: 11 additions & 54 deletions frontend/src/components/header/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import { Link } from "react-router";

import { useState } from "react";
import type { FormEvent } from "react";

import Logo from "../logo/Logo";
import MenuButton from "../menuButton/MenuButton";

import "../button/Button.css";
import Button from "../button/Button";
import SearchField from "../searchfield/SearchField";
import "./Header.css";

type HeaderProps = {
Expand All @@ -30,18 +28,6 @@ export default function Header({
isMenuOpen = false,
listCount = 0,
}: HeaderProps) {
const [searchInput, setSearchInput] = useState(searchValue);

const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();

const value = searchInput.trim();

if (value && onSearch) {
onSearch(value);
}
};

return (
<header className={`header header--${variant}`}>
<div className="header__logo">
Expand All @@ -67,47 +53,18 @@ export default function Header({
</span>
</nav>
) : (
<form
<SearchField
className="header__search"
role="search"
onSubmit={handleSubmit}
>
<span className="header__search-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none">
<circle
cx="10.5"
cy="10.5"
r="6.5"
stroke="currentColor"
strokeWidth="1.8"
/>
<path
d="M15.5 15.5L21 21"
stroke="currentColor"
strokeWidth="1.8"
strokeLinecap="round"
/>
</svg>
</span>

<input
type="search"
className="header__search-input"
placeholder="Søk etter matvarer, f.eks. havregryn"
aria-label="Søk etter matvarer"
value={searchInput}
onChange={(event) =>
setSearchInput(event.target.value)
}
disabled={!onSearch}
/>
</form>
defaultValue={searchValue}
onSearch={onSearch}
/>
)}

<div className="header__actions">
<button
type="button"
className="button button--dark button--s header__list-button"
<Button
variant="dark"
size="s"
className="header__list-button"
onClick={onOpenList}
disabled={!onOpenList}
title={!onOpenList ? "Handlelisten kobles til senere" : undefined}
Expand All @@ -116,7 +73,7 @@ export default function Header({
<span className="button__count">
{String(listCount).padStart(2, "0")}
</span>
</button>
</Button>

<MenuButton
onClick={onOpenMenu}
Expand Down
5 changes: 3 additions & 2 deletions frontend/src/components/noteItem/NoteItem.css
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
color: var(--text-secondary);
}

/* Avkrysset: varen blir grå */
.note-item--checked .note-item__name {
/* Avkrysset: varen blir grå og strøket ut */
.note-item:has(.checkbox:checked) .note-item__name {
color: var(--text-muted);
text-decoration: line-through;
}
21 changes: 21 additions & 0 deletions frontend/src/components/noteItem/NoteItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import Checkbox from "../checkbox/Checkbox";
import "./NoteItem.css";

// Fra Figma: Components → NoteItem. En vare på handlelappen.
type NoteItemProps = {
name: string;
count?: number; // vises som "×2", bare når det er mer enn 1
defaultChecked?: boolean;
};

export default function NoteItem({ name, count, defaultChecked }: NoteItemProps) {
return (
<label className="note-item">
<Checkbox defaultChecked={defaultChecked} />
<span className="note-item__name">{name}</span>
{count !== undefined && count > 1 && (
<span className="note-item__count">×{count}</span>
)}
</label>
);
}
55 changes: 55 additions & 0 deletions frontend/src/components/searchfield/SearchField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { useState } from "react";
import type { FormEvent, ReactNode } from "react";
import "./Searchfield.css";

// Fra Figma: Components → SearchField.
// Søker bare når du trykker Enter (eller en submit-knapp i children).
type SearchFieldProps = {
onSearch?: (value: string) => void;
defaultValue?: string;
size?: "normal" | "large";
placeholder?: string;
className?: string;
children?: ReactNode; // vises til høyre, f.eks. en Søk-knapp
};

export default function SearchField({
onSearch,
defaultValue = "",
size = "normal",
placeholder = "Søk etter matvarer, f.eks. havregryn",
className = "",
children,
}: SearchFieldProps) {
const [value, setValue] = useState(defaultValue);

const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault(); // ikke last siden på nytt
const search = value.trim();
if (search && onSearch) {
onSearch(search);
}
};

const sizeClass = size === "large" ? "search-field--large" : "";

return (
<form
className={`search-field ${sizeClass} ${className}`.trim()}
role="search"
onSubmit={handleSubmit}
>
<span className="search-field__icon" aria-hidden="true" />
<input
className="search-field__input"
type="search"
value={value}
onChange={(event) => setValue(event.target.value)}
placeholder={placeholder}
aria-label="Søk etter matvarer"
disabled={!onSearch}
/>
{children}
</form>
);
}
7 changes: 3 additions & 4 deletions frontend/src/pages/SearchResults/SearchFilters.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@

import type { FilterCount } from "./searchTypes";
import Checkbox from "../../components/checkbox/Checkbox";

type SearchFiltersProps = {
categories: FilterCount[];
Expand Down Expand Up @@ -73,8 +74,7 @@ export default function SearchFilters({
key={category.value}
>
<span className="search-filter-option-name">
<input
type="checkbox"
<Checkbox
checked={selectedCategories.includes(
category.value,
)}
Expand Down Expand Up @@ -107,8 +107,7 @@ export default function SearchFilters({
key={store.value}
>
<span className="search-filter-option-name">
<input
type="checkbox"
<Checkbox
checked={selectedStores.includes(store.value)}
onChange={(event) =>
toggle(
Expand Down
Loading