Merged in feat/mypages-mobile-dropdown (pull request #256)

Feat/mypages mobile dropdown

Approved-by: Michael Zetterberg
This commit is contained in:
Chuma Mcphoy (We Ahead)
2024-06-19 15:13:52 +00:00
committed by Michael Zetterberg
23 changed files with 432 additions and 81 deletions

View File

@@ -0,0 +1,14 @@
import { logout } from "@/constants/routes/handleAuth"
import { serverClient } from "@/lib/trpc/server"
import MyPagesMobileDropdown from "@/components/Current/Header/MyPagesMobileDropdown"
import { LangParams, PageArgs } from "@/types/params"
export default async function MyPagesMobileDropdownPage({
params,
}: PageArgs<LangParams>) {
const navigation = await serverClient().contentstack.myPages.navigation.get()
if (!navigation) return null
return <MyPagesMobileDropdown navigation={navigation} lang={params.lang} />
}

View File

@@ -5,8 +5,15 @@ import { LangParams, PageArgs } from "@/types/params"
export default function HeaderLayout({
params,
languageSwitcher,
myPagesMobileDropdown,
}: PageArgs<LangParams> & {
languageSwitcher: React.ReactNode
}) {
return <Header lang={params.lang} languageSwitcher={languageSwitcher} />
} & { myPagesMobileDropdown: React.ReactNode }) {
return (
<Header
lang={params.lang}
myPagesMobileDropdown={myPagesMobileDropdown}
languageSwitcher={languageSwitcher}
/>
)
}

View File

@@ -1,14 +1,20 @@
import { baseUrls } from "@/constants/routes/baseUrls"
import { serverClient } from "@/lib/trpc/server"
import Header from "@/components/Current/Header"
import LanguageSwitcher from "@/components/Current/Header/LanguageSwitcher"
import MyPagesMobileDropdown from "@/components/Current/Header/MyPagesMobileDropdown"
import { LangParams, PageArgs } from "@/types/params"
export default async function HeaderPage({ params }: PageArgs<LangParams>) {
const navigation = await serverClient().contentstack.myPages.navigation.get()
return (
<Header
lang={params.lang}
myPagesMobileDropdown={
<MyPagesMobileDropdown navigation={navigation} lang={params.lang} />
}
languageSwitcher={<LanguageSwitcher urls={baseUrls} lang={params.lang} />}
/>
)

View File

@@ -0,0 +1,15 @@
import { serverClient } from "@/lib/trpc/server"
import MyPagesMobileDropdown from "@/components/Current/Header/MyPagesMobileDropdown"
import { LangParams, PageArgs } from "@/types/params"
export default async function MyPagesMobileDropdownPage({
params,
}: PageArgs<LangParams>) {
const navigation = await serverClient().contentstack.myPages.navigation.get()
if (!navigation) {
return null
}
return <MyPagesMobileDropdown navigation={navigation} lang={params.lang} />
}

View File

@@ -24,8 +24,11 @@ export default async function RootLayout({
children,
params,
languageSwitcher,
myPagesMobileDropdown,
}: React.PropsWithChildren<
LayoutArgs<LangParams> & { languageSwitcher: React.ReactNode }
LayoutArgs<LangParams> & { languageSwitcher: React.ReactNode } & {
myPagesMobileDropdown: React.ReactNode
}
>) {
const { defaultLocale, locale, messages } = await getIntl()
return (
@@ -82,7 +85,11 @@ export default async function RootLayout({
<LangPopup lang={params.lang} />
<SkipToMainContent />
<ServerIntlProvider intl={{ defaultLocale, locale, messages }}>
<Header lang={params.lang} languageSwitcher={languageSwitcher} />
<Header
lang={params.lang}
myPagesMobileDropdown={myPagesMobileDropdown}
languageSwitcher={languageSwitcher}
/>
{children}
<Footer lang={params.lang} />
</ServerIntlProvider>

View File

@@ -31,7 +31,7 @@
text-decoration: underline;
}
@media screen and (min-width: 950px) {
@media screen and (min-width: 1367px) {
.button {
font-weight: 600;
font-size: 16px;

View File

@@ -4,8 +4,10 @@ import { useIntl } from "react-intl"
import { login } from "@/constants/routes/handleAuth"
import { myPages } from "@/constants/routes/myPages"
import useDropdownStore from "@/stores/main-menu"
import Image from "@/components/Image"
import Avatar from "@/components/MyPages/Avatar"
import Link from "@/components/TempDesignSystem/Link"
import BookingButton from "../BookingButton"
@@ -21,16 +23,19 @@ export function MainMenu({
logo,
topMenuMobileLinks,
languageSwitcher,
myPagesMobileDropdown,
bookingHref,
isLoggedIn,
user,
lang,
}: MainMenuProps) {
const intl = useIntl()
const [isOpen, setIsOpen] = useState(false)
function toogleIsOpen() {
setIsOpen((prevIsOpen) => !prevIsOpen)
}
const {
isHamburgerMenuOpen,
isMyPagesMobileMenuOpen,
toggleHamburgerMenu,
toggleMyPagesMobileMenu,
} = useDropdownStore()
return (
<div className={styles.mainMenu}>
@@ -43,8 +48,8 @@ export function MainMenu({
<nav className={styles.navBar}>
<button
aria-pressed="false"
className={`${styles.expanderBtn} ${isOpen ? styles.expanded : ""}`}
onClick={toogleIsOpen}
className={`${styles.expanderBtn} ${isHamburgerMenuOpen ? styles.expanded : ""}`}
onClick={toggleHamburgerMenu}
type="button"
>
<span className={styles.iconBars}></span>
@@ -66,13 +71,16 @@ export function MainMenu({
</a>
<ul
className={`${styles.listWrapper} ${isOpen ? styles.isOpen : ""}`}
className={`${styles.listWrapper} ${isHamburgerMenuOpen ? styles.isOpen : ""}`}
>
<ul className={styles.linkRow}>
{isLoggedIn ? (
{!!user ? (
<>
<li>
<div className={styles.loggedInLogo} />
<li className={styles.avatarWrapper}>
<Avatar
firstName={user.firstName}
lastName={user.lastName}
/>
</li>
<li className={styles.mobileLinkRow}>
<Link
@@ -139,8 +147,23 @@ export function MainMenu({
) : null}
</ul>
<div className={styles.buttonContainer}>
<div className={styles.myPagesDesktopLink}>
<Link className={styles.link} href={myPages[lang]}>
{intl.formatMessage({ id: "My pages" })}
</Link>
</div>
<BookingButton href={bookingHref} />
{myPagesMobileDropdown && user ? (
<div
role="button"
onClick={() => toggleMyPagesMobileMenu()}
className={styles.avatarButton}
>
<Avatar firstName={user.firstName} lastName={user.lastName} />
</div>
) : null}
</div>
{isMyPagesMobileMenuOpen ? myPagesMobileDropdown : null}
</nav>
</div>
</div>

View File

@@ -1,21 +1,20 @@
.mainMenu {
background-color: #fff;
background-color: var(--Main-Grey-White);
background-image: none;
box-shadow: 0 0 7px rgba(0, 0, 0, 0.75);
box-shadow: 0px 1.001px 1.001px 0px rgba(0, 0, 0, 0.05);
max-height: 100%;
overflow: visible;
position: fixed;
top: 0;
width: 100%;
z-index: 99999;
height: 52.39px;
height: 70.047px;
}
.container {
box-sizing: content-box;
height: 100%;
margin: 0 auto;
max-width: 1200px;
padding: 0;
}
@@ -26,8 +25,11 @@
.navBar {
display: grid;
grid-template-columns: 1fr 80px 1fr;
grid-template-columns: auto auto 1fr auto;
grid-template-areas: "expanderBtn logoLink . buttonContainer";
grid-template-rows: 100%;
height: 100%;
padding: 0 var(--Spacing-x2);
}
.expanderBtn {
@@ -46,7 +48,7 @@
background: #757575;
border-radius: 2.3px;
display: inline-block;
height: 5px;
height: 3px;
position: relative;
transition: 0.3s;
width: 32px;
@@ -98,16 +100,19 @@
}
.logoLink {
display: inline;
/*padding: 16px 0 8px;*/
display: inline-flex;
align-items: center;
height: 100%;
width: 80px;
padding: 16px 0 8px;
padding-left: var(--Spacing-x1);
}
.logo {
width: 80px;
object-fit: fill;
}
.listWrapper {
background-color: #fff;
border-top: 1px solid #e3e0db;
@@ -175,13 +180,8 @@
padding: 15px 15px 15px 5px;
}
.loggedInLogo {
height: 35px;
width: 35px;
border-radius: 50px;
background-color: #000;
.avatarWrapper {
margin-right: 4px;
margin-left: -4px;
}
.mobileLinkButton {
@@ -228,50 +228,56 @@
}
.buttonContainer {
display: flex;
display: inline-flex;
justify-content: flex-end;
align-items: center;
margin-right: 8px;
gap: var(--Spacing-x3);
}
@media screen and (min-width: 950px) {
.myPagesDesktopLink {
display: none;
}
@media (min-width: 1367px) {
.navBar {
grid-template-columns: 140px auto 1fr;
height: 82.4px;
align-content: center;
padding: 0px 0px var(--Spacing-x-quarter) 0px;
overflow: hidden;
}
.logoLink {
display: inline-block;
width: 100%;
padding: 27px 30px 26px 0;
text-align: center;
align-items: center;
}
.mainMenu {
box-shadow: none;
background-color: hsla(0, 0%, 100%, 0.95);
position: relative;
z-index: unset;
height: 85.09px;
height: 82.4px;
}
.container {
padding: 0px 30px;
padding: 0 var(--Spacing-x5) 0 120px;
}
.mainLinks {
padding-top: 2.5px;
background-color: transparent;
}
.navBar {
grid-template-columns: 132.18px 1fr auto;
align-content: center;
padding-bottom: 2px;
overflow: hidden;
height: 100%;
}
.expanderBtn {
display: none;
}
.logoLink {
display: flex;
height: 100%;
padding: 30px 10px 30px 15px;
width: auto;
align-items: center;
}
.logo {
width: 102.17px;
height: 100%;
@@ -282,9 +288,11 @@
border-top: none;
display: flex;
align-items: center;
padding-bottom: 0;
padding-top: 0;
position: static;
width: 100%;
padding-bottom: 0px;
height: 100%;
}
.listWrapper.isOpen {
@@ -292,7 +300,7 @@
}
.li {
display: table-cell;
display: inline-grid;
float: none;
vertical-align: middle;
line-height: 1.15;
@@ -300,10 +308,16 @@
.link {
background-image: none;
font-family: Helvetica, Arial, sans-serif;
font-weight: 700;
line-height: 1.15;
font-family: var(--typography-Body-Regular-fontFamily);
font-size: var(--typography-Body-Regular-fontSize);
font-feature-settings:
"clig" off,
"liga" off;
font-weight: 600;
line-height: 125%;
padding: 30px 15px;
text-transform: uppercase;
color: var(--text-black); /* Design system should return #404040 */
}
.linkRow {
@@ -322,25 +336,11 @@
.buttonContainer {
margin-right: 0;
}
}
@media (min-width: 1200px) {
.mainMenu {
height: 82.4px;
.avatarButton {
display: none;
}
.navBar {
grid-template-columns: 140px auto 1fr;
height: 82.4px;
}
.logoLink {
display: inline-block;
width: 100%;
padding: 27px 30px 26px 0;
text-align: center;
}
.listWrapper {
padding-top: 0;
.myPagesDesktopLink {
display: block;
}
}

View File

@@ -0,0 +1,80 @@
"use client"
import { Fragment } from "react"
import { useIntl } from "react-intl"
import { Lang } from "@/constants/languages"
import { logout } from "@/constants/routes/handleAuth"
import { navigationQueryRouter } from "@/server/routers/contentstack/myPages/navigation/query"
import useDropdownStore from "@/stores/main-menu"
import Divider from "@/components/TempDesignSystem/Divider"
import Link from "@/components/TempDesignSystem/Link"
import Title from "@/components/TempDesignSystem/Text/Title"
import styles from "./my-pages-mobile-dropdown.module.css"
type Navigation = Awaited<ReturnType<(typeof navigationQueryRouter)["get"]>>
export default function MyPagesMobileDropdown({
navigation,
lang,
}: {
navigation: Navigation
lang: Lang | null
}) {
const { formatMessage } = useIntl()
const { toggleMyPagesMobileMenu, isMyPagesMobileMenuOpen } =
useDropdownStore()
if (!navigation) {
return null
}
return (
<nav
className={`${styles.navigationMenu} ${isMyPagesMobileMenuOpen ? styles.navigationMenuIsOpen : ""}`}
>
<Title className={styles.heading} textTransform="capitalize" level="h5">
{navigation.title}
</Title>
{navigation.menuItems.map((menuItem, idx) => (
<Fragment key={`${menuItem.display_sign_out_link}-${idx}`}>
<div className={styles.dividerWrapper}>
<Divider color="subtle" />
</div>
<ul className={styles.dropdownWrapper}>
<ul className={styles.dropdownLinks}>
{menuItem.links.map((link) => (
<li key={link.uid}>
<Link
href={link.originalUrl || link.url}
partialMatch
size={menuItem.display_sign_out_link ? "small" : "regular"}
variant="myPageMobileDropdown"
color="burgundy"
onClick={toggleMyPagesMobileMenu}
>
{link.linkText}
</Link>
</li>
))}
{menuItem.display_sign_out_link && lang ? (
<li>
<Link
color="burgundy"
href={logout[lang]}
prefetch={false}
size="small"
variant="sidebar"
>
{formatMessage({ id: "Log out" })}
</Link>
</li>
) : null}
</ul>
</ul>
</Fragment>
))}
</nav>
)
}

View File

@@ -0,0 +1,63 @@
.navigationMenu {
background-color: #fff;
border-top: 1px solid #e3e0db;
display: none;
list-style: none;
overflow-y: visible;
margin: 0;
padding-inline-start: 0;
}
.navigationMenu.navigationMenuIsOpen {
display: block;
left: 0;
position: absolute;
right: 0;
top: 100%;
}
.dropdownWrapper {
display: flex;
width: 100%;
padding: 20px var(--Spacing-x2);
flex-direction: column;
justify-content: center;
align-items: flex-start;
background-color: var(--Main-Grey-White);
box-shadow:
0px 276px 77px 0px rgba(0, 0, 0, 0),
0px 177px 71px 0px rgba(0, 0, 0, 0.01),
0px 99px 60px 0px rgba(0, 0, 0, 0.05),
0px 44px 44px 0px rgba(0, 0, 0, 0.09),
0px 11px 24px 0px rgba(0, 0, 0, 0.1);
}
.dividerWrapper {
background-color: var(--Main-Grey-White);
padding: 0 var(--Spacing-x2);
margin: auto;
place-content: center;
display: flex;
}
.heading {
padding: 20px var(--Spacing-x2);
}
.dropdownLinks {
display: flex;
flex-direction: column;
gap: var(--Spacing-x-half);
width: 100%;
list-style: none;
}
@media screen and (min-width: 1367px) {
.navigationMenu {
display: none;
}
.navigationMenu.navigationMenuIsOpen {
display: none;
}
}

View File

@@ -2,8 +2,8 @@
display: grid;
}
@media screen and (max-width: 950px) {
@media screen and (max-width: 1366px) {
.header {
height: 50px;
height: 70.047px;
}
}

View File

@@ -15,11 +15,18 @@ import { LangParams } from "@/types/params"
export default async function Header({
lang,
languageSwitcher,
}: LangParams & { languageSwitcher: React.ReactNode }) {
const data = await serverClient().contentstack.base.header({
lang,
})
const session = await auth()
myPagesMobileDropdown,
}: LangParams & { languageSwitcher: React.ReactNode } & {
myPagesMobileDropdown: React.ReactNode
}) {
const [data, session] = await Promise.all([
serverClient().contentstack.base.header({
lang,
}),
auth(),
])
const user = !!session ? await serverClient().user.get() : null
if (!data) {
return null
@@ -49,8 +56,9 @@ export default async function Header({
logo={logo}
topMenuMobileLinks={topMenuMobileLinks}
languageSwitcher={languageSwitcher}
myPagesMobileDropdown={myPagesMobileDropdown}
bookingHref={homeHref}
isLoggedIn={!!session}
user={user}
lang={lang}
/>
</header>

View File

@@ -0,0 +1,27 @@
.avatar {
display: inline-flex;
align-items: center;
justify-content: center;
vertical-align: middle;
overflow: hidden;
cursor: pointer;
width: 35px;
height: 35px;
border-radius: 100%;
background-color: rgba(0, 0, 0, 0.05);
}
.avatarInitialsText {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--Theme-Primary-Dark-Surface-Normal);
color: var(--Theme-Primary-Dark-On-Surface-Text);
font-size: var(--typography-Caption-Bold-fontSize);
font-family: var(--typography-Body-Regular-fontFamily);
font-weight: var(--typography-Caption-Bold-fontWeight);
line-height: 150%;
letter-spacing: 0.096px;
}

View File

@@ -0,0 +1,20 @@
import { getInitials } from "@/utils/user"
import styles from "./avatar.module.css"
import { User } from "@/types/user"
export default function Avatar({
firstName,
lastName,
}: {
firstName: User["firstName"]
lastName: User["lastName"]
}) {
const initials = getInitials(firstName, lastName)
return (
<span className={styles.avatar}>
<span className={styles.avatarInitialsText}>{initials}</span>
</span>
)
}

View File

@@ -17,6 +17,10 @@
border-bottom-color: var(--Theme-Primary-Light-On-Surface-Divider);
}
.subtle {
border-bottom-color: var(--Base-Border-Subtle);
}
.opacity100 {
opacity: 1;
}

View File

@@ -7,6 +7,7 @@ export const dividerVariants = cva(styles.divider, {
color: {
burgundy: styles.burgundy,
peach: styles.peach,
subtle: styles.subtle,
},
opacity: {
100: styles.opacity100,

View File

@@ -29,6 +29,33 @@
text-decoration: underline;
}
.myPageMobileDropdown {
color: var(--Scandic-Brand-Burgundy);
font-family: var(--typography-Body-Regular-fontFamily);
font-size: var(--typography-Body-Regular-fontSize);
line-height: var(--typography-Body-Regular-lineHeight);
letter-spacing: 0.096px;
padding: var(--Spacing-x1) var(--Spacing-x1) var(--Spacing-x1)
var(--Spacing-x-one-and-half);
width: 100%;
border-radius: var(--Corner-radius-Medium);
gap: 4px;
display: flex;
align-items: center;
gap: 4px;
align-self: stretch;
}
.myPageMobileDropdown.active {
background-color: var(--Scandic-Brand-Pale-Peach);
border-radius: var(--Corner-radius-Medium);
font-family: var(--typography-Body-Underlined-fontFamily);
font-size: var(--typography-Body-Underlined-fontSize);
font-weight: var(--typography-Body-Underlined-fontWeight);
letter-spacing: var(--typography-Body-Underlined-letterSpacing);
line-height: var(--typography-Body-Underlined-lineHeight);
}
.shortcut {
align-items: center;
border-bottom: 0.5px solid var(--Scandic-Beige-20);

View File

@@ -23,6 +23,7 @@ export const linkVariants = cva(styles.link, {
default: styles.default,
icon: styles.icon,
myPage: styles.myPage,
myPageMobileDropdown: styles.myPageMobileDropdown,
shortcut: styles.shortcut,
sidebar: styles.sidebar,
},

View File

@@ -68,6 +68,10 @@
text-decoration: var(--typography-Title-5-textDecoration);
}
.capitalize {
text-transform: capitalize;
}
.regular {
text-transform: none;
}

View File

@@ -16,6 +16,7 @@ const config = {
left: styles.left,
},
textTransform: {
capitalize: styles.capitalize,
regular: styles.regular,
uppercase: styles.uppercase,
},

31
stores/main-menu.ts Normal file
View File

@@ -0,0 +1,31 @@
import { create } from "zustand"
interface DropdownState {
isHamburgerMenuOpen: boolean
isMyPagesMobileMenuOpen: boolean
toggleHamburgerMenu: () => void
toggleMyPagesMobileMenu: () => void
}
const useDropdownStore = create<DropdownState>((set) => ({
isHamburgerMenuOpen: false,
isMyPagesMobileMenuOpen: false,
toggleHamburgerMenu: () =>
set((state) => {
// Close the other dropdown if it's open
if (!state.isHamburgerMenuOpen && state.isMyPagesMobileMenuOpen) {
set({ isMyPagesMobileMenuOpen: false })
}
return { isHamburgerMenuOpen: !state.isHamburgerMenuOpen }
}),
toggleMyPagesMobileMenu: () =>
set((state) => {
// Close the other dropdown if it's open
if (!state.isMyPagesMobileMenuOpen && state.isHamburgerMenuOpen) {
set({ isHamburgerMenuOpen: false })
}
return { isMyPagesMobileMenuOpen: !state.isMyPagesMobileMenuOpen }
}),
}))
export default useDropdownStore

View File

@@ -5,6 +5,7 @@ import type {
CurrentHeaderLink,
TopMenuHeaderLink,
} from "@/types/requests/currentHeader"
import { User } from "@/types/user"
export type MainMenuProps = {
frontpageLinkText: string
@@ -13,7 +14,8 @@ export type MainMenuProps = {
logo: Image
topMenuMobileLinks: TopMenuHeaderLink[]
languageSwitcher: React.ReactNode | null
myPagesMobileDropdown: React.ReactNode | null
bookingHref: string
isLoggedIn: boolean
user: User | null
lang: Lang
}

View File

@@ -6,3 +6,13 @@ export function getMembership(memberships: User["memberships"]) {
membership.membershipType.toLowerCase() === "guestpr" || "scandicfriend's"
)
}
export function getInitials(
firstName: User["firstName"],
lastName: User["lastName"]
) {
if (!firstName || !lastName) return null
const firstInitial = firstName.charAt(0).toUpperCase()
const lastInitial = lastName.charAt(0).toUpperCase()
return `${firstInitial}${lastInitial}`
}