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
6 changes: 3 additions & 3 deletions src/components/Cart/CartContents.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ const CartContents = () => {
data.cart.contents.nodes,
)
}
color="red"
variant="secondary"
buttonDisabled={updateCartProcessing}
>
Fjern
Expand All @@ -156,7 +156,7 @@ const CartContents = () => {
{!isCheckoutPage && (
<div className="flex justify-center mb-4">
<Link href="/kasse" passHref>
<Button fullWidth>GÅ TIL KASSE</Button>
<Button variant="primary" fullWidth>GÅ TIL KASSE</Button>
</Link>
</div>
)}
Expand All @@ -168,7 +168,7 @@ const CartContents = () => {
Ingen produkter i handlekurven
</h2>
<Link href="/produkter" passHref>
<Button>Fortsett å handle</Button>
<Button variant="primary">Fortsett å handle</Button>
</Link>
</div>
)}
Expand Down
2 changes: 1 addition & 1 deletion src/components/Index/Hero.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const Hero = () => (
</h1>
<Button
href="/produkter"
isHero
variant="hero"
>
Se Utvalget
</Button>
Expand Down
68 changes: 30 additions & 38 deletions src/components/Product/ProductFilters.component.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { Dispatch, SetStateAction } from 'react';
import { Product, ProductType } from '@/types/product';
import Button from '@/components/UI/Button.component';
import Checkbox from '@/components/UI/Checkbox.component';
import RangeSlider from '@/components/UI/RangeSlider.component';

interface ProductFiltersProps {
selectedSizes: string[];
Expand Down Expand Up @@ -65,59 +68,48 @@ const ProductFilters = ({

return (
<div className="w-full md:w-64 flex-shrink-0">
<div className="bg-white p-8 sm:p-6 rounded-lg shadow-sm">
<div className="bg-white px-8 pb-8 sm:px-6 sm:pb-6 rounded-lg shadow-sm">
<div className="mb-8">
<h3 className="font-semibold mb-4">PRODUKT TYPE</h3>
<div className="space-y-2">
{productTypes.map((type) => (
<label key={type.id} className="flex items-center">
<input
type="checkbox"
className="form-checkbox"
checked={type.checked}
onChange={() => toggleProductType(type.id)}
/>
<span className="ml-2">{type.name}</span>
</label>
<Checkbox
key={type.id}
id={type.id}
label={type.name}
checked={type.checked}
onChange={() => toggleProductType(type.id)}
/>
))}
</div>
</div>

<div className="mb-8">
<h3 className="font-semibold mb-4">PRIS</h3>
<label htmlFor="price-range" className="sr-only">Pris</label>
<input
id="price-range"
type="range"
min="0"
max="1000"
value={priceRange[1]}
onChange={(e) =>
setPriceRange([priceRange[0], parseInt(e.target.value)])
}
className="w-full"
/>
<div className="flex justify-between mt-2">
<span>kr {priceRange[0]}</span>
<span>kr {priceRange[1]}</span>
</div>
<RangeSlider
id="price-range"
label="Pris"
min={0}
max={1000}
value={priceRange[1]}
startValue={priceRange[0]}
onChange={(value) => setPriceRange([priceRange[0], value])}
formatValue={(value) => `kr ${value}`}
/>
</div>

<div className="mb-8">
<h3 className="font-semibold mb-4">STØRRELSE</h3>
<div className="grid grid-cols-3 gap-2">
{sizes.map((size) => (
<button
<Button
key={size}
onClick={() => toggleSize(size)}
className={`px-3 py-1 border rounded ${
selectedSizes.includes(size)
? 'bg-gray-900 text-white'
: 'hover:bg-gray-100'
}`}
handleButtonClick={() => toggleSize(size)}
variant="filter"
selected={selectedSizes.includes(size)}
>
{size}
</button>
</Button>
))}
</div>
</div>
Expand All @@ -142,12 +134,12 @@ const ProductFilters = ({
</div>
</div>

<button
onClick={resetFilters}
className="w-full mt-8 py-2 px-4 bg-gray-100 text-gray-700 rounded hover:bg-gray-200 transition-colors"
<Button
handleButtonClick={resetFilters}
variant="reset"
>
Resett filter
</button>
</Button>
</div>
</div>
);
Expand Down
44 changes: 27 additions & 17 deletions src/components/UI/Button.component.tsx
Original file line number Diff line number Diff line change
@@ -1,53 +1,62 @@
import { ReactNode } from 'react';
import Link from 'next/link';

type TButtonColors = 'red' | 'blue';
type TButtonVariant = 'primary' | 'secondary' | 'hero' | 'filter' | 'reset';

interface IButtonProps {
handleButtonClick?: () => void;
buttonDisabled?: boolean;
color?: TButtonColors;
children: ReactNode;
variant?: TButtonVariant;
children?: ReactNode;
fullWidth?: boolean;
isHero?: boolean;
href?: string;
title?: string;
selected?: boolean;
}

/**
* Renders a clickable button
* @function Button
* @param {void} handleButtonClick - Handle button click
* @param {boolean?} buttonDisabled - Is button disabled?
* @param {TButtonColors?} color - Color for button, either red or blue
* @param {TButtonVariant?} variant - Button variant
* @param {ReactNode} children - Children for button
* @param {boolean?} fullWidth - Whether the button should be full-width on mobile
* @param {boolean?} selected - Whether the button is in a selected state
* @returns {JSX.Element} - Rendered component
*/
const Button = ({
handleButtonClick,
buttonDisabled,
color = 'blue',
variant = 'primary',
children,
fullWidth = false,
isHero = false,
href,
title,
selected = false,
}: IButtonProps) => {
const getColorClasses = (buttonColor: TButtonColors) => {
if (buttonColor === 'blue') {
return 'bg-blue-500 hover:bg-blue-600';
const getVariantClasses = (variant: TButtonVariant = 'primary') => {
switch (variant) {
case 'hero':
return 'inline-block px-8 py-4 text-sm tracking-wider uppercase bg-white text-gray-900 hover:bg-gray-400 hover:text-white hover:shadow-md';
case 'filter':
return selected
? 'px-3 py-1 border rounded bg-gray-900 text-white'
: 'px-3 py-1 border rounded hover:bg-gray-100 bg-white text-gray-900';
case 'reset':
return 'w-full mt-8 py-2 px-4 bg-gray-100 text-gray-700 rounded hover:bg-gray-200 transition-colors';
case 'secondary':
return 'px-2 lg:px-4 py-2 font-bold border border-gray-400 border-solid rounded text-white bg-red-500 hover:bg-red-600';
default: // primary
return 'px-2 lg:px-4 py-2 font-bold border border-gray-400 border-solid rounded text-white bg-blue-500 hover:bg-blue-600';
}
return 'bg-red-500 hover:bg-red-600';
};

const buttonClasses = isHero
? 'inline-block px-8 py-4 text-sm tracking-wider uppercase bg-white text-gray-900 hover:bg-gray-400 hover:text-white hover:shadow-md'
: `px-2 lg:px-4 py-2 font-bold border border-gray-400 border-solid rounded text-white ${getColorClasses(color)}`;

const classes = `${buttonClasses} ease-in-out transition-all duration-300 disabled:opacity-50 ${
const classes = `${getVariantClasses(variant)} ease-in-out transition-all duration-300 disabled:opacity-50 ${
fullWidth ? 'w-full md:w-auto' : ''
}`;

if (href && isHero) {
if (href) {
return (
<Link href={href} className={classes}>
{children}
Expand All @@ -60,6 +69,7 @@ const Button = ({
onClick={handleButtonClick}
disabled={buttonDisabled}
className={classes}
title={title}
>
{children}
</button>
Expand Down
34 changes: 34 additions & 0 deletions src/components/UI/Checkbox.component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { ChangeEvent } from 'react';

interface ICheckboxProps {
id: string;
label: string;
checked: boolean;
onChange: (e: ChangeEvent<HTMLInputElement>) => void;
}

/**
* A reusable checkbox component with a label
* @function Checkbox
* @param {string} id - Unique identifier for the checkbox
* @param {string} label - Label text to display next to the checkbox
* @param {boolean} checked - Whether the checkbox is checked
* @param {function} onChange - Handler for when the checkbox state changes
* @returns {JSX.Element} - Rendered component
*/
const Checkbox = ({ id, label, checked, onChange }: ICheckboxProps) => {
return (
<label htmlFor={id} className="flex items-center py-2 cursor-pointer">
<input
id={id}
type="checkbox"
className="form-checkbox h-5 w-5 cursor-pointer"
checked={checked}
onChange={onChange}
/>
<span className="ml-3 text-base">{label}</span>
</label>
);
};

export default Checkbox;
63 changes: 63 additions & 0 deletions src/components/UI/RangeSlider.component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { ChangeEvent } from 'react';

interface IRangeSliderProps {
id: string;
label: string;
min: number;
max: number;
value: number;
onChange: (value: number) => void;
startValue?: number;
formatValue?: (value: number) => string;
}

/**
* A reusable range slider component with labels
* @function RangeSlider
* @param {string} id - Unique identifier for the slider
* @param {string} label - Accessible label for the slider
* @param {number} min - Minimum value of the range
* @param {number} max - Maximum value of the range
* @param {number} value - Current value of the slider
* @param {function} onChange - Handler for when the slider value changes
* @param {number} startValue - Optional starting value to display (defaults to min)
* @param {function} formatValue - Optional function to format the displayed values
* @returns {JSX.Element} - Rendered component
*/
const RangeSlider = ({
id,
label,
min,
max,
value,
onChange,
startValue = min,
formatValue = (val: number) => val.toString(),
}: IRangeSliderProps) => {
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
onChange(parseInt(e.target.value));
};

return (
<div>
<label htmlFor={id} className="sr-only">
{label}
</label>
<input
id={id}
type="range"
min={min}
max={max}
value={value}
onChange={handleChange}
className="w-full cursor-pointer"
/>
<div className="flex justify-between mt-2">
<span>{formatValue(startValue)}</span>
<span>{formatValue(value)}</span>
</div>
</div>
);
};

export default RangeSlider;
Loading