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
25 changes: 20 additions & 5 deletions client/src/components/recipe/RecipeDetail.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Recipe } from '../../types/recipe';
import RestaurantIcon from '@mui/icons-material/Restaurant';
import AccessTimeIcon from '@mui/icons-material/AccessTime';
import { useState } from 'react';
import { Rating } from '@mui/material';
import Checkbox from '@mui/material/Checkbox';
import FormControlLabel from '@mui/material/FormControlLabel';
Expand All @@ -11,6 +12,14 @@ type RecipeDetailProps = {
};

export function RecipeDetail({ recipe }: RecipeDetailProps) {
const [checkedIds, setCheckedIds] = useState<String[]>([]);

function toggeIngredient(id: string) {
setCheckedIds((prev)=>
prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]
);
}

return (
<article className="grid md:grid-cols-2 gap-6">
<section className="border border-olive rounded-lg p-4 flex flex-col bg-background">
Expand Down Expand Up @@ -55,13 +64,19 @@ export function RecipeDetail({ recipe }: RecipeDetailProps) {
<FormControlLabel
control={
<Checkbox
checked={checkedIds.includes(ingredient.id)}
onChange={() => toggeIngredient(ingredient.id)}
sx={{
color: 'var(--color-olive)',
'&.Mui-checked': { color: 'var(--color-olive)' },
color: 'var(--color-primary)',
'&.Mui-checked': { color: 'var(--color-primary)' },
}}
/>
}
label={`${ingredient.amount} ${ingredient.unit} ${ingredient.name}`}
label={
<span className={checkedIds.includes(ingredient.id) ? 'line-through opacity-60' : ''}>
{`${ingredient.amount} ${ingredient.unit} ${ingredient.name}`}
</span>
}
/>
</li>
))}
Expand All @@ -76,8 +91,8 @@ export function RecipeDetail({ recipe }: RecipeDetailProps) {
control={
<Checkbox
sx={{
color: 'var(--color-olive)',
'&.Mui-checked': { color: 'var(--color-olive)' },
color: 'var(--color-primary)',
'&.Mui-checked': { color: 'var(--color-primary)' },
}}
/>
}
Expand Down
72 changes: 46 additions & 26 deletions client/src/components/slideshow/RecipeSlideshow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,41 @@ import type { Recipe } from '../../types/recipe';
import useEmblaCarousel from 'embla-carousel-react';
import { Link } from 'react-router';
import { useEffect, useState } from 'react';
import Autoplay from 'embla-carousel-autoplay';
import AccessTimeIcon from '@mui/icons-material/AccessTime';
import RestaurantIcon from '@mui/icons-material/Restaurant';
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';

// Fargetemaer for slidesene. panel er tekstboksen, button er knappen for å se en oppskrift
type SlideshowTheme = 'light' | 'dark' | 'rose';

const themes = {
light: { panel: 'bg-secondary/50 text-olive-dark', button: 'bg-olive-dark text-background', dot: 'border-olive-dark', dotActive: 'bg-olive-dark' },
dark: { panel: 'bg-olive-dark text-background', button: 'bg-pink-light text-olive-dark', dot: 'border-background', dotActive: 'bg-background' },
rose: { panel: 'bg-pink text-pink-light', button: 'bg-primary-hover text-pink-light', dot: 'border-background', dotActive: 'bg-background' },
}
import PauseIcon from '@mui/icons-material/Pause';
import PlayArrowIcon from '@mui/icons-material/PlayArrow';

type RecipeSlideshowProps = {
recipes: Recipe[];
reversed?: boolean; // Denne er valgfri, og gjør at karusellen i midten er speilvendt
theme?: SlideshowTheme;
recipes: Recipe[];
};

// Slideshow på forsiden som viser én oppskrift av gangen.
// Det er mulig å bla med piler, prikkene, sveiping eller med tastaturet.
export function RecipeSlideshow({ recipes, reversed = false, theme = 'light' }: RecipeSlideshowProps) {
export function RecipeSlideshow({ recipes }: RecipeSlideshowProps) {
// emblaRef kobler Embla opp til vinduet, også brukes emblaAPI til å styre slideshowet.
const [emblaRef, emblaApi] = useEmblaCarousel({ loop: true });
const [emblaRef, emblaApi] = useEmblaCarousel({ loop: true }, [
Autoplay({ delay: 5000, stopOnMouseEnter: true, stopOnFocusIn: true}),
]);
// Husker hvilken av sidene i slideshowet som vises, sånn at riktig prikk blir farget inn.
const [selectedIndex, setSelectedIndex] = useState(0);

const colors = themes[theme];
const [isPlaying, setIsPlaying] = useState(true);

function togglePlay() {
const autoplay = emblaApi?.plugins().autoplay;
if(!autoplay) return;

if (isPlaying) {
autoplay.stop();
} else {
autoplay.play();
}
setIsPlaying(!isPlaying);
}

// Oppdaterer selectedIndex hver gang det byttes hvilken side som vises.
useEffect(() => {
Expand All @@ -45,18 +51,23 @@ export function RecipeSlideshow({ recipes, reversed = false, theme = 'light' }:
}, [emblaApi]);

return (
<section aria-label="Utvalgte oppskrifter" className="relative">
<section aria-label="Utvalgte oppskrifter" className="relative px-16">
{/* Vinduet: det skjuler slidene som ikke vises */}
<div className="overflow-hidden" ref={emblaRef}>
{/* Raden med alle slidene som Embla flytter sidelengs */}
<div className="flex">
{recipes.map((recipe) => (
// Én slide som bruker hele bredden i vinduet
<div key={recipe.id} className="min-w-0 flex-[0_0_100%]">
<div className="grid md:grid-cols-2 overflow-hidden rounded-xl">
{/* Tomt alternativ for bildet, ettersom tittelen allerede står rett ved siden av bildet. */}
<img className={`w-full h-80 md:h-full object-cover ${reversed ? 'md:order-last' : ''}`} src={recipe.imageUrl ?? '/images/placeholder.webp'} alt=""/>
<div className={`py-8 px-20 flex flex-col justify-center gap-4 ${colors.panel}`}>
<div className="grid md:grid-cols-2 gap-8">
<div className="py-8 pr-8 flex flex-col justify-center gap-4 text-olive-dark">
<div className="flex items-center gap-4 text-sm tracking-widest">
<span>UKENS OPPSKRIFT</span>
<span aria-hidden="true" className="h-px flex-1 bg-olive-dark opacity-40" />
<span>
{String(selectedIndex + 1).padStart(2, '0')} / {String(recipes.length).padStart(2, '0')}
</span>
</div>
<h3 className="font-heading text-4xl md:text-5xl">{recipe.title}</h3>
<p>{recipe.description}</p>
<div className="flex gap-6">
Expand All @@ -70,8 +81,10 @@ export function RecipeSlideshow({ recipes, reversed = false, theme = 'light' }:
</li>
))}
</ul>
<Link to={`/recipes/${recipe.id}`} className={`self-start rounded-full ${colors.button} px-6 py-3 hover:opacity-90 flex items-center gap-1`}>Se oppskrift<ArrowForwardIcon/></Link>
<Link to={`/recipes/${recipe.id}`} className="self-start rounded-full bg-olive-dark text-background px-6 py-3 hover:opacity-90 flex items-center gap-1">Se oppskrift<ArrowForwardIcon/></Link>
</div>
{/* Tomt alternativ for bildet, ettersom tittelen allerede står rett ved siden av bildet. */}
<img className="w-full h-80 md:h-full object-cover rounded-lg" src={recipe.imageUrl ?? '/images/placeholder.webp'} alt=""/>
</div>
</div>
))}
Expand All @@ -83,33 +96,40 @@ export function RecipeSlideshow({ recipes, reversed = false, theme = 'light' }:
type="button"
aria-label="Forrige oppskrift"
onClick={() => emblaApi?.scrollPrev()}
className="absolute left-4 top-1/2 -translate-y-1/2 flex h-12 w-12 items-center justify-center rounded-full bg-background/80 hover:bg-background focus-visible:outline-2 focus-visible:outline-primary"
className="absolute left-0 top-1/2 -translate-y-1/2 flex h-12 w-12 items-center justify-center rounded-full bg-background/80 hover:bg-background focus-visible:outline-2 focus-visible:outline-primary"
>
<ArrowBackIcon/>
</button>
<button
type="button"
aria-label="Neste oppskrift"
onClick={() => emblaApi?.scrollNext()}
className="absolute right-4 top-1/2 -translate-y-1/2 flex h-12 w-12 items-center justify-center rounded-full bg-background/80 hover:bg-background focus-visible:outline-2 focus-visible:outline-primary"
className="absolute right-0 top-1/2 -translate-y-1/2 flex h-12 w-12 items-center justify-center rounded-full bg-background/80 hover:bg-background focus-visible:outline-2 focus-visible:outline-primary"
>
<ArrowForwardIcon/>
</button>

{/* Prikker: én prikk per oppskrift. Den siden som vises er fylt inn. Dersom du trykker på en annen prikk hopper du til den siden. */}
<div className={`absolute bottom-4 flex gap-2 ${reversed ? 'left-4' : 'right-4'}`}>
<div className="mt-6 flex items-center justify-end gap-2 border-t border-olive-dark/50 pt-6">
{recipes.map((recipe, index) => (
<button
key={recipe.id}
type="button"
aria-label={`Gå til ${recipe.title}`}
aria-current={index === selectedIndex}
onClick={() => emblaApi?.scrollTo(index)}
className={`h-3 w-3 rounded-full border ${colors.dot} ${
index === selectedIndex ? colors.dotActive : 'bg-transparent'
className={`h-3 w-3 rounded-full border border-olive-dark/50 ${
index === selectedIndex ? 'bg-olive-dark/50' : 'bg-transparent'
}`}
/>
))}
<button
type="button"
onClick={togglePlay}
aria-label={isPlaying ? 'Pause' : 'Start'}
>
{isPlaying ? <PauseIcon fontSize="small" /> : <PlayArrowIcon fontSize="small" />}
</button>
</div>
</section>
);
Expand Down
4 changes: 2 additions & 2 deletions client/src/routes/HomePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ export default function HomePage() {
return (
<main className="bg-background min-h-screen">

<div className="max-w-6xl mx-auto px-6 py-10">
<div className="mx-auto px-6 py-10">


<RecipeSlideshow recipes={recipes.slice(0, 5)} theme="light"/>
<RecipeSlideshow recipes={recipes.slice(0, 5)}/>



Expand Down