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 ? ( + + ); +} + +// 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 ? ( + + ); +} + +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.
{/* synlig bare for skjermlesere */} -