diff --git a/src/app/(main)/[lang]/[...path]/page.tsx b/src/app/(main)/[lang]/[...path]/page.tsx index a5ebe84bd..b2ea7ce52 100644 --- a/src/app/(main)/[lang]/[...path]/page.tsx +++ b/src/app/(main)/[lang]/[...path]/page.tsx @@ -1,4 +1,5 @@ import { Metadata } from "next"; +import { headers } from "next/headers"; import Compensations from "src/components/compensations/Compensations"; import CompensationsPreview from "src/components/compensations/CompensationsPreview"; @@ -6,10 +7,12 @@ import CustomerCase from "src/components/customerCases/customerCase/CustomerCase import CustomerCases from "src/components/customerCases/CustomerCases"; import CustomerCasesPreview from "src/components/customerCases/CustomerCasesPreview"; import CustomErrorMessage from "src/components/customErrorMessage/CustomErrorMessage"; +import EmployeePage from "src/components/employeePage/EmployeePage"; import Legal from "src/components/legal/Legal"; import LegalPreview from "src/components/legal/LegalPreview"; import PageHeader from "src/components/navigation/header/PageHeader"; import { homeLink } from "src/components/utils/linkTypes"; +import { ChewbaccaEmployee } from "src/types/employees"; import { getDraftModeInfo } from "src/utils/draftmode"; import { fetchPageDataFromParams } from "src/utils/pageData"; import SectionRenderer from "src/utils/renderSection"; @@ -21,6 +24,17 @@ type Props = { params: { lang: string; path: string[] }; }; +function seoDataFromChewbaccaEmployee(employee: ChewbaccaEmployee) { + return { + title: employee.name ?? undefined, + description: employee.email ?? undefined, + imageUrl: employee.imageThumbUrl ?? undefined, + keywords: [employee.name, employee.email, employee.telephone] + .filter((d) => d != null) + .join(","), + }; +} + function seoDataFromPageData( data: Awaited>, ): SeoData | null { @@ -42,6 +56,9 @@ function seoDataFromPageData( case "compensations": { return data.queryResponse.compensationsPage.data.seo; } + case "employee": { + return seoDataFromChewbaccaEmployee(data.queryResponse); + } } } @@ -52,6 +69,7 @@ export async function generateMetadata({ params }: Props): Promise { language, path: params.path, perspective: perspective ?? "published", + hostname: headers().get("host"), }); return generateMetadataFromSeo(seoDataFromPageData(pageData), language); } @@ -73,6 +91,7 @@ async function Page({ params }: Props) { language: lang, path, perspective: perspective ?? "published", + hostname: headers().get("host"), }); if (pageData == null) { @@ -93,6 +112,7 @@ async function Page({ params }: Props) { {queryResponse.data?.sections?.map((section, index) => ( ); + case "employee": + return ; } return Page404; })()} diff --git a/src/app/(main)/[lang]/page.tsx b/src/app/(main)/[lang]/page.tsx index e3ad4d87d..9c3f43a51 100644 --- a/src/app/(main)/[lang]/page.tsx +++ b/src/app/(main)/[lang]/page.tsx @@ -76,6 +76,7 @@ const Home = async ({ params }: Props) => { {initialLandingPage.data.sections.map((section, index) => ( +
+
+ {image != null && ( +
+ {employee.name} +
+ )} +
+ {employee.name} + {employee.email && ( + + {employee.email} + + )} + {employee.telephone && ( + + {employee.telephone} + + )} + {employee.officeName && ( + + {employee.officeName} + + )} +
+
+
+ + ) + ); +} diff --git a/src/components/employeePage/employeePage.module.css b/src/components/employeePage/employeePage.module.css new file mode 100644 index 000000000..12cf6af83 --- /dev/null +++ b/src/components/employeePage/employeePage.module.css @@ -0,0 +1,68 @@ +.wrapper { + width: 100%; + display: flex; + flex-direction: column; + align-items: center; +} + +.content { + width: 100%; + max-width: 1200px; + display: flex; + flex-direction: column; + align-items: center; + padding: 4rem 0 8rem 0; +} + +.employee { + display: flex; + gap: 3rem; + flex-direction: column; + margin: 1rem; + + @media (min-width: 1024px) { + flex-direction: row; + align-items: center; + } +} + +.employeeImage { + display: flex; + flex-direction: column; + align-items: center; + background-color: var(--primary-black); + border-radius: 12px; + width: 300px; + height: 300px; + padding: 1rem; + position: relative; + + @media (max-width: 1024px) { + flex-direction: column; + width: 200px; + height: 200px; + } +} + +.employeeInfo { + display: flex; + flex-direction: column; + gap: 6px; +} + +.employeeName { + color: var(--primary-black); + font-size: 64px; + font-weight: 600; +} + +.employeeRole { + color: var(--primary-black); + font-weight: 300; +} + +.employeeEmail, +.employeeTelephone { + color: var(--primary-black); + font-weight: 300; +} diff --git a/src/components/sections/employees/Employees.tsx b/src/components/sections/employees/Employees.tsx index 6b381e5cf..2f901fdc4 100644 --- a/src/components/sections/employees/Employees.tsx +++ b/src/components/sections/employees/Employees.tsx @@ -1,15 +1,33 @@ +import { headers } from "next/headers"; import Image from "next/image"; +import Link from "next/link"; -import { fetchAllChewbaccaEmployees } from "src/utils/employees"; +import { + aliasFromEmail, + domainFromEmail, + fetchAllChewbaccaEmployees, +} from "src/utils/employees"; +import { domainFromHostname } from "src/utils/url"; import { EmployeesSection } from "studio/lib/interfaces/pages"; +import { EMPLOYEE_PAGE_SLUG_QUERY } from "studio/lib/queries/siteSettings"; +import { loadStudioQuery } from "studio/lib/store"; import styles from "./employees.module.css"; export interface EmployeesProps { + language: string; section: EmployeesSection; } -export default async function Employees({ section }: EmployeesProps) { +export default async function Employees({ language, section }: EmployeesProps) { + const employeesPageRes = await loadStudioQuery<{ slug: string }>( + EMPLOYEE_PAGE_SLUG_QUERY, + { + language, + }, + ); + const employeesPageSlug = employeesPageRes.data.slug; + const employeesResult = await fetchAllChewbaccaEmployees(); if (!employeesResult.ok) { @@ -17,7 +35,11 @@ export default async function Employees({ section }: EmployeesProps) { return; } - const employees = employeesResult.value; + const domain = domainFromHostname(headers().get("host")); + const employees = employeesResult.value.filter( + (employee) => + employee.email != null && domainFromEmail(employee.email) === domain, + ); const total = employees.length; return ( @@ -37,14 +59,18 @@ export default async function Employees({ section }: EmployeesProps) { employee.name && employee.email && (
-
- {employee.name} -
+ +
+ {employee.name} +
+

{employee.name}

{employee.officeName && ( diff --git a/src/middlewares/languageMiddleware.ts b/src/middlewares/languageMiddleware.ts index 9076df4e7..cf32f0369 100644 --- a/src/middlewares/languageMiddleware.ts +++ b/src/middlewares/languageMiddleware.ts @@ -145,6 +145,26 @@ async function translateCustomerCasePath( ); } +async function translateEmployeePagePath( + path: string[], + targetLanguageId: string, + sourceLanguageId?: string, +) { + if (path.length !== 2) { + return undefined; + } + const pageSlugTranslation = await translateSlug( + path[0], + targetLanguageId, + sourceLanguageId, + "pageBuilder", + ); + if (pageSlugTranslation === undefined) { + return undefined; + } + return [pageSlugTranslation, path[1]]; +} + async function translatePath( path: string[], targetLanguageId: string, @@ -158,7 +178,15 @@ async function translatePath( (slug) => (slug !== undefined ? [slug] : undefined), ); } - const pathTranslation = await translateCustomerCasePath( + let pathTranslation = await translateCustomerCasePath( + path, + targetLanguageId, + sourceLanguageId, + ); + if (pathTranslation !== undefined) { + return pathTranslation; + } + pathTranslation = await translateEmployeePagePath( path, targetLanguageId, sourceLanguageId, diff --git a/src/utils/employees.ts b/src/utils/employees.ts index 27882430d..0de1217c1 100644 --- a/src/utils/employees.ts +++ b/src/utils/employees.ts @@ -4,6 +4,8 @@ import { } from "src/types/employees"; import { Result, ResultError, ResultOk } from "studio/utils/result"; +import { domainFromHostname } from "./url"; + const CHEWBACCA_URL = "https://chewie-webapp-ld2ijhpvmb34c.azurewebsites.net"; export async function fetchAllChewbaccaEmployees(): Promise< @@ -23,3 +25,34 @@ export async function fetchAllChewbaccaEmployees(): Promise< } return ResultOk(employeesData.employees); } + +export async function fetchChewbaccaEmployee( + email: string, +): Promise> { + const allEmployeesRes = await fetchAllChewbaccaEmployees(); + if (!allEmployeesRes.ok) { + return allEmployeesRes; + } + const employee = allEmployeesRes.value.find( + (employee) => employee.email === email, + ); + if (!employee) { + return ResultError("Employee does not exist for given email"); + } + return ResultOk(employee); +} + +export function emailFromAliasAndHostname( + alias: string, + hostname: string | null, +) { + return `${alias}@${domainFromHostname(hostname)}`; +} + +export function aliasFromEmail(email: string): string { + return email.split("@")[0]; +} + +export function domainFromEmail(email: string) { + return email.split("@")[1]; +} diff --git a/src/utils/pageData.ts b/src/utils/pageData.ts index bdf0853a8..5b108cf94 100644 --- a/src/utils/pageData.ts +++ b/src/utils/pageData.ts @@ -1,6 +1,7 @@ import { ClientPerspective } from "@sanity/client"; import { QueryResponseInitial } from "@sanity/react-loader"; +import { ChewbaccaEmployee } from "src/types/employees"; import { CompanyLocation } from "studio/lib/interfaces/companyDetails"; import { CompensationsPage } from "studio/lib/interfaces/compensations"; import { InternationalizedString } from "studio/lib/interfaces/global"; @@ -14,6 +15,7 @@ import { } from "studio/lib/queries/admin"; import { LOCALE_QUERY } from "studio/lib/queries/locale"; import { PAGE_BY_SLUG_QUERY } from "studio/lib/queries/pages"; +import { EMPLOYEE_PAGE_SLUG_QUERY } from "studio/lib/queries/siteSettings"; import { SLUG_FIELD_TRANSLATIONS_FROM_LANGUAGE_BY_TYPE_QUERY, SLUG_FIELD_TRANSLATIONS_FROM_LANGUAGE_QUERY, @@ -33,6 +35,7 @@ import { CUSTOMER_CASE_QUERY } from "studioShared/lib/queries/customerCases"; import { loadSharedQuery } from "studioShared/lib/store"; import { customerCaseID } from "studioShared/schemas/documents/customerCase"; +import { emailFromAliasAndHostname, fetchChewbaccaEmployee } from "./employees"; import { isNonNullQueryResponse } from "./queryResponse"; type PageFromParams = { @@ -236,6 +239,53 @@ async function fetchCustomerCase({ }; } +async function fetchEmployeePage({ + language, + path, + perspective, + hostname, +}: PageDataParams): Promise | null> { + if (path.length !== 2) { + return null; + } + const employeePageSlugRes = await loadStudioQuery<{ slug: string } | null>( + EMPLOYEE_PAGE_SLUG_QUERY, + { + language, + }, + { perspective }, + ); + if (!isNonNullQueryResponse(employeePageSlugRes)) { + return null; + } + if (path[0] !== employeePageSlugRes.data.slug) { + return null; + } + const employee = await fetchChewbaccaEmployee( + emailFromAliasAndHostname(path[1], hostname), + ); + if (!employee.ok) { + return null; + } + const pathTranslations = + await loadStudioQuery( + SLUG_FIELD_TRANSLATIONS_FROM_LANGUAGE_QUERY, + { + slug: path[0], + language, + }, + { perspective }, + ); + return { + queryResponse: employee.value, + docType: "employee", + pathTranslations: pathTranslations.data ?? [], + }; +} + async function fetchLegalDocument({ language, path, @@ -279,10 +329,12 @@ export interface PageDataParams { language: string; path: string[]; perspective: ClientPerspective; + hostname: string | null; } export async function fetchPageDataFromParams(params: PageDataParams) { return ( + (await fetchEmployeePage(params)) ?? (await fetchDynamicPage(params)) ?? (await fetchCompensationsPage(params)) ?? (await fetchCustomerCase(params)) ?? diff --git a/src/utils/renderSection.tsx b/src/utils/renderSection.tsx index e12762e55..57a98b2d2 100644 --- a/src/utils/renderSection.tsx +++ b/src/utils/renderSection.tsx @@ -32,6 +32,7 @@ import { } from "studio/lib/interfaces/pages"; interface SectionRendererProps { + language: string; section: Section; sectionIndex: number; isDraftMode: boolean; @@ -158,6 +159,7 @@ const renderGridSection = ( }; const SectionRenderer = ({ + language, section, sectionIndex, isDraftMode, @@ -218,7 +220,7 @@ const SectionRenderer = ({ case "grid": return renderGridSection(section, sectionIndex, isDraftMode, initialData); case "employees": - return ; + return ; default: return null; } diff --git a/src/utils/url.ts b/src/utils/url.ts index 154b8974c..7dcd871a4 100644 --- a/src/utils/url.ts +++ b/src/utils/url.ts @@ -14,3 +14,42 @@ export function absoluteUrlFromNextRequest( } return absoluteUrl; } + +const FALLBACK_DOMAIN = "variant.no"; + +/** + * attempts to extract the relevant Variant domain, without subdomains, from the given hostname + * + * accepts any Top-level domain (TLD), not just .no or .se + * + * in general: + * ``` + * 'variant.{someTLD}' -> 'variant.{someTLD}' + * '{someString}.variant.{someTLD}' -> 'variant.{someTLD}' + * ``` + * + * non-exhaustive list of examples: + * ``` + * 'v3.variant.no' -> 'variant.no' + * + * 'variant.se' -> 'variant.se' + * + * 'example.org' -> [fallback] + * + * 'localhost' -> [fallback] + * + * null -> [fallback] + * ``` + * + * @param hostname + */ +export function domainFromHostname(hostname: string | null): string { + if (hostname === null) { + return FALLBACK_DOMAIN; + } + const matches = hostname.match(/^(?:[a-z0-9-]+\.)*?(variant\.[a-z]{2,})$/i); + if (!matches) { + return FALLBACK_DOMAIN; + } + return matches[1]; +} diff --git a/studio/lib/queries/siteSettings.ts b/studio/lib/queries/siteSettings.ts index 94ea11edd..6f0870b08 100644 --- a/studio/lib/queries/siteSettings.ts +++ b/studio/lib/queries/siteSettings.ts @@ -6,6 +6,7 @@ import { TRANSLATED_SLUG_VALUE_FRAGMENT, } from "./i18n"; import { PAGE_FRAGMENT, SEO_FRAGMENT } from "./pages"; +import { translatedFieldFragment } from "./utils/i18n"; //Brand Assets export const BRAND_ASSETS_QUERY = groq` @@ -53,6 +54,12 @@ export const LANDING_PAGE_SITEMAP_QUERY = groq` } `; +export const EMPLOYEE_PAGE_SLUG_QUERY = groq` + *[_type == "navigationManager"][0].employeesPage -> { + "slug": ${translatedFieldFragment("slug")} + } +`; + //Social Media Profiles export const SOME_PROFILES_QUERY = groq` *[_type == "soMeLinksID" && _id == "soMeLinksID"][0] diff --git a/studio/schemas/documents/siteSettings/navigationManager.ts b/studio/schemas/documents/siteSettings/navigationManager.ts index 471593ffa..89f9a2c33 100644 --- a/studio/schemas/documents/siteSettings/navigationManager.ts +++ b/studio/schemas/documents/siteSettings/navigationManager.ts @@ -12,6 +12,7 @@ export const navManagerID = { main: "main", sidebar: "sidebar", footer: "footer", + employeesPage: "employeesPage", }; const navigationManager = defineType({ @@ -46,6 +47,17 @@ const navigationManager = defineType({ return ctaCount <= 2 || "You can only have two Call to Action links"; }), }, + { + name: navManagerID.employeesPage, + title: "Employees Page", + description: + "Select the employees page for the website. This is used to define subpages for each employee. The employees page typically includes the Employees page section. ", + type: "reference", + to: [{ type: pageBuilderID }], + options: { + disableNew: true, + }, + }, { name: navManagerID.footer, title: "Footer Menu",