diff --git a/app/globals.css b/app/globals.css index f4dec9ed1..480f72fc9 100644 --- a/app/globals.css +++ b/app/globals.css @@ -121,7 +121,6 @@ html, body { margin: 0; padding: 0; - scroll-behavior: smooth; } body { diff --git a/components/Current/Footer/index.tsx b/components/Current/Footer/index.tsx index e21afaeb4..83860cae7 100644 --- a/components/Current/Footer/index.tsx +++ b/components/Current/Footer/index.tsx @@ -8,7 +8,7 @@ import Navigation from "./Navigation" import styles from "./footer.module.css" export default async function Footer() { - const footerData = await serverClient().contentstack.base.footer({ + const footerData = await serverClient().contentstack.base.currentFooter({ lang: getLang(), }) if (!footerData) { diff --git a/components/Current/Header/MainMenu/index.tsx b/components/Current/Header/MainMenu/index.tsx index eed0317ee..e8f41a12e 100644 --- a/components/Current/Header/MainMenu/index.tsx +++ b/components/Current/Header/MainMenu/index.tsx @@ -18,6 +18,7 @@ import LoginButton from "../LoginButton" import styles from "./mainMenu.module.css" import type { MainMenuProps } from "@/types/components/current/header/mainMenu" +import { DropdownTypeEnum } from "@/types/components/dropdown/dropdown" export function MainMenu({ frontpageLinkText, @@ -61,12 +62,8 @@ export function MainMenu({ "/sv/current-content-page", ].includes(pathname) - const { - isHamburgerMenuOpen, - isMyPagesMobileMenuOpen, - toggleHamburgerMenu, - toggleMyPagesMobileMenu, - } = useDropdownStore() + const { toggleDropdown, isMyPagesMobileMenuOpen, isHamburgerMenuOpen } = + useDropdownStore() function handleMyPagesMobileMenuClick() { // Only track click when opening it @@ -74,7 +71,7 @@ export function MainMenu({ trackClick("profile picture icon") } - toggleMyPagesMobileMenu() + toggleDropdown(DropdownTypeEnum.MyPagesMobileMenu) } return ( @@ -89,7 +86,7 @@ export function MainMenu({ - + { if (event.key === "Escape" && isMyPagesMenuOpen) { - toggleMyPagesMenu() + toggleDropdown(DropdownTypeEnum.MyPagesMenu) } }) return (
- + toggleDropdown(DropdownTypeEnum.MyPagesMenu)} + > {intl.formatMessage({ id: "Hi" })} {user.firstName}! @@ -51,7 +54,9 @@ export default function MyPagesMenu({ navigation={navigation} user={user} membership={membership} - toggleOpenStateFn={toggleMyPagesMenu} + toggleOpenStateFn={() => + toggleDropdown(DropdownTypeEnum.MyPagesMenu) + } />
) : null} diff --git a/components/Header/MainMenu/MyPagesMobileMenu/index.tsx b/components/Header/MainMenu/MyPagesMobileMenu/index.tsx index 29a8023a1..6620c87d8 100644 --- a/components/Header/MainMenu/MyPagesMobileMenu/index.tsx +++ b/components/Header/MainMenu/MyPagesMobileMenu/index.tsx @@ -14,6 +14,7 @@ import MyPagesMenuContent from "../MyPagesMenuContent" import styles from "./myPagesMobileMenu.module.css" +import { DropdownTypeEnum } from "@/types/components/dropdown/dropdown" import type { MyPagesMenuProps } from "@/types/components/header/myPagesMenu" export default function MyPagesMobileMenu({ @@ -22,12 +23,11 @@ export default function MyPagesMobileMenu({ user, }: MyPagesMenuProps) { const intl = useIntl() - const { toggleMyPagesMobileMenu, isMyPagesMobileMenuOpen } = - useDropdownStore() + const { isMyPagesMobileMenuOpen, toggleDropdown } = useDropdownStore() useHandleKeyUp((event: KeyboardEvent) => { if (event.key === "Escape" && isMyPagesMobileMenuOpen) { - toggleMyPagesMobileMenu() + toggleDropdown(DropdownTypeEnum.MyPagesMobileMenu) } }) @@ -35,7 +35,7 @@ export default function MyPagesMobileMenu({
toggleDropdown(DropdownTypeEnum.MyPagesMobileMenu)} aria-label={intl.formatMessage({ id: "Open my pages menu" })} > @@ -49,7 +49,9 @@ export default function MyPagesMobileMenu({ membership={membership} navigation={navigation} user={user} - toggleOpenStateFn={toggleMyPagesMobileMenu} + toggleOpenStateFn={() => + toggleDropdown(DropdownTypeEnum.MyPagesMobileMenu) + } />
diff --git a/components/LanguageSwitcher/LanguageSwitcherContent/index.tsx b/components/LanguageSwitcher/LanguageSwitcherContent/index.tsx index 7a20e3da1..9aebfa711 100644 --- a/components/LanguageSwitcher/LanguageSwitcherContent/index.tsx +++ b/components/LanguageSwitcher/LanguageSwitcherContent/index.tsx @@ -13,6 +13,7 @@ import { useTrapFocus } from "@/hooks/useTrapFocus" import styles from "./languageSwitcherContent.module.css" +import { DropdownTypeEnum } from "@/types/components/dropdown/dropdown" import type { LanguageSwitcherProps } from "@/types/components/languageSwitcher/languageSwitcher" export default function LanguageSwitcherContent({ @@ -21,9 +22,13 @@ export default function LanguageSwitcherContent({ }: LanguageSwitcherProps) { const intl = useIntl() const currentLanguage = useLang() - const { toggleLanguageSwitcher } = useDropdownStore() + const { toggleDropdown } = useDropdownStore() const languageSwitcherRef = useTrapFocus() const urlKeys = Object.keys(urls) as Lang[] + const position = + type === "footer" + ? DropdownTypeEnum.FooterLanguageSwitcher + : DropdownTypeEnum.HamburgerMenu return (
@@ -32,7 +37,7 @@ export default function LanguageSwitcherContent({ diff --git a/components/LanguageSwitcher/languageSwitcher.module.css b/components/LanguageSwitcher/languageSwitcher.module.css index 89ce55b56..6c85a22aa 100644 --- a/components/LanguageSwitcher/languageSwitcher.module.css +++ b/components/LanguageSwitcher/languageSwitcher.module.css @@ -1,6 +1,5 @@ .button { background-color: transparent; - color: var(--Base-Text-High-contrast); font-family: var(--typography-Caption-Regular-fontFamily); font-size: var(--typography-Caption-Regular-fontSize); border-width: 0; @@ -13,6 +12,14 @@ width: 100%; } +.burgundy .button { + color: var(--Base-Text-High-contrast); +} + +.pale .button { + color: var(--Primary-Dark-On-Surface-Text); +} + .chevron { justify-self: end; transition: transform 0.3s; @@ -45,29 +52,43 @@ .dropdown { position: absolute; - top: 2.25rem; background-color: var(--Base-Surface-Primary-light-Normal); border-radius: var(--Corner-radius-Large); box-shadow: 0px 0px 14px 6px #0000001a; display: none; min-width: 12.5rem; z-index: 1; + } + .top .dropdown { + top: 2.25rem; bottom: auto; } - /* Triangle above dropdown */ + .top .dropdown::before { + top: -1.25rem; + transform: rotate(180deg); + } + + /* Triangle dropdown */ .dropdown::before { content: ""; position: absolute; - top: -1.25rem; right: 2.4rem; - transform: rotate(180deg); border-width: 0.75rem; border-style: solid; border-color: var(--Base-Surface-Primary-light-Normal) transparent transparent transparent; } + .bottom .dropdown { + top: auto; + bottom: 2.25rem; + } + + .bottom .dropdown::before { + top: 100%; + } + .button { grid-template-columns: repeat(3, max-content); font-size: var(--typography-Body-Bold-fontSize); diff --git a/components/LanguageSwitcher/variants.ts b/components/LanguageSwitcher/variants.ts new file mode 100644 index 000000000..0fac0e5da --- /dev/null +++ b/components/LanguageSwitcher/variants.ts @@ -0,0 +1,20 @@ +import { cva } from "class-variance-authority" + +import styles from "./languageSwitcher.module.css" + +export const languageSwitcherVariants = cva(styles.languageSwitcher, { + variants: { + color: { + burgundy: styles.burgundy, + pale: styles.pale, + }, + position: { + header: styles.top, + footer: styles.bottom, + }, + defaultVariants: { + color: "burgundy", + position: "top", + }, + }, +}) diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index 9cd58ebbe..fc8186ab8 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -44,6 +44,7 @@ "Compare all levels": "Sammenlign alle niveauer", "Contact us": "Kontakt os", "Continue": "Blive ved", + "Copyright all rights reserved": "Scandic AB Alle rettigheder forbeholdes", "Could not find requested resource": "Kunne ikke finde den anmodede ressource", "Country": "Land", "Country code": "Landekode", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index 79b9d5940..9092608b6 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -42,6 +42,7 @@ "Compare all levels": "Vergleichen Sie alle Levels", "Contact us": "Kontaktieren Sie uns", "Continue": "Weitermachen", + "Copyright all rights reserved": "Scandic AB Alle Rechte vorbehalten", "Could not find requested resource": "Die angeforderte Ressource konnte nicht gefunden werden.", "Country": "Land", "Country code": "Landesvorwahl", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index cf4620b0b..d08f7fd8b 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -43,6 +43,7 @@ "Compare all levels": "Compare all levels", "Contact us": "Contact us", "Continue": "Continue", + "Copyright all rights reserved": "Scandic AB All rights reserved", "Could not find requested resource": "Could not find requested resource", "Country": "Country", "Country code": "Country code", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index eac9da8c2..b1d91abe3 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -43,6 +43,7 @@ "Compare all levels": "Vertaa kaikkia tasoja", "Contact us": "Ota meihin yhteyttä", "Continue": "Jatkaa", + "Copyright all rights reserved": "Scandic AB Kaikki oikeudet pidätetään", "Could not find requested resource": "Pyydettyä resurssia ei löytynyt", "Country": "Maa", "Country code": "Maatunnus", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index 1960369b7..9d66f5df8 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -43,6 +43,7 @@ "Compare all levels": "Sammenlign alle nivåer", "Contact us": "Kontakt oss", "Continue": "Fortsette", + "Copyright all rights reserved": "Scandic AB Alle rettigheter forbeholdt", "Could not find requested resource": "Kunne ikke finne den forespurte ressursen", "Country": "Land", "Country code": "Landskode", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index 3254a6a2f..be1e56601 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -43,6 +43,7 @@ "Compare all levels": "Jämför alla nivåer", "Contact us": "Kontakta oss", "Continue": "Fortsätt", + "Copyright all rights reserved": "Scandic AB Alla rättigheter förbehålls", "Could not find requested resource": "Det gick inte att hitta den begärda resursen", "Country": "Land", "Country code": "Landskod", diff --git a/lib/graphql/Fragments/CurrentFooter/AppDownloads.graphql b/lib/graphql/Fragments/CurrentFooter/AppDownloads.graphql new file mode 100644 index 000000000..20b7d112f --- /dev/null +++ b/lib/graphql/Fragments/CurrentFooter/AppDownloads.graphql @@ -0,0 +1,27 @@ +#import "../Image.graphql" + +fragment AppDownloads on CurrentFooter { + app_downloads { + title + app_store { + href + imageConnection { + edges { + node { + ...Image + } + } + } + } + google_play { + href + imageConnection { + edges { + node { + ...Image + } + } + } + } + } +} diff --git a/lib/graphql/Fragments/Footer/Logo.graphql b/lib/graphql/Fragments/CurrentFooter/Logo.graphql similarity index 100% rename from lib/graphql/Fragments/Footer/Logo.graphql rename to lib/graphql/Fragments/CurrentFooter/Logo.graphql diff --git a/lib/graphql/Fragments/CurrentFooter/MainLinks.graphql b/lib/graphql/Fragments/CurrentFooter/MainLinks.graphql new file mode 100644 index 000000000..62abc0eb1 --- /dev/null +++ b/lib/graphql/Fragments/CurrentFooter/MainLinks.graphql @@ -0,0 +1,29 @@ +fragment MainLinks on Footer { + main_links { + title + open_in_new_tab + link { + href + title + } + pageConnection { + edges { + node { + __typename + ... on AccountPage { + title + url + } + ... on LoyaltyPage { + title + url + } + ... on ContentPage { + title + url + } + } + } + } + } +} diff --git a/lib/graphql/Fragments/Footer/Navigation.graphql b/lib/graphql/Fragments/CurrentFooter/Navigation.graphql similarity index 100% rename from lib/graphql/Fragments/Footer/Navigation.graphql rename to lib/graphql/Fragments/CurrentFooter/Navigation.graphql diff --git a/lib/graphql/Fragments/CurrentFooter/Refs/MainLinks.graphql b/lib/graphql/Fragments/CurrentFooter/Refs/MainLinks.graphql new file mode 100644 index 000000000..cea5340f4 --- /dev/null +++ b/lib/graphql/Fragments/CurrentFooter/Refs/MainLinks.graphql @@ -0,0 +1,18 @@ +fragment MainLinksRef on Footer { + __typename + main_links { + pageConnection { + edges { + node { + __typename + ...LoyaltyPageRef + ...ContentPageRef + ...AccountPageRef + } + } + } + } + system { + ...System + } +} diff --git a/lib/graphql/Fragments/CurrentFooter/Refs/SecondaryLinks.graphql b/lib/graphql/Fragments/CurrentFooter/Refs/SecondaryLinks.graphql new file mode 100644 index 000000000..d324e40b1 --- /dev/null +++ b/lib/graphql/Fragments/CurrentFooter/Refs/SecondaryLinks.graphql @@ -0,0 +1,20 @@ +fragment SecondaryLinksRef on Footer { + __typename + secondary_links { + links { + pageConnection { + edges { + node { + __typename + ...LoyaltyPageRef + ...ContentPageRef + ...AccountPageRef + } + } + } + } + } + system { + ...System + } +} diff --git a/lib/graphql/Fragments/CurrentFooter/SecondaryLinks.graphql b/lib/graphql/Fragments/CurrentFooter/SecondaryLinks.graphql new file mode 100644 index 000000000..e6724cd7f --- /dev/null +++ b/lib/graphql/Fragments/CurrentFooter/SecondaryLinks.graphql @@ -0,0 +1,24 @@ +#import "../Refs/MyPages/AccountPage.graphql" +#import "../Refs/ContentPage/ContentPage.graphql" +#import "../Refs/LoyaltyPage/LoyaltyPage.graphql" + +fragment SecondaryLinks on Footer { + secondary_links { + title + links { + title + open_in_new_tab + pageConnection { + edges { + node { + __typename + } + } + } + link { + href + title + } + } + } +} diff --git a/lib/graphql/Fragments/CurrentFooter/SocialMedia.graphql b/lib/graphql/Fragments/CurrentFooter/SocialMedia.graphql new file mode 100644 index 000000000..55cf49515 --- /dev/null +++ b/lib/graphql/Fragments/CurrentFooter/SocialMedia.graphql @@ -0,0 +1,17 @@ +fragment SocialMedia on CurrentFooter { + social_media { + title + facebook { + href + title + } + instagram { + href + title + } + twitter { + href + title + } + } +} diff --git a/lib/graphql/Fragments/Footer/TripAdvisor.graphql b/lib/graphql/Fragments/CurrentFooter/TripAdvisor.graphql similarity index 100% rename from lib/graphql/Fragments/Footer/TripAdvisor.graphql rename to lib/graphql/Fragments/CurrentFooter/TripAdvisor.graphql diff --git a/lib/graphql/Fragments/Footer/AppDownloads.graphql b/lib/graphql/Fragments/Footer/AppDownloads.graphql index 20b7d112f..f9e2f7d40 100644 --- a/lib/graphql/Fragments/Footer/AppDownloads.graphql +++ b/lib/graphql/Fragments/Footer/AppDownloads.graphql @@ -1,26 +1,11 @@ -#import "../Image.graphql" - -fragment AppDownloads on CurrentFooter { +fragment AppDownloads on Footer { app_downloads { title - app_store { - href - imageConnection { - edges { - node { - ...Image - } - } - } - } - google_play { - href - imageConnection { - edges { - node { - ...Image - } - } + links { + type + href { + href + title } } } diff --git a/lib/graphql/Fragments/Footer/Refs/TertiaryLinks.graphql b/lib/graphql/Fragments/Footer/Refs/TertiaryLinks.graphql new file mode 100644 index 000000000..d1fd47864 --- /dev/null +++ b/lib/graphql/Fragments/Footer/Refs/TertiaryLinks.graphql @@ -0,0 +1,22 @@ +#import "../../Refs/LoyaltyPage/LoyaltyPage.graphql" +#import "../../Refs/MyPages/AccountPage.graphql" +#import "../../Refs/ContentPage/ContentPage.graphql" + +fragment TertiaryLinksRef on Footer { + __typename + tertiary_links { + pageConnection { + edges { + node { + __typename + ...LoyaltyPageRef + ...ContentPageRef + ...AccountPageRef + } + } + } + } + system { + ...System + } +} diff --git a/lib/graphql/Fragments/Footer/SocialMedia.graphql b/lib/graphql/Fragments/Footer/SocialMedia.graphql index 55cf49515..02947ae5d 100644 --- a/lib/graphql/Fragments/Footer/SocialMedia.graphql +++ b/lib/graphql/Fragments/Footer/SocialMedia.graphql @@ -1,17 +1,11 @@ -fragment SocialMedia on CurrentFooter { +fragment SocialMedia on Footer { social_media { - title - facebook { - href - title - } - instagram { - href - title - } - twitter { - href - title + links { + href { + href + title + } + type } } } diff --git a/lib/graphql/Query/CurrentFooter.graphql b/lib/graphql/Query/CurrentFooter.graphql index 5ec7f6bc9..325f7c04e 100644 --- a/lib/graphql/Query/CurrentFooter.graphql +++ b/lib/graphql/Query/CurrentFooter.graphql @@ -1,8 +1,8 @@ -#import "../Fragments/Footer/AppDownloads.graphql" -#import "../Fragments/Footer/Logo.graphql" -#import "../Fragments/Footer/Navigation.graphql" -#import "../Fragments/Footer/SocialMedia.graphql" -#import "../Fragments/Footer/TripAdvisor.graphql" +#import "../Fragments/CurrentFooter/AppDownloads.graphql" +#import "../Fragments/CurrentFooter/Logo.graphql" +#import "../Fragments/CurrentFooter/Navigation.graphql" +#import "../Fragments/CurrentFooter/SocialMedia.graphql" +#import "../Fragments/CurrentFooter/TripAdvisor.graphql" #import "../Fragments/Refs/System.graphql" query GetCurrentFooter($locale: String!) { diff --git a/lib/graphql/Query/Footer.graphql b/lib/graphql/Query/Footer.graphql new file mode 100644 index 000000000..a1ca752fe --- /dev/null +++ b/lib/graphql/Query/Footer.graphql @@ -0,0 +1,122 @@ +#import "../Fragments/Refs/System.graphql" + +#import "../Fragments/PageLink/AccountPageLink.graphql" +#import "../Fragments/PageLink/ContentPageLink.graphql" +#import "../Fragments/PageLink/HotelPageLink.graphql" +#import "../Fragments/PageLink/LoyaltyPageLink.graphql" + +#import "../Fragments/Refs/ContentPage/ContentPage.graphql" +#import "../Fragments/Refs/HotelPage/HotelPage.graphql" +#import "../Fragments/Refs/LoyaltyPage/LoyaltyPage.graphql" +#import "../Fragments/Refs/MyPages/AccountPage.graphql" + +#import "../Fragments/Footer/AppDownloads.graphql" +#import "../Fragments/Footer/SocialMedia.graphql" + +query GetFooter($locale: String!) { + all_footer(limit: 1, locale: $locale) { + items { + main_links { + title + open_in_new_tab + link { + href + title + } + pageConnection { + edges { + node { + ...ContentPageLink + ...HotelPageLink + ...LoyaltyPageLink + } + } + } + } + secondary_links { + title + links { + title + open_in_new_tab + pageConnection { + edges { + node { + ...ContentPageLink + ...HotelPageLink + ...LoyaltyPageLink + } + } + } + link { + href + title + } + } + } + tertiary_links { + title + open_in_new_tab + link { + href + title + } + pageConnection { + edges { + node { + ...ContentPageLink + ...HotelPageLink + ...LoyaltyPageLink + } + } + } + } + ...AppDownloads + ...SocialMedia + } + } +} + +query GetFooterRef($locale: String!) { + all_footer(limit: 1, locale: $locale) { + items { + main_links { + pageConnection { + edges { + node { + ...ContentPageRef + ...HotelPageRef + ...LoyaltyPageRef + } + } + } + } + secondary_links { + links { + pageConnection { + edges { + node { + ...ContentPageRef + ...HotelPageRef + ...LoyaltyPageRef + } + } + } + } + } + tertiary_links { + pageConnection { + edges { + node { + ...ContentPageRef + ...HotelPageRef + ...LoyaltyPageRef + } + } + } + } + system { + ...System + } + } + } +} diff --git a/package-lock.json b/package-lock.json index 763cd1b67..cae0b620f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,6 +36,7 @@ "graphql": "^16.8.1", "graphql-request": "^6.1.0", "graphql-tag": "^2.12.6", + "immer": "10.1.1", "libphonenumber-js": "^1.10.60", "next": "^14.2.3", "next-auth": "^5.0.0-beta.19", @@ -10974,6 +10975,15 @@ "node": ">=0.10.0" } }, + "node_modules/immer": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", + "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/immutable": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.6.tgz", diff --git a/package.json b/package.json index 0331f0834..963a2a49a 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "graphql": "^16.8.1", "graphql-request": "^6.1.0", "graphql-tag": "^2.12.6", + "immer": "10.1.1", "libphonenumber-js": "^1.10.60", "next": "^14.2.3", "next-auth": "^5.0.0-beta.19", diff --git a/public/_static/img/store-badges/app-store-badge-de.svg b/public/_static/img/store-badges/app-store-badge-de.svg new file mode 100644 index 000000000..cbe9e530d --- /dev/null +++ b/public/_static/img/store-badges/app-store-badge-de.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/public/_static/img/store-badges/app-store-badge-dk.svg b/public/_static/img/store-badges/app-store-badge-dk.svg new file mode 100644 index 000000000..1346da85b --- /dev/null +++ b/public/_static/img/store-badges/app-store-badge-dk.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/public/_static/img/app-store-badge.svg b/public/_static/img/store-badges/app-store-badge-en.svg similarity index 100% rename from public/_static/img/app-store-badge.svg rename to public/_static/img/store-badges/app-store-badge-en.svg diff --git a/public/_static/img/store-badges/app-store-badge-fi.svg b/public/_static/img/store-badges/app-store-badge-fi.svg new file mode 100644 index 000000000..43a0ed481 --- /dev/null +++ b/public/_static/img/store-badges/app-store-badge-fi.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/public/_static/img/store-badges/app-store-badge-no.svg b/public/_static/img/store-badges/app-store-badge-no.svg new file mode 100644 index 000000000..5986e0d29 --- /dev/null +++ b/public/_static/img/store-badges/app-store-badge-no.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/_static/img/store-badges/app-store-badge-se.svg b/public/_static/img/store-badges/app-store-badge-se.svg new file mode 100644 index 000000000..d6122c75e --- /dev/null +++ b/public/_static/img/store-badges/app-store-badge-se.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/public/_static/img/store-badges/google-play-badge-de.svg b/public/_static/img/store-badges/google-play-badge-de.svg new file mode 100644 index 000000000..b0380fc96 --- /dev/null +++ b/public/_static/img/store-badges/google-play-badge-de.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/_static/img/store-badges/google-play-badge-dk.svg b/public/_static/img/store-badges/google-play-badge-dk.svg new file mode 100644 index 000000000..88a123aee --- /dev/null +++ b/public/_static/img/store-badges/google-play-badge-dk.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/_static/img/google-play-badge.svg b/public/_static/img/store-badges/google-play-badge-en.svg similarity index 100% rename from public/_static/img/google-play-badge.svg rename to public/_static/img/store-badges/google-play-badge-en.svg diff --git a/public/_static/img/store-badges/google-play-badge-fi.svg b/public/_static/img/store-badges/google-play-badge-fi.svg new file mode 100644 index 000000000..d06057408 --- /dev/null +++ b/public/_static/img/store-badges/google-play-badge-fi.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/_static/img/store-badges/google-play-badge-no.svg b/public/_static/img/store-badges/google-play-badge-no.svg new file mode 100644 index 000000000..64bc35727 --- /dev/null +++ b/public/_static/img/store-badges/google-play-badge-no.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/_static/img/store-badges/google-play-badge-se.svg b/public/_static/img/store-badges/google-play-badge-se.svg new file mode 100644 index 000000000..669140670 --- /dev/null +++ b/public/_static/img/store-badges/google-play-badge-se.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/server/routers/contentstack/base/output.ts b/server/routers/contentstack/base/output.ts index 983a024ed..9b015969f 100644 --- a/server/routers/contentstack/base/output.ts +++ b/server/routers/contentstack/base/output.ts @@ -175,7 +175,7 @@ const validateNavigationItem = z.object({ export type NavigationItem = z.infer -export const validateFooterConfigSchema = z.object({ +export const validateCurrentFooterConfigSchema = z.object({ all_current_footer: z.object({ items: z.array( z.object({ @@ -242,16 +242,18 @@ export const validateFooterConfigSchema = z.object({ }), }) -export type FooterDataRaw = z.infer +export type CurrentFooterDataRaw = z.infer< + typeof validateCurrentFooterConfigSchema +> -export type FooterData = Omit< - FooterDataRaw["all_current_footer"]["items"][0], +export type CurrentFooterData = Omit< + CurrentFooterDataRaw["all_current_footer"]["items"][0], "logoConnection" > & { logo: Image } -const validateFooterRefConfigSchema = z.object({ +const validateCurrentFooterRefConfigSchema = z.object({ all_current_footer: z.object({ items: z.array( z.object({ @@ -264,7 +266,170 @@ const validateFooterRefConfigSchema = z.object({ }), }) -export type FooterRefDataRaw = z.infer +export type CurrentFooterRefDataRaw = z.infer< + typeof validateCurrentFooterRefConfigSchema +> + +const validateExternalLink = z + .object({ + href: z.string(), + title: z.string(), + }) + .optional() + +const validateInternalLink = z + .object({ + edges: z + .array( + z.object({ + node: z.object({ + system: z.object({ + uid: z.string(), + locale: z.nativeEnum(Lang), + }), + url: z.string(), + title: z.string(), + web: z + .object({ + original_url: z.string(), + }) + .optional(), + }), + }) + ) + .max(1), + }) + .transform((data) => { + const node = data.edges[0]?.node + if (!node) { + return null + } + const url = node.url + const originalUrl = node.web?.original_url + const lang = node.system.locale + + return { + url: originalUrl ?? removeMultipleSlashes(`/${lang}/${url}`), + title: node.title, + } + }) + .optional() + +export const validateLinkItem = z + .object({ + title: z.string(), + open_in_new_tab: z.boolean(), + link: validateExternalLink, + pageConnection: validateInternalLink, + }) + .transform((data) => { + return { + url: data.pageConnection?.url ?? data.link?.href ?? "", + title: data?.title ?? data.link?.title, + openInNewTab: data.open_in_new_tab, + isExternal: !!data.link?.href, + } + }) + +export const validateSecondaryLinks = z.array( + z.object({ + title: z.string(), + links: z.array(validateLinkItem), + }) +) + +export const validateLinksWithType = z.array( + z.object({ + type: z.string(), + href: validateExternalLink, + }) +) + +export const validateFooterConfigSchema = z + .object({ + all_footer: z.object({ + items: z.array( + z.object({ + main_links: z.array(validateLinkItem), + app_downloads: z.object({ + title: z.string(), + links: validateLinksWithType, + }), + secondary_links: validateSecondaryLinks, + social_media: z.object({ + links: validateLinksWithType, + }), + tertiary_links: z.array(validateLinkItem), + }) + ), + }), + }) + .transform((data) => { + const { + main_links, + app_downloads, + secondary_links, + social_media, + tertiary_links, + } = data.all_footer.items[0] + + return { + mainLinks: main_links, + appDownloads: app_downloads, + secondaryLinks: secondary_links, + socialMedia: social_media, + tertiaryLinks: tertiary_links, + } + }) + +const pageConnectionRefs = z.object({ + edges: z + .array( + z.object({ + node: z.object({ + system: z.object({ + content_type_uid: z.string(), + uid: z.string(), + }), + }), + }) + ) + .max(1), +}) + +export const validateFooterRefConfigSchema = z.object({ + all_footer: z.object({ + items: z + .array( + z.object({ + main_links: z.array( + z.object({ + pageConnection: pageConnectionRefs, + }) + ), + secondary_links: z.array( + z.object({ + links: z.array( + z.object({ + pageConnection: pageConnectionRefs, + }) + ), + }) + ), + tertiary_links: z.array( + z.object({ + pageConnection: pageConnectionRefs, + }) + ), + system: z.object({ + content_type_uid: z.string(), + uid: z.string(), + }), + }) + ) + .length(1), + }), +}) const linkConnectionNodeSchema = z .object({ @@ -333,7 +498,7 @@ const cardButtonSchema = z const href = isContentstackLink && externalLink.href ? externalLink.href - : linkConnectionData?.href || "" + : linkConnectionData?.href ?? "" return { openInNewTab: data.open_in_new_tab, diff --git a/server/routers/contentstack/base/query.ts b/server/routers/contentstack/base/query.ts index aa530e03c..c2f0c948a 100644 --- a/server/routers/contentstack/base/query.ts +++ b/server/routers/contentstack/base/query.ts @@ -9,6 +9,7 @@ import { GetCurrentHeader, GetCurrentHeaderRef, } from "@/lib/graphql/Query/CurrentHeader.graphql" +import { GetFooter, GetFooterRef } from "@/lib/graphql/Query/Footer.graphql" import { GetHeader, GetHeaderRef } from "@/lib/graphql/Query/Header.graphql" import { request } from "@/lib/graphql/request" import { notFound } from "@/server/errors/trpc" @@ -23,19 +24,25 @@ import { import { langInput } from "./input" import { type ContactConfigData, + CurrentFooterDataRaw, + CurrentFooterRefDataRaw, CurrentHeaderData, CurrentHeaderDataRaw, CurrentHeaderRefDataRaw, - FooterDataRaw, - FooterRefDataRaw, getHeaderRefSchema, getHeaderSchema, validateContactConfigSchema, + validateCurrentFooterConfigSchema, validateCurrentHeaderConfigSchema, validateFooterConfigSchema, + validateFooterRefConfigSchema, } from "./output" -import { getConnections } from "./utils" +import { getConnections, getFooterConnections } from "./utils" +import type { + FooterDataRaw, + FooterRefDataRaw, +} from "@/types/components/footer/footer" import type { HeaderRefResponse, HeaderResponse } from "@/types/header" const meter = metrics.getMeter("trpc.contentstack.base") @@ -86,6 +93,27 @@ const getHeaderSuccessCounter = meter.createCounter( const getHeaderFailCounter = meter.createCounter( "trpc.contentstack.header.get-fail" ) + +// OpenTelemetry metrics: CurrentHeader +const getCurrentFooterRefCounter = meter.createCounter( + "trpc.contentstack.currentFooter.ref.get" +) +const getCurrentFooterRefSuccessCounter = meter.createCounter( + "trpc.contentstack.currentFooter.ref.get-success" +) +const getCurrentFooterRefFailCounter = meter.createCounter( + "trpc.contentstack.currentFooter.ref.get-fail" +) +const getCurrentFooterCounter = meter.createCounter( + "trpc.contentstack.currentFooter.get" +) +const getCurrentFooterSuccessCounter = meter.createCounter( + "trpc.contentstack.currentFooter.get-success" +) +const getCurrentFooterFailCounter = meter.createCounter( + "trpc.contentstack.currentFooter.get-fail" +) + // OpenTelemetry metrics: Footer const getFooterRefCounter = meter.createCounter( "trpc.contentstack.footer.ref.get" @@ -396,15 +424,15 @@ export const baseQueryRouter = router({ logo, } as CurrentHeaderData }), - footer: contentstackBaseProcedure + currentFooter: contentstackBaseProcedure .input(langInput) .query(async ({ input }) => { - getFooterRefCounter.add(1, { lang: input.lang }) + getCurrentFooterRefCounter.add(1, { lang: input.lang }) console.info( - "contentstack.footer.ref start", + "contentstack.currentFooter.ref start", JSON.stringify({ query: { lang: input.lang } }) ) - const responseRef = await request( + const responseRef = await request( GetCurrentFooterRef, { locale: input.lang, @@ -417,9 +445,9 @@ export const baseQueryRouter = router({ } ) // There's currently no error handling/validation for the responseRef, should it be added? - getFooterCounter.add(1, { lang: input.lang }) + getCurrentFooterCounter.add(1, { lang: input.lang }) console.info( - "contentstack.footer start", + "contentstack.currentFooter start", JSON.stringify({ query: { lang: input.lang, @@ -428,13 +456,13 @@ export const baseQueryRouter = router({ ) const currentFooterUID = responseRef.data.all_current_footer.items[0].system.uid - const response = await request( + + const response = await request( GetCurrentFooter, { locale: input.lang, }, { - cache: "force-cache", next: { tags: [generateTag(input.lang, currentFooterUID)], }, @@ -443,13 +471,13 @@ export const baseQueryRouter = router({ if (!response.data) { const notFoundError = notFound(response) - getFooterFailCounter.add(1, { + getCurrentFooterFailCounter.add(1, { lang: input.lang, error_type: "not_found", error: JSON.stringify({ code: notFoundError.code }), }) console.error( - "contentstack.footer not found error", + "contentstack.currentFooter not found error", JSON.stringify({ query: { lang: input.lang, @@ -460,30 +488,172 @@ export const baseQueryRouter = router({ throw notFoundError } - const validatedFooterConfig = validateFooterConfigSchema.safeParse( - response.data - ) + const validatedCurrentFooterConfig = + validateCurrentFooterConfigSchema.safeParse(response.data) - if (!validatedFooterConfig.success) { + if (!validatedCurrentFooterConfig.success) { getFooterFailCounter.add(1, { lang: input.lang, error_type: "validation_error", - error: JSON.stringify(validatedFooterConfig.error), + error: JSON.stringify(validatedCurrentFooterConfig.error), }) console.error( - "contentstack.footer validation error", + "contentstack.currentFooter validation error", JSON.stringify({ query: { lang: input.lang }, - error: validatedFooterConfig.error, + error: validatedCurrentFooterConfig.error, }) ) return null } - getFooterSuccessCounter.add(1, { lang: input.lang }) + getCurrentFooterSuccessCounter.add(1, { lang: input.lang }) console.info( - "contentstack.footer success", + "contentstack.currentFooter success", JSON.stringify({ query: { lang: input.lang } }) ) - return validatedFooterConfig.data.all_current_footer.items[0] + return validatedCurrentFooterConfig.data.all_current_footer.items[0] }), + footer: contentstackBaseProcedure.query(async ({ ctx }) => { + const { lang } = ctx + getFooterRefCounter.add(1, { lang }) + console.info( + "contentstack.footer.ref start", + JSON.stringify({ query: { lang } }) + ) + const responseRef = await request( + GetFooterRef, + { + locale: lang, + }, + { + cache: "force-cache", + next: { + tags: [generateRefsResponseTag(lang, "footer")], + }, + } + ) + + if (!responseRef.data) { + const notFoundError = notFound(responseRef) + getFooterRefFailCounter.add(1, { + lang, + error_type: "not_found", + error: JSON.stringify({ code: notFoundError.code }), + }) + console.error( + "contentstack.footer.refs not found error", + JSON.stringify({ + query: { + lang, + }, + error: { code: notFoundError.code }, + }) + ) + throw notFoundError + } + + const validatedFooterRefs = validateFooterRefConfigSchema.safeParse( + responseRef.data + ) + + if (!validatedFooterRefs.success) { + getFooterRefFailCounter.add(1, { + lang, + error_type: "validation_error", + error: JSON.stringify(validatedFooterRefs.error), + }) + console.error( + "contentstack.footer.refs validation error", + JSON.stringify({ + query: { + lang, + }, + error: validatedFooterRefs.error, + }) + ) + return null + } + + getFooterRefSuccessCounter.add(1, { lang }) + console.info( + "contentstack.footer.refs success", + JSON.stringify({ query: { lang } }) + ) + + const connections = getFooterConnections(validatedFooterRefs.data) + const footerUID = responseRef.data.all_footer.items[0].system.uid + + getFooterCounter.add(1, { lang: lang }) + console.info( + "contentstack.footer start", + JSON.stringify({ + query: { + lang, + }, + }) + ) + const tags = [ + generateTags(lang, connections), + generateTag(lang, footerUID), + ].flat() + + const response = await request( + GetFooter, + { + locale: lang, + }, + { + cache: "force-cache", + next: { + tags, + }, + } + ) + + if (!response.data) { + const notFoundError = notFound(response) + getFooterFailCounter.add(1, { + lang, + error_type: "not_found", + error: JSON.stringify({ code: notFoundError.code }), + }) + console.error( + "contentstack.footer not found error", + JSON.stringify({ + query: { + lang, + }, + error: { code: notFoundError.code }, + }) + ) + throw notFoundError + } + + const validatedFooterConfig = validateFooterConfigSchema.safeParse( + response.data + ) + + if (!validatedFooterConfig.success) { + getFooterFailCounter.add(1, { + lang, + error_type: "validation_error", + error: JSON.stringify(validatedFooterConfig.error), + }) + console.error( + "contentstack.footer validation error", + JSON.stringify({ + query: { lang: lang }, + error: validatedFooterConfig.error, + }) + ) + return null + } + getFooterSuccessCounter.add(1, { lang }) + console.info( + "contentstack.footer success", + JSON.stringify({ query: { lang } }) + ) + + return validatedFooterConfig.data + }), }) diff --git a/server/routers/contentstack/base/utils.ts b/server/routers/contentstack/base/utils.ts index dfdacfb66..6125eec92 100644 --- a/server/routers/contentstack/base/utils.ts +++ b/server/routers/contentstack/base/utils.ts @@ -1,4 +1,8 @@ -import { HeaderRefResponse } from "@/types/header" +import type { + FooterLinkItem, + FooterRefDataRaw, +} from "@/types/components/footer/footer" +import type { HeaderRefResponse } from "@/types/header" import { Edges } from "@/types/requests/utils/edges" import { NodeRefs } from "@/types/requests/utils/refs" @@ -38,3 +42,30 @@ export function getConnections(refs: HeaderRefResponse) { return connections } + +export function getFooterConnections(refs: FooterRefDataRaw) { + const connections: Edges[] = [] + const footerData = refs.all_footer.items[0] + const mainLinks = footerData.main_links + const secondaryLinks = footerData.secondary_links + const tertiaryLinks = footerData.tertiary_links + if (mainLinks) { + mainLinks.forEach(({ pageConnection }) => { + connections.push(pageConnection) + }) + } + secondaryLinks?.forEach(({ links }) => { + if (links) { + links.forEach(({ pageConnection }) => { + connections.push(pageConnection) + }) + } + }) + if (tertiaryLinks) { + tertiaryLinks.forEach(({ pageConnection }) => { + connections.push(pageConnection) + }) + } + + return connections +} diff --git a/stores/main-menu.ts b/stores/main-menu.ts index 6818d4cdb..29e039e99 100644 --- a/stores/main-menu.ts +++ b/stores/main-menu.ts @@ -1,102 +1,93 @@ +import { produce } from "immer" import { create } from "zustand" -// TODO: When MyPagesMobileMenu is removed, also remove the -// isMyPagesMobileMenuOpen state and toggleMyPagesMobileMenu function -interface DropdownState { - isHamburgerMenuOpen: boolean - isMyPagesMobileMenuOpen: boolean - isMyPagesMenuOpen: boolean - isLanguageSwitcherOpen: boolean - toggleHamburgerMenu: () => void - toggleMyPagesMobileMenu: () => void - toggleMyPagesMenu: () => void - toggleLanguageSwitcher: () => void -} +import { + type DropdownState, + DropdownTypeEnum, +} from "@/types/components/dropdown/dropdown" -const useDropdownStore = create((set) => ({ +// TODO: When MyPagesMobileMenu is removed, also remove the +// isMyPagesMobileMenuOpen state + +const useDropdownStore = create((set, get) => ({ isHamburgerMenuOpen: false, isMyPagesMobileMenuOpen: false, isMyPagesMenuOpen: false, - isLanguageSwitcherOpen: false, - toggleHamburgerMenu: () => - set( - ({ isHamburgerMenuOpen, isMyPagesMenuOpen, isMyPagesMobileMenuOpen }) => { - // Close the other dropdowns if they're open - if (!isHamburgerMenuOpen) { - if (isMyPagesMenuOpen) { - set({ isMyPagesMenuOpen: false }) - } - if (isMyPagesMobileMenuOpen) { - set({ isMyPagesMobileMenuOpen: false }) - } - } - return { isHamburgerMenuOpen: !isHamburgerMenuOpen } + isHeaderLanguageSwitcherOpen: false, + isHeaderLanguageSwitcherMobileOpen: false, + isFooterLanguageSwitcherOpen: false, + handleHamburgerClick: () => { + const state = get() + if (state.isMyPagesMobileMenuOpen) { + set({ isMyPagesMobileMenuOpen: false }) + } else { + if (state.isHeaderLanguageSwitcherMobileOpen) { + set({ isHeaderLanguageSwitcherMobileOpen: false }) } - ), - toggleMyPagesMobileMenu: () => - set( - ({ - isMyPagesMenuOpen, - isMyPagesMobileMenuOpen, - isHamburgerMenuOpen, - isLanguageSwitcherOpen, - }) => { - // Close the other dropdowns if they're open - if (!isMyPagesMobileMenuOpen) { - if (isMyPagesMenuOpen) { - set({ isMyPagesMenuOpen: false }) - } - if (isHamburgerMenuOpen) { - set({ isHamburgerMenuOpen: false }) - } - if (isLanguageSwitcherOpen) { - set({ isLanguageSwitcherOpen: false }) - } - } - return { isMyPagesMobileMenuOpen: !isMyPagesMobileMenuOpen } + if (!state.isFooterLanguageSwitcherOpen) { + set({ isHamburgerMenuOpen: !state.isHamburgerMenuOpen }) + } else { + set({ isFooterLanguageSwitcherOpen: false }) } - ), - toggleMyPagesMenu: () => + } + }, + toggleDropdown: (dropdown: DropdownTypeEnum) => set( - ({ - isHamburgerMenuOpen, - isLanguageSwitcherOpen, - isMyPagesMenuOpen, - isMyPagesMobileMenuOpen, - }) => { - // Close the other dropdowns if they're open - if (!isMyPagesMenuOpen) { - if (isHamburgerMenuOpen) { - set({ isHamburgerMenuOpen: false }) - } - if (isMyPagesMobileMenuOpen) { - set({ isMyPagesMobileMenuOpen: false }) - } - if (isLanguageSwitcherOpen) { - set({ isLanguageSwitcherOpen: false }) - } + produce((state: DropdownState) => { + switch (dropdown) { + case DropdownTypeEnum.HamburgerMenu: + state.isHamburgerMenuOpen = !state.isHamburgerMenuOpen + state.isMyPagesMobileMenuOpen = false + state.isMyPagesMenuOpen = false + state.isHeaderLanguageSwitcherOpen = false + state.isHeaderLanguageSwitcherMobileOpen = false + state.isFooterLanguageSwitcherOpen = false + break + case DropdownTypeEnum.MyPagesMobileMenu: + state.isMyPagesMobileMenuOpen = !state.isMyPagesMobileMenuOpen + state.isHamburgerMenuOpen = false + state.isMyPagesMenuOpen = false + state.isHeaderLanguageSwitcherOpen = false + state.isHeaderLanguageSwitcherMobileOpen = false + state.isFooterLanguageSwitcherOpen = false + break + case DropdownTypeEnum.MyPagesMenu: + state.isMyPagesMenuOpen = !state.isMyPagesMenuOpen + state.isHamburgerMenuOpen = false + state.isMyPagesMobileMenuOpen = false + state.isHeaderLanguageSwitcherOpen = false + state.isHeaderLanguageSwitcherMobileOpen = false + state.isFooterLanguageSwitcherOpen = false + break + case DropdownTypeEnum.HeaderLanguageSwitcher: + state.isHeaderLanguageSwitcherOpen = + !state.isHeaderLanguageSwitcherOpen + state.isHamburgerMenuOpen = false + state.isMyPagesMobileMenuOpen = false + state.isMyPagesMenuOpen = false + state.isHeaderLanguageSwitcherMobileOpen = false + state.isFooterLanguageSwitcherOpen = false + break + case DropdownTypeEnum.HeaderLanguageSwitcherMobile: + state.isHeaderLanguageSwitcherMobileOpen = + !state.isHeaderLanguageSwitcherMobileOpen + state.isHamburgerMenuOpen = false + state.isMyPagesMobileMenuOpen = false + state.isMyPagesMenuOpen = false + state.isHeaderLanguageSwitcherOpen = false + state.isFooterLanguageSwitcherOpen = false + break + case DropdownTypeEnum.FooterLanguageSwitcher: + state.isFooterLanguageSwitcherOpen = + !state.isFooterLanguageSwitcherOpen + state.isHamburgerMenuOpen = false + state.isMyPagesMobileMenuOpen = false + state.isMyPagesMenuOpen = false + state.isHeaderLanguageSwitcherOpen = false + state.isHeaderLanguageSwitcherMobileOpen = false + break } - return { isMyPagesMenuOpen: !isMyPagesMenuOpen } - } - ), - toggleLanguageSwitcher: () => - set( - ({ - isLanguageSwitcherOpen, - isMyPagesMenuOpen, - isMyPagesMobileMenuOpen, - }) => { - // Close the other dropdowns if they're open - if (!isLanguageSwitcherOpen) { - if (isMyPagesMenuOpen) { - set({ isMyPagesMenuOpen: false }) - } - if (isMyPagesMobileMenuOpen) { - set({ isMyPagesMobileMenuOpen: false }) - } - } - return { isLanguageSwitcherOpen: !isLanguageSwitcherOpen } - } + }) ), })) diff --git a/types/components/dropdown/dropdown.ts b/types/components/dropdown/dropdown.ts new file mode 100644 index 000000000..899086336 --- /dev/null +++ b/types/components/dropdown/dropdown.ts @@ -0,0 +1,21 @@ +export interface DropdownState { + isHamburgerMenuOpen: boolean + isMyPagesMobileMenuOpen: boolean + isMyPagesMenuOpen: boolean + isHeaderLanguageSwitcherOpen: boolean + isHeaderLanguageSwitcherMobileOpen: boolean + isFooterLanguageSwitcherOpen: boolean + toggleDropdown: (dropdown: DropdownTypeEnum) => void + handleHamburgerClick: () => void +} + +export enum DropdownTypeEnum { + HamburgerMenu = "hamburgerMenu", + MyPagesMobileMenu = "myPagesMobileMenu", + MyPagesMenu = "myPagesMenu", + HeaderLanguageSwitcher = "headerLanguageSwitcher", + HeaderLanguageSwitcherMobile = "headerLanguageSwitcherMobile", + FooterLanguageSwitcher = "footerLanguageSwitcher", +} + +export type DropdownType = `${DropdownTypeEnum}` diff --git a/types/components/footer/appDownloadIcons.ts b/types/components/footer/appDownloadIcons.ts new file mode 100644 index 000000000..e41eea35e --- /dev/null +++ b/types/components/footer/appDownloadIcons.ts @@ -0,0 +1,14 @@ +export enum AppDownLoadLinks { + apple_da = "/_static/img/store-badges/app-store-badge-da.svg", + apple_de = "/_static/img/store-badges/app-store-badge-de.svg", + apple_en = "/_static/img/store-badges/app-store-badge-en.svg", + apple_fi = "/_static/img/store-badges/app-store-badge-fi.svg", + apple_no = "/_static/img/store-badges/app-store-badge-no.svg", + apple_sv = "/_static/img/store-badges/app-store-badge-sv.svg", + google_da = "/_static/img/store-badges/google-play-badge-da.svg", + google_de = "/_static/img/store-badges/google-play-badge-de.svg", + google_en = "/_static/img/store-badges/google-play-badge-en.svg", + google_fi = "/_static/img/store-badges/google-play-badge-fi.svg", + google_no = "/_static/img/store-badges/google-play-badge-no.svg", + google_sv = "/_static/img/store-badges/google-play-badge-sv.svg", +} diff --git a/types/components/footer/footer.ts b/types/components/footer/footer.ts new file mode 100644 index 000000000..a40443e0d --- /dev/null +++ b/types/components/footer/footer.ts @@ -0,0 +1,11 @@ +import { z } from "zod" + +import { + validateFooterConfigSchema, + validateFooterRefConfigSchema, + validateLinkItem, +} from "@/server/routers/contentstack/base/output" + +export type FooterRefDataRaw = z.infer +export type FooterDataRaw = z.infer +export type FooterLinkItem = z.infer diff --git a/types/components/footer/navigation.ts b/types/components/footer/navigation.ts index 2e828298c..e17c7d65b 100644 --- a/types/components/footer/navigation.ts +++ b/types/components/footer/navigation.ts @@ -1,37 +1,41 @@ -export type FooterMainNav = { - id: string - href: string - title: string - openInNewTab: boolean - isExternal: boolean -} +import { z } from "zod" + +import { + validateLinkItem, + validateLinksWithType, + validateSecondaryLinks, +} from "@/server/routers/contentstack/base/output" + +import type { LanguageSwitcherData } from "@/types/requests/languageSwitcher" + +export type FooterLink = z.output + export type FooterMainNavProps = { - mainLinks: FooterMainNav[] + mainLinks: FooterLink[] } -export type FooterSecondaryNav = { - id: string - href: string +type FooterSecondaryNavGroup = z.output + +type FooterLinkWithType = z.output + +type FooterAppDownloads = { title: string - openInNewTab: boolean - isExternal: boolean -} -export type FooterSecondaryNavProps = { - secondaryLinks: { - title: string - links: FooterSecondaryNav[] - }[] - appDownloads: { - title: string - links: { - title: string - href: string - id: string - }[] - } + links: FooterLinkWithType } -export enum AppDownLoadLinks { - apple = "/_static/img/app-store-badge.svg", - google = "/_static/img/google-play-badge.svg", +type FooterSocialMedia = { + links: FooterLinkWithType } + +export type FooterSecondaryNavProps = { + secondaryLinks: FooterSecondaryNavGroup + appDownloads: FooterAppDownloads +} + +export type FooterDetailsProps = { + socialMedia?: FooterSocialMedia + tertiaryLinks?: FooterLink[] + languageUrls?: LanguageSwitcherData +} + +export type FooterNavigationProps = FooterMainNavProps & FooterSecondaryNavProps diff --git a/types/components/languageSwitcher/languageSwitcher.ts b/types/components/languageSwitcher/languageSwitcher.ts index fbadfab5d..6672fbe83 100644 --- a/types/components/languageSwitcher/languageSwitcher.ts +++ b/types/components/languageSwitcher/languageSwitcher.ts @@ -1,6 +1,6 @@ -import { LanguageSwitcherData } from "@/types/requests/languageSwitcher" +import type { LanguageSwitcherData } from "@/types/requests/languageSwitcher" export interface LanguageSwitcherProps { - type: "mobileHeader" | "mobileFooter" | "desktopHeader" | "desktopFooter" + type: "mobileHeader" | "desktopHeader" | "footer" urls: LanguageSwitcherData }