diff --git a/client/src/components/layout/BottomNav.tsx b/client/src/components/layout/BottomNav.tsx
new file mode 100644
index 0000000..fabcf28
--- /dev/null
+++ b/client/src/components/layout/BottomNav.tsx
@@ -0,0 +1,138 @@
+import { NavLink } from 'react-router';
+import BookIcon from '@mui/icons-material/Book';
+import MenuBookIcon from '@mui/icons-material/MenuBook';
+import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorder';
+import FavoriteIcon from '@mui/icons-material/Favorite';
+import ShoppingCartIcon from '@mui/icons-material/ShoppingCart';
+import AddShoppingCartIcon from '@mui/icons-material/AddShoppingCart';
+import PersonOutlinedIcon from '@mui/icons-material/PersonOutlined';
+import PersonIcon from '@mui/icons-material/Person';
+import AddIcon from '@mui/icons-material/Add';
+import type { SvgIconComponent } from '@mui/icons-material';
+import { useCurrentUser } from '../../lib/useCurrentUser';
+import { LoginRequiredDialog } from './LoginRequiredDialog';
+import { NewRecipeButton } from './NewRecipeButton';
+
+// rosa fokusring
+const FOCUS_RING =
+ 'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-pink-light';
+
+// ikon over tekst, rødt og bold når aktiv
+function tabClass({ isActive }: { isActive: boolean }) {
+ return `flex flex-col items-center gap-1 text-[11px] transition-colors ${FOCUS_RING} ${
+ isActive ? 'font-bold text-primary' : 'text-text'
+ }`;
+}
+
+type TabLinkProps = {
+ to: string;
+ label: string;
+ icon: SvgIconComponent;
+ activeIcon: SvgIconComponent;
+};
+
+// ett valg i bunnmenyen og bytter ikon på aktiv side
+function TabLink({
+ to,
+ label,
+ icon: Icon,
+ activeIcon: ActiveIcon,
+}: TabLinkProps) {
+ return (
+
+ {({ isActive }) => (
+ <>
+ {isActive ? (
+
+ ) : (
+
+ )}
+ {label}
+ >
+ )}
+
+ );
+}
+
+// menyen nederst på mobil
+export function BottomNav() {
+ const { user } = useCurrentUser();
+
+ // ser ut som et valg men åpner innloggingspopupen
+ const favoritesButton = (
+
+ );
+
+ // den runde plussknappen i midten
+ const newRecipeButton = (
+
+ );
+
+ return (
+
+ );
+}
diff --git a/client/src/components/layout/Header.tsx b/client/src/components/layout/Header.tsx
index 579c141..6d94970 100644
--- a/client/src/components/layout/Header.tsx
+++ b/client/src/components/layout/Header.tsx
@@ -1,139 +1,38 @@
-import { useState } from 'react';
-import { Link, NavLink, useSearchParams } from 'react-router';
-import SearchBar from './SearchBar';
-import { Button } from '../ui/Button';
-import { Dialog } from '../ui/Dialog';
-import NewRecipePage from '../../routes/NewRecipePage';
-import { RecipeStepIndicator } from '../new-recipe/RecipeStepIndicator';
-import { useCurrentUser } from '../../lib/useCurrentUser';
-import { LoginRequiredDialog } from './LoginRequiredDialog';
+import { useEffect, useState } from 'react';
+import { useSearchParams } from 'react-router';
+import { TopHeader } from './TopHeader';
+import { StickyHeader } from './StickyHeader';
+import { MobileHeader } from './MobileHeader';
-// Fokusmarkering ved tastaturnavigasjon
-const focusRing =
- 'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary';
-
-// Aktiv lenke markeres med både farge og understrek
-function navLinkClass({ isActive }: { isActive: boolean }) {
- const base = `whitespace-nowrap font-body transition-colors hover:text-primary ${focusRing}`;
-
- return isActive
- ? `${base} text-primary underline underline-offset-4`
- : `${base} text-text`;
-}
-
-// Styling for knapper/lenker i header
-const buttonBase = `whitespace-nowrap rounded-medium px-3 py-2 text-sm font-body transition-colors ${focusRing}`;
-
-// Brukerknapp
-function userClass({ isActive }: { isActive: boolean }) {
- const base = `${buttonBase} border border-primary`;
-
- return isActive
- ? `${base} bg-primary text-background`
- : `${base} text-primary hover:bg-pink-light`;
-}
+// hvor langt man scroller før den lille headeren vises
+const SCROLL_LIMIT = 160;
export default function Header() {
- // hvem som er logget inn
- const { user } = useCurrentUser();
- // Søkeordet fra URL-en
+ // Søkeordet fra URL
const [searchParams] = useSearchParams();
const query = searchParams.get('q') ?? '';
- // holder styr på hvilket steg i Ny oppskrift brukeren er på
- const [currentStep, setCurrentStep] = useState(1);
-
- // knapp som åpner ny oppskrift eller innloggingspopup hvis man ikke er logget inn
- const newRecipeButton = (
-
- );
-
- // åpner innloggings popupen
- const favoritesButton = (
-
- );
-
- return (
-
-
- {/* Logo */}
-
- Oppskriftsbok
-
-
- {/* Søkefelt */}
-
+ // om man har scrollet forbi den store headeren
+ const [scrolled, setScrolled] = useState(false);
- {/* Navigasjon */}
-
-
-
+ return (
+ <>
+
+
+
+ >
);
}
diff --git a/client/src/components/layout/Layout.tsx b/client/src/components/layout/Layout.tsx
index e91e1e9..ada95ae 100644
--- a/client/src/components/layout/Layout.tsx
+++ b/client/src/components/layout/Layout.tsx
@@ -1,15 +1,19 @@
import { Outlet } from 'react-router';
import Header from './Header';
+import { BottomNav } from './BottomNav';
-// Felles ramme for alle sider
+// felles ramme for alle sider
export default function Layout() {
return (
-
+ {/* ekstra plass nederst på mobil så bunnmenyen ikke dekker innholdet */}
+
+
+
);
}
diff --git a/client/src/components/layout/LoginRequiredDialog.tsx b/client/src/components/layout/LoginRequiredDialog.tsx
index 4069dba..e2c74d0 100644
--- a/client/src/components/layout/LoginRequiredDialog.tsx
+++ b/client/src/components/layout/LoginRequiredDialog.tsx
@@ -20,9 +20,9 @@ export function LoginRequiredDialog({
{/* knappene som åpner popupen */}
{trigger}
-
+
{/* popup */}
-
+
Du må logge inn for å bruke dette
diff --git a/client/src/components/layout/MobileHeader.tsx b/client/src/components/layout/MobileHeader.tsx
new file mode 100644
index 0000000..19ce238
--- /dev/null
+++ b/client/src/components/layout/MobileHeader.tsx
@@ -0,0 +1,57 @@
+import { useState } from 'react';
+import { Link } from 'react-router';
+import SearchIcon from '@mui/icons-material/Search';
+import SearchBar from './SearchBar';
+
+type MobileHeaderProps = {
+ query: string;
+};
+
+// headeren på mobil med logo og søk
+export function MobileHeader({ query }: MobileHeaderProps) {
+ // om søkefeltet er åpent
+ const [searchOpen, setSearchOpen] = useState(false);
+
+ return (
+
+
+ {/* søkefeltet tar hele linja når det er åpent */}
+ {searchOpen ? (
+
+ event.key === 'Escape' && setSearchOpen(false)
+ }
+ onBlur={(event) => {
+ // lukker når man trykker utenfor
+ if (!event.currentTarget.contains(event.relatedTarget)) {
+ setSearchOpen(false);
+ }
+ }}
+ >
+
+
+ ) : (
+ <>
+
+ Oppskriftsbok
+
+
+ >
+ )}
+
+
+ );
+}
diff --git a/client/src/components/layout/NavMenu.tsx b/client/src/components/layout/NavMenu.tsx
new file mode 100644
index 0000000..55e28e7
--- /dev/null
+++ b/client/src/components/layout/NavMenu.tsx
@@ -0,0 +1,124 @@
+import { NavLink } from 'react-router';
+import BookIcon from '@mui/icons-material/Book';
+import MenuBookIcon from '@mui/icons-material/MenuBook';
+import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorder';
+import FavoriteIcon from '@mui/icons-material/Favorite';
+import ShoppingCartIcon from '@mui/icons-material/ShoppingCart';
+import AddShoppingCartIcon from '@mui/icons-material/AddShoppingCart';
+import type { SvgIconComponent } from '@mui/icons-material';
+import { useCurrentUser } from '../../lib/useCurrentUser';
+import { LoginRequiredDialog } from './LoginRequiredDialog';
+
+// rosa fokusring
+const FOCUS_RING =
+ 'focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-pink-light';
+
+// ikon og tekst ved siden av hverandre, rødt når aktiv
+function linkClass({ isActive }: { isActive: boolean }) {
+ return `flex items-center gap-2 whitespace-nowrap transition-colors ${FOCUS_RING} ${
+ isActive ? 'text-primary' : 'text-text hover:text-primary'
+ }`;
+}
+
+// gjør at det bare blir ikoner på smale skjermer (gjelder ikke mobil eller skjermlesere)
+const COMPACT_LABEL = 'sr-only lg:not-sr-only';
+
+type MenuLinkProps = {
+ to: string;
+ label: string;
+ icon: SvgIconComponent;
+ activeIcon: SvgIconComponent;
+ // bare ikon på smale skjermer
+ compact: boolean;
+};
+
+// bytter ikon og understreker teksten på aktiv side
+function MenuLink({
+ to,
+ label,
+ icon: Icon,
+ activeIcon: ActiveIcon,
+ compact,
+}: MenuLinkProps) {
+ return (
+
+ {({ isActive }) => (
+ <>
+ {isActive ? (
+
+ ) : (
+
+ )}
+
+ {label}
+
+ >
+ )}
+
+ );
+}
+
+type NavMenuProps = {
+ compact?: boolean;
+};
+
+// menyen som brukes i begge headerne på pc
+export function NavMenu({ compact = false }: NavMenuProps) {
+ const { user } = useCurrentUser();
+
+ // ser ut som menylenken men åpner innloggingspopupen
+ const favoritesButton = (
+
+ );
+
+ return (
+
+ );
+}
diff --git a/client/src/components/layout/NewRecipeButton.tsx b/client/src/components/layout/NewRecipeButton.tsx
new file mode 100644
index 0000000..79a4e2f
--- /dev/null
+++ b/client/src/components/layout/NewRecipeButton.tsx
@@ -0,0 +1,40 @@
+import { useState, type ReactNode } from 'react';
+import { Dialog } from '../ui/Dialog';
+import NewRecipePage from '../../routes/NewRecipePage';
+import { RecipeStepIndicator } from '../new-recipe/RecipeStepIndicator';
+import { useCurrentUser } from '../../lib/useCurrentUser';
+import { LoginRequiredDialog } from './LoginRequiredDialog';
+
+type NewRecipeButtonProps = {
+ // knappen som vises, er forskjellig på pc og mobil
+ trigger: ReactNode;
+};
+
+// åpner ny oppskrift eller innloggingspopup hvis man ikke er logget inn
+export function NewRecipeButton({ trigger }: NewRecipeButtonProps) {
+ const { user } = useCurrentUser();
+ // hvordan steg i ny oppskrift man er på
+ const [currentStep, setCurrentStep] = useState(1);
+
+ if (!user) {
+ return (
+
+ );
+ }
+
+ return (
+ }
+ >
+
+
+ );
+}
diff --git a/client/src/components/layout/SearchBar.tsx b/client/src/components/layout/SearchBar.tsx
index 1a19a8f..f292ba0 100644
--- a/client/src/components/layout/SearchBar.tsx
+++ b/client/src/components/layout/SearchBar.tsx
@@ -3,10 +3,18 @@ import { useNavigate } from 'react-router';
type SearchBarProps = {
initialQuery: string;
+ // egen id for hvert søkefelt så to felt ikke får samme id
+ inputId: string;
+ // markøren havner i feltet når det åpnes
+ autoFocus?: boolean;
};
-// Søkefelt i headeren. Søket legges i URL-en (/recipes?q=...), slik at det kan deles som lenke og fungerer med tilbakeknappen.
-export default function SearchBar({ initialQuery }: SearchBarProps) {
+// Søkefelt i headeren. Søket legges i URL-en, sånn at det kan deles som lenke og fungerer med tilbakeknappen
+export default function SearchBar({
+ initialQuery,
+ inputId,
+ autoFocus = false,
+}: SearchBarProps) {
const [input, setInput] = useState(initialQuery);
const navigate = useNavigate();
@@ -22,36 +30,26 @@ export default function SearchBar({ initialQuery }: SearchBarProps) {
}
return (
- // role="search" gjør at søket kan leses av skjermlesere.
);
}
diff --git a/client/src/components/layout/StickyHeader.tsx b/client/src/components/layout/StickyHeader.tsx
new file mode 100644
index 0000000..eda7b4e
--- /dev/null
+++ b/client/src/components/layout/StickyHeader.tsx
@@ -0,0 +1,50 @@
+import { Link } from 'react-router';
+import AddIcon from '@mui/icons-material/Add';
+import SearchBar from './SearchBar';
+import { NavMenu } from './NavMenu';
+import { NewRecipeButton } from './NewRecipeButton';
+import { UserLink } from './UserLink';
+
+type StickyHeaderProps = {
+ // om headeren skal vises bestemmes av scrolling
+ visible: boolean;
+ query: string;
+};
+
+// den lille headeren som glir inn fra toppen når man har scrollet ned
+export function StickyHeader({ visible, query }: StickyHeaderProps) {
+ const newRecipeButton = (
+
+ );
+
+ return (
+
+
+
+ Oppskriftsbok
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/client/src/components/layout/TopHeader.tsx b/client/src/components/layout/TopHeader.tsx
new file mode 100644
index 0000000..4a738ef
--- /dev/null
+++ b/client/src/components/layout/TopHeader.tsx
@@ -0,0 +1,87 @@
+import { useState } from 'react';
+import { Link } from 'react-router';
+import AddIcon from '@mui/icons-material/Add';
+import SearchIcon from '@mui/icons-material/Search';
+import SearchBar from './SearchBar';
+import { NavMenu } from './NavMenu';
+import { NewRecipeButton } from './NewRecipeButton';
+import { UserLink } from './UserLink';
+
+type TopHeaderProps = {
+ query: string;
+ // skjules for skjermlesere når stickyheader vises
+ scrolled: boolean;
+};
+
+// den store headeren øverst på siden
+export function TopHeader({ query, scrolled }: TopHeaderProps) {
+ // om søkefeltet under headeren er åpent
+ const [searchOpen, setSearchOpen] = useState(false);
+
+ const newRecipeButton = (
+
+ );
+
+ return (
+
+ {/* søk til venstre, logo i midten, knapper til høyre */}
+
+ {/* søkeikon som blir til søkefelt når man trykker */}
+ {searchOpen ? (
+
+ event.key === 'Escape' && setSearchOpen(false)
+ }
+ onBlur={(event) => {
+ // lukker når man klikker utenfor
+ if (!event.currentTarget.contains(event.relatedTarget)) {
+ setSearchOpen(false);
+ }
+ }}
+ >
+
+
+ ) : (
+
+ )}
+
+
+ Oppskriftsbok
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/client/src/components/layout/UserLink.tsx b/client/src/components/layout/UserLink.tsx
new file mode 100644
index 0000000..fcc5f0e
--- /dev/null
+++ b/client/src/components/layout/UserLink.tsx
@@ -0,0 +1,28 @@
+import { NavLink } from 'react-router';
+import PersonOutlinedIcon from '@mui/icons-material/PersonOutlined';
+import PersonIcon from '@mui/icons-material/Person';
+import { useCurrentUser } from '../../lib/useCurrentUser';
+
+// brukerikonet går til min side eller til innlogging
+export function UserLink() {
+ const { user } = useCurrentUser();
+
+ return (
+
+ `flex text-4xl transition-colors focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-pink-light ${
+ isActive ? 'text-primary' : 'text-primary/80 hover:text-primary'
+ }`
+ }
+ >
+ {/* fylt ikon når innlogget */}
+ {user ? (
+
+ ) : (
+
+ )}
+
+ );
+}
diff --git a/client/src/components/ui/Dialog.tsx b/client/src/components/ui/Dialog.tsx
index e167fc6..91bf786 100644
--- a/client/src/components/ui/Dialog.tsx
+++ b/client/src/components/ui/Dialog.tsx
@@ -28,7 +28,7 @@ export function Dialog({
{/* Mørkt lag bak dialogen */}
@@ -36,7 +36,7 @@ export function Dialog({
{/* Selve dialogboksen */}
-
+
{/* Skjult beskrivelse for skjermlesere */}
diff --git a/client/src/index.css b/client/src/index.css
index 865de5d..a8a28c3 100644
--- a/client/src/index.css
+++ b/client/src/index.css
@@ -29,6 +29,9 @@
--radius-small: 6px;
--radius-medium: 10px;
--radius-large: 16px;
+
+ /* bredde der pc header tar over fra mobil header */
+ --breakpoint-desktop: 35rem;
}
/* Grunnleggende styling */