From 5b6acfe282241f3de556f51c4d95adcae62959c0 Mon Sep 17 00:00:00 2001 From: Mathias Oterhals Myklebust Date: Thu, 31 Oct 2024 07:37:12 +0100 Subject: [PATCH] feat(Header): announcement banner --- .../navigation/header/Header.stories.tsx | 2 ++ src/components/navigation/header/Header.tsx | 25 +++++++++++++ .../navigation/header/HeaderPreview.tsx | 17 +++++++-- .../navigation/header/PageHeader.tsx | 30 ++++++++++------ .../navigation/header/header.module.css | 24 +++++++++++++ src/components/navigation/mockData.ts | 9 +++++ src/styles/global.css | 1 + studio/deskStructure.ts | 11 ++++++ studio/lib/interfaces/announcement.ts | 8 +++++ studio/lib/queries/siteSettings.ts | 13 +++++++ studio/schema.ts | 2 ++ .../documents/siteSettings/announcement.ts | 36 +++++++++++++++++++ 12 files changed, 165 insertions(+), 13 deletions(-) create mode 100644 studio/lib/interfaces/announcement.ts create mode 100644 studio/schemas/documents/siteSettings/announcement.ts diff --git a/src/components/navigation/header/Header.stories.tsx b/src/components/navigation/header/Header.stories.tsx index 469f0ba7f..ccd89bc20 100644 --- a/src/components/navigation/header/Header.stories.tsx +++ b/src/components/navigation/header/Header.stories.tsx @@ -2,6 +2,7 @@ import { Meta, StoryObj } from "@storybook/react"; import { defaultLanguage } from "i18n/supportedLanguages"; import { + mockAnnouncement, mockLogo, mockNavigation, mockPathTranslations, @@ -30,6 +31,7 @@ export const Default: Story = { args: { navigation: mockNavigation, assets: mockLogo, + announcement: mockAnnouncement, currentLanguage: defaultLanguage?.id ?? "en", pathTranslations: mockPathTranslations, }, diff --git a/src/components/navigation/header/Header.tsx b/src/components/navigation/header/Header.tsx index 819e21665..c90c37091 100644 --- a/src/components/navigation/header/Header.tsx +++ b/src/components/navigation/header/Header.tsx @@ -11,7 +11,9 @@ import LanguageSwitcher from "src/components/languageSwitcher/LanguageSwitcher"; import CustomLink from "src/components/link/CustomLink"; import LinkButton from "src/components/linkButton/LinkButton"; import { BreadCrumbMenu } from "src/components/navigation/breadCrumbMenu/BreadCrumbMenu"; +import Text from "src/components/text/Text"; import { getHref } from "src/utils/link"; +import { Announcement } from "studio/lib/interfaces/announcement"; import { BrandAssets } from "studio/lib/interfaces/brandAssets"; import { InternationalizedString } from "studio/lib/interfaces/global"; import { ILink, Navigation } from "studio/lib/interfaces/navigation"; @@ -23,6 +25,7 @@ import styles from "./header.module.css"; export interface IHeader { navigation: Navigation; assets: BrandAssets; + announcement: Announcement | null; currentLanguage: string; pathTranslations: InternationalizedString; } @@ -33,6 +36,7 @@ const filterLinks = (data: ILink[], type: string) => export const Header = ({ navigation, assets, + announcement, currentLanguage, pathTranslations, }: IHeader) => { @@ -68,6 +72,11 @@ export const Header = ({ }; }, []); + const showAnnouncement = + announcement !== null && + announcement.text.length > 0 && + (!announcement.hideAfter || new Date(announcement.hideAfter) > new Date()); + return ( <> )} + {showAnnouncement && ( +
+
+ {announcement.text} + {announcement.link && announcement.link.linkTitle && ( +
+ +
+ )} +
+
+ )}
{pathname !== "/" && pathname !== "/" + currentLanguage && ( diff --git a/src/components/navigation/header/HeaderPreview.tsx b/src/components/navigation/header/HeaderPreview.tsx index 8a702e354..bd2ce2201 100644 --- a/src/components/navigation/header/HeaderPreview.tsx +++ b/src/components/navigation/header/HeaderPreview.tsx @@ -1,27 +1,34 @@ "use client"; import { QueryResponseInitial, useQuery } from "@sanity/react-loader"; +import { Announcement } from "studio/lib/interfaces/announcement"; import { BrandAssets } from "studio/lib/interfaces/brandAssets"; import { InternationalizedString } from "studio/lib/interfaces/global"; import { Navigation } from "studio/lib/interfaces/navigation"; -import { BRAND_ASSETS_QUERY, NAV_QUERY } from "studio/lib/queries/siteSettings"; +import { + ANNOUNCEMENT_QUERY, + BRAND_ASSETS_QUERY, + NAV_QUERY, +} from "studio/lib/queries/siteSettings"; import { Header } from "./Header"; export default function HeaderPreview({ initialNav, initialBrandAssets, + initialAnnouncement, currentLanguage, pathTranslations, }: { initialNav: QueryResponseInitial; initialBrandAssets: QueryResponseInitial; + initialAnnouncement: QueryResponseInitial; currentLanguage: string; pathTranslations: InternationalizedString; }) { const { data: newNav } = useQuery( NAV_QUERY, - { language: initialNav.data.language }, + { language: currentLanguage }, { initial: initialNav }, ); const { data: newBrandAssets } = useQuery( @@ -29,6 +36,11 @@ export default function HeaderPreview({ {}, { initial: initialBrandAssets }, ); + const { data: newAnnouncement } = useQuery( + ANNOUNCEMENT_QUERY, + { language: currentLanguage }, + { initial: initialAnnouncement }, + ); return ( newNav && @@ -36,6 +48,7 @@ export default function HeaderPreview({
diff --git a/src/components/navigation/header/PageHeader.tsx b/src/components/navigation/header/PageHeader.tsx index be4403d01..e4d8eb76f 100644 --- a/src/components/navigation/header/PageHeader.tsx +++ b/src/components/navigation/header/PageHeader.tsx @@ -1,8 +1,14 @@ import { getDraftModeInfo } from "src/utils/draftmode"; +import { isNonNullQueryResponse } from "src/utils/queryResponse"; +import { Announcement } from "studio/lib/interfaces/announcement"; import { BrandAssets } from "studio/lib/interfaces/brandAssets"; import { InternationalizedString } from "studio/lib/interfaces/global"; import { Navigation } from "studio/lib/interfaces/navigation"; -import { BRAND_ASSETS_QUERY, NAV_QUERY } from "studio/lib/queries/siteSettings"; +import { + ANNOUNCEMENT_QUERY, + BRAND_ASSETS_QUERY, + NAV_QUERY, +} from "studio/lib/queries/siteSettings"; import { loadStudioQuery } from "studio/lib/store"; import { Header } from "./Header"; @@ -31,19 +37,20 @@ export default async function PageHeader({ { perspective }, ); + const initialAnnouncement = await loadStudioQuery( + ANNOUNCEMENT_QUERY, + { language }, + { perspective }, + ); + return ( - initialBrandAssets.data !== null && - initialNav.data !== null && + isNonNullQueryResponse(initialBrandAssets) && + isNonNullQueryResponse(initialNav) && (isDraftMode ? ( @@ -51,6 +58,7 @@ export default async function PageHeader({
diff --git a/src/components/navigation/header/header.module.css b/src/components/navigation/header/header.module.css index e22c1497a..85f3a3114 100644 --- a/src/components/navigation/header/header.module.css +++ b/src/components/navigation/header/header.module.css @@ -145,3 +145,27 @@ composes: button; background: url("/_assets/menu-close.svg") no-repeat 50% 50%; } + +.announcementWrapper { + display: flex; + flex-direction: column; + padding: 0.75rem; + background-color: var(--primary-bg-blue); + align-items: center; +} + +.announcementContent { + display: flex; + gap: 2rem; + + @media (max-width: 1024px) { + align-items: center; + flex-direction: column; + gap: 0.5rem; + } +} + +.announcementContent p { + font-weight: 600; + color: var(--primary-white); +} diff --git a/src/components/navigation/mockData.ts b/src/components/navigation/mockData.ts index e59b080f0..b846e7a4e 100644 --- a/src/components/navigation/mockData.ts +++ b/src/components/navigation/mockData.ts @@ -1,5 +1,6 @@ import primaryLogoFile from "src/stories/assets/energiai-primary-logo.svg"; import secondaryLogoFile from "src/stories/assets/energiai-secondary-logo.svg"; +import { Announcement } from "studio/lib/interfaces/announcement"; import { InternationalizedString } from "studio/lib/interfaces/global"; import { LinkType, @@ -131,6 +132,14 @@ export const mockLogo = { favicon: {}, }; +export const mockAnnouncement: Announcement = { + language: "no", + text: "Mandag 21.10. er det TDC! Møt oss der!", + hideAfter: new Date( + new Date().setMonth(new Date().getMonth() + 1), + ).toISOString(), +}; + // Mock Social Media Profiles export const mockSocialMediaProfiles: SocialMediaProfiles = { _id: "profile123", diff --git a/src/styles/global.css b/src/styles/global.css index d20a2e753..5e1607a49 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -14,6 +14,7 @@ html { --primary-light: #f5a4c4; --primary-bg: #f2f2f2; --primary-bg-dark: #d9d9d9; + --primary-bg-blue: #0014cd; --primary-white-bright: #ffffff; --primary-white: #faf8f5; diff --git a/studio/deskStructure.ts b/studio/deskStructure.ts index 16e40a722..7db88f680 100644 --- a/studio/deskStructure.ts +++ b/studio/deskStructure.ts @@ -1,6 +1,7 @@ import { CaseIcon, CogIcon, + ConfettiIcon, EarthGlobeIcon, HeartIcon, ImagesIcon, @@ -22,6 +23,7 @@ import { legalDocumentID } from "./schemas/documents/admin/legalDocuments"; import { compensationsId } from "./schemas/documents/compensations"; import { languageSettingsID } from "./schemas/documents/languageSettings"; import { pageBuilderID } from "./schemas/documents/pageBuilder"; +import { announcementID } from "./schemas/documents/siteSettings/announcement"; import { brandAssetsID } from "./schemas/documents/siteSettings/brandAssets"; import { localeID } from "./schemas/documents/siteSettings/locale"; import { soMeLinksID } from "./schemas/documents/siteSettings/socialMediaProfiles"; @@ -120,6 +122,15 @@ const siteSettingSection = (S: StructureBuilder) => .documentId(defaultSeoID) .title("Default SEO"), ), + S.listItem() + .title("Announcement") + .icon(ConfettiIcon) + .child( + S.document() + .schemaType(announcementID) + .documentId(announcementID) + .title("Announcement"), + ), ]), ); diff --git a/studio/lib/interfaces/announcement.ts b/studio/lib/interfaces/announcement.ts new file mode 100644 index 000000000..0e11476aa --- /dev/null +++ b/studio/lib/interfaces/announcement.ts @@ -0,0 +1,8 @@ +import { ILink } from "./navigation"; + +export interface Announcement { + language: string; + text: string; + link?: ILink; + hideAfter?: string; +} diff --git a/studio/lib/queries/siteSettings.ts b/studio/lib/queries/siteSettings.ts index 6f0870b08..b221d95a9 100644 --- a/studio/lib/queries/siteSettings.ts +++ b/studio/lib/queries/siteSettings.ts @@ -80,3 +80,16 @@ export const DEFAULT_SEO_QUERY = groq` ${SEO_FRAGMENT} } `; + +// Announcement +export const ANNOUNCEMENT_QUERY = groq` + *[_type == "announcement"][0]{ + ${LANGUAGE_FIELD_FRAGMENT}, + "text": ${translatedFieldFragment("text")}, + "link": link { + ..., + ${TRANSLATED_LINK_FRAGMENT} + }, + hideAfter + } +`; diff --git a/studio/schema.ts b/studio/schema.ts index 099a9e4f6..324b8c11c 100644 --- a/studio/schema.ts +++ b/studio/schema.ts @@ -7,6 +7,7 @@ import legalDocument from "./schemas/documents/admin/legalDocuments"; import compensations from "./schemas/documents/compensations"; import languageSettings from "./schemas/documents/languageSettings"; import pageBuilder from "./schemas/documents/pageBuilder"; +import announcement from "./schemas/documents/siteSettings/announcement"; import brandAssets from "./schemas/documents/siteSettings/brandAssets"; import locale from "./schemas/documents/siteSettings/locale"; import navigationManager from "./schemas/documents/siteSettings/navigationManager"; @@ -41,5 +42,6 @@ export const schema: { types: SchemaTypeDefinition[] } = { locale, richText, seo, + announcement, ], }; diff --git a/studio/schemas/documents/siteSettings/announcement.ts b/studio/schemas/documents/siteSettings/announcement.ts new file mode 100644 index 000000000..1a3e9f236 --- /dev/null +++ b/studio/schemas/documents/siteSettings/announcement.ts @@ -0,0 +1,36 @@ +import { defineField, defineType } from "sanity"; + +import { link } from "studio/schemas/objects/link"; + +export const announcementID = "announcement"; + +const announcement = defineType({ + name: announcementID, + type: "document", + title: "Announcement", + description: "Message displayed in a banner at the top of each page.", + fields: [ + defineField({ + name: "text", + type: "internationalizedArrayString", + title: "Text", + }), + link, + defineField({ + name: "hideAfter", + type: "datetime", + title: "Hide After", + description: + "The announcement will be hidden after the specified date and time.", + }), + ], + preview: { + prepare() { + return { + title: "Announcement", + }; + }, + }, +}); + +export default announcement;