Skip to content

Commit

Permalink
header and footer web component (#378)
Browse files Browse the repository at this point in the history
  • Loading branch information
andnorda authored Jul 9, 2024
1 parent 0131a31 commit ab725b9
Show file tree
Hide file tree
Showing 11 changed files with 294 additions and 271 deletions.
89 changes: 5 additions & 84 deletions packages/client/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,102 +1,23 @@
/// <reference types="./client.d.ts" />
import { type Context, type ParamKey } from "decorator-shared/params";
import { getHeadAssetsProps } from "decorator-shared/head";
import Cookies from "js-cookie";
import "vite/modulepreload-polyfill";
import { initAnalytics } from "./analytics/analytics";
import { initAuth } from "./auth";
import { initLogoutWarning } from "./logout-warning";
import { createEvent, initHistoryEvents } from "./events";
import { initHistoryEvents } from "./events";
import { addFaroMetaData } from "./faro";
import { buildHtmlElement } from "./helpers/html-element-builder";
import { cdnUrl } from "./helpers/urls";
import { initLogoutWarning } from "./logout-warning";
import "./main.css";
import { env, param, updateDecoratorParams } from "./params";
import { useLoadIfActiveSession } from "./screensharing";
import { getHeadAssetsProps } from "decorator-shared/head";
import { buildHtmlElement } from "./helpers/html-element-builder";
import { cdnUrl, endpointUrlWithParams } from "./helpers/urls";

import.meta.glob("./styles/*.css", { eager: true });
import.meta.glob(["./views/**/*.ts", "!./views/**/*.test.ts"], { eager: true });

updateDecoratorParams({});

window.addEventListener("paramsupdated", (e) => {
if (e.detail.params.language) {
Promise.all(
["header", "footer"].map((key) =>
fetch(endpointUrlWithParams(`/${key}`)).then((res) =>
res.text(),
),
),
).then(([header, footer]) => {
const headerEl = document.getElementById("decorator-header");
const footerEl = document.getElementById("decorator-footer");
if (headerEl && footerEl) {
headerEl.outerHTML = header;
footerEl.outerHTML = footer;
init();
}
});
}
});

const msgSafetyCheck = (message: MessageEvent) => {
const { origin, source } = message;
// Only allow messages from own window
return window.location.href.startsWith(origin) && source === window;
};

window.addEventListener("message", (e) => {
if (!msgSafetyCheck(e)) {
return;
}
if (e.data.source === "decoratorClient" && e.data.event === "ready") {
window.postMessage({ source: "decorator", event: "ready" });
}
if (e.data.source === "decoratorClient" && e.data.event == "params") {
const payload = e.data.payload;

(
[
"breadcrumbs",
"availableLanguages",
"utilsBackground",
"language",
"chatbotVisible",
] satisfies ParamKey[]
).forEach((key) => {
if (payload[key] !== undefined) {
updateDecoratorParams({
[key]: payload[key],
});
}
});

if (e.data.payload.context) {
const context = e.data.payload.context;
if (
["privatperson", "arbeidsgiver", "samarbeidspartner"].includes(
context,
)
) {
window.dispatchEvent(
createEvent("activecontext", {
bubbles: true,
detail: { context },
}),
);
} else {
console.warn("Unrecognized context", context);
}
}
}
});

window.addEventListener("activecontext", (event) => {
updateDecoratorParams({
context: (event as CustomEvent<{ context: Context }>).detail.context,
});
});

// @TODO: Refactor loaders
window.addEventListener("load", () => {
useLoadIfActiveSession({
Expand Down
44 changes: 20 additions & 24 deletions packages/client/src/views/context-link.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,14 @@
import { erNavDekoratoren } from "decorator-shared/urls";
import headerClasses from "../styles/header.module.css";
import { tryParse } from "decorator-shared/json";
import { type AnalyticsEventArgs } from "../analytics/constants";
import { createEvent, CustomEvents } from "../events";
import { Context } from "decorator-shared/params";
import { CustomLinkElement } from "../helpers/custom-link-element";
import { erNavDekoratoren } from "decorator-shared/urls";
import { amplitudeEvent } from "../analytics/amplitude";
import { type AnalyticsEventArgs } from "../analytics/constants";
import { defineCustomElement } from "../custom-elements";
import { CustomLinkElement } from "../helpers/custom-link-element";
import { updateDecoratorParams } from "../params";
import headerClasses from "../styles/header.module.css";

class ContextLink extends CustomLinkElement {
handleActiveContext = (
event: CustomEvent<CustomEvents["activecontext"]>,
) => {
this.anchor.classList.toggle(
headerClasses.lenkeActive,
this.getAttribute("data-context") === event.detail.context,
);
};

handleClick = (e: MouseEvent) => {
if (erNavDekoratoren(window.location.href)) {
e.preventDefault();
Expand All @@ -29,14 +20,9 @@ class ContextLink extends CustomLinkElement {
null,
);

this.dispatchEvent(
createEvent("activecontext", {
bubbles: true,
detail: {
context: this.getAttribute("data-context") as Context,
},
}),
);
updateDecoratorParams({
context: this.getAttribute("data-context") as Context,
});

if (eventArgs) {
const payload = {
Expand All @@ -47,15 +33,25 @@ class ContextLink extends CustomLinkElement {
}
};

handleParamsUpdated = (event: CustomEvent) => {
if (event.detail.params.context) {
this.anchor.classList.toggle(
headerClasses.lenkeActive,
this.getAttribute("data-context") ===
event.detail.params.context,
);
}
};

connectedCallback() {
super.connectedCallback();

window.addEventListener("activecontext", this.handleActiveContext);
this.addEventListener("click", this.handleClick);
window.addEventListener("paramsupdated", this.handleParamsUpdated);
}

disconnectedCallback() {
window.removeEventListener("activecontext", this.handleActiveContext);
window.removeEventListener("paramsupdated", this.handleParamsUpdated);
}
}

Expand Down
22 changes: 22 additions & 0 deletions packages/client/src/views/footer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { defineCustomElement } from "../custom-elements";
import { endpointUrlWithParams } from "../helpers/urls";

class Footer extends HTMLElement {
handleParamsUpdated = (e: CustomEvent) => {
if (e.detail.params.language || e.detail.params.context) {
fetch(endpointUrlWithParams("/footer"))
.then((res) => res.text())
.then((footer) => (this.innerHTML = footer));
}
};

connectedCallback() {
window.addEventListener("paramsupdated", this.handleParamsUpdated);
}

disconnectedCallback() {
window.addEventListener("paramsupdated", this.handleParamsUpdated);
}
}

defineCustomElement("decorator-footer", Footer);
62 changes: 62 additions & 0 deletions packages/client/src/views/header.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { defineCustomElement } from "../custom-elements";
import { endpointUrlWithParams } from "../helpers/urls";
import { updateDecoratorParams } from "../params";

const msgSafetyCheck = (message: MessageEvent) => {
const { origin, source } = message;
return window.location.href.startsWith(origin) && source === window;
};

class Header extends HTMLElement {
handleMessage = (e: MessageEvent) => {
if (!msgSafetyCheck(e)) {
return;
} else if (
e.data.source === "decoratorClient" &&
e.data.event === "ready"
) {
window.postMessage({ source: "decorator", event: "ready" });
} else if (
e.data.source === "decoratorClient" &&
e.data.event == "params"
) {
const payload = e.data.payload;

[
"breadcrumbs",
"availableLanguages",
"utilsBackground",
"language",
"chatbotVisible",
"context",
].forEach((key) => {
if (payload[key] !== undefined) {
// TODO: validation
updateDecoratorParams({
[key]: payload[key],
});
}
});
}
};

handleParamsUpdated = (e: CustomEvent) => {
if (e.detail.params.language) {
fetch(endpointUrlWithParams("/header"))
.then((res) => res.text())
.then((header) => (this.innerHTML = header));
}
};

connectedCallback() {
window.addEventListener("message", this.handleMessage);
window.addEventListener("paramsupdated", this.handleParamsUpdated);
}

disconnectedCallback() {
window.removeEventListener("message", this.handleMessage);
window.addEventListener("paramsupdated", this.handleParamsUpdated);
}
}

defineCustomElement("decorator-header", Header);
18 changes: 9 additions & 9 deletions packages/client/src/views/main-menu.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Context } from "decorator-shared/params";
import { Context, Params } from "decorator-shared/params";
import { CustomEvents } from "../events";
import { ResponseCache } from "decorator-shared/response-cache";
import { param } from "../params";
Expand Down Expand Up @@ -38,19 +38,19 @@ class MainMenu extends HTMLElement {
});
};

private onContextChange = (
e: CustomEvent<CustomEvents["activecontext"]>,
) => {
this.updateMenuContent(e.detail.context);
handleParamsUpdated = (event: CustomEvent) => {
if (event.detail.params.context) {
this.updateMenuContent(event.detail.params.context);
}
};

private connectedCallback() {
connectedCallback() {
window.addEventListener("paramsupdated", this.handleParamsUpdated);
this.updateMenuContent(param("context"));
window.addEventListener("activecontext", this.onContextChange);
}

private disconnectedCallback() {
window.removeEventListener("activecontext", this.onContextChange);
disconnectedCallback() {
window.removeEventListener("paramsupdated", this.handleParamsUpdated);
}
}

Expand Down
17 changes: 14 additions & 3 deletions packages/client/src/views/user-menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,33 @@ import cls from "../styles/user-menu.module.css";
import globalCls from "../styles/global.module.css";
import { defineCustomElement } from "../custom-elements";
import { CustomEvents } from "../events";
import { AuthDataResponse } from "decorator-shared/auth";

let auth: AuthDataResponse;

class UserMenu extends HTMLElement {
private onAuthUpdated = (e: CustomEvent<CustomEvents["authupdated"]>) => {
private update = (auth: AuthDataResponse) => {
this.classList.add(cls.userMenuContainer);
this.querySelector(`.${cls.loader}`)?.classList.add(globalCls.hidden);
if (e.detail.auth.authenticated) {
this.innerHTML = e.detail.usermenuHtml!;
if (auth.auth.authenticated) {
this.innerHTML = auth.usermenuHtml!;
} else {
this.querySelector("login-button")?.classList.remove(
globalCls.hidden,
);
}
};

private onAuthUpdated = (e: CustomEvent<CustomEvents["authupdated"]>) => {
auth = e.detail;
this.update(auth);
};

connectedCallback() {
window.addEventListener("authupdated", this.onAuthUpdated);
if (auth) {
this.update(auth);
}
}

disconnectedCallback() {
Expand Down
23 changes: 17 additions & 6 deletions packages/server/src/server.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,28 @@
import { Server } from "bun";
import html from "decorator-shared/html";
import { clientTextsKeys } from "decorator-shared/types";
import { makeFrontpageUrl } from "decorator-shared/urls";
import { Hono } from "hono";
import { serveStatic } from "hono/bun";
import { HTTPException } from "hono/http-exception";
import { cspDirectives } from "./content-security-policy";
import { clientEnv, env } from "./env/server";
import { authHandler } from "./handlers/auth-handler";
import { searchHandler } from "./handlers/search-handler";
import { headers } from "./handlers/headers";
import { searchHandler } from "./handlers/search-handler";
import { versionProxyHandler } from "./handlers/version-proxy";
import i18n from "./i18n";
import { getMainMenuLinks, mainMenuContextLinks } from "./menu/main-menu";
import { setupMocks } from "./mocks";
import { archiveNotification } from "./notifications";
import { fetchOpsMessages } from "./ops-msgs";
import renderIndex, { renderFooter, renderHeader } from "./render-index";
import { getTaskAnalyticsConfig } from "./task-analytics-config";
import { texts } from "./texts";
import { getFeatures } from "./unleash";
import { validParams } from "./validateParams";
import { csrAssets } from "./views";
import { MainMenu } from "./views/header/main-menu";
import { texts } from "./texts";
import { clientTextsKeys } from "decorator-shared/types";
import { versionProxyHandler } from "./handlers/version-proxy";

const app = new Hono({
strict: false,
Expand Down Expand Up @@ -132,8 +133,18 @@ app.get("/env", async ({ req, json }) => {
const features = getFeatures();

return json({
header: renderHeader({ data }).render(data),
footer: (await renderFooter({ data, features })).render(data),
header: html`
<header id="decorator-header">
<decorator-header>${renderHeader({ data })}</decorator-header>
</header>
`.render(data),
footer: html`
<div id="decorator-footer">
<decorator-footer>
${await renderFooter({ data, features })}
</decorator-footer>
</div>
`.render(data),
data: {
texts: Object.entries(texts[data.language])
.filter(([key]) => clientTextsKeys.includes(key as any))
Expand Down
Loading

0 comments on commit ab725b9

Please sign in to comment.