diff --git a/client/config/webpack.config.js b/client/config/webpack.config.js index 0c6e8a9460a4..96d83038f9d9 100644 --- a/client/config/webpack.config.js +++ b/client/config/webpack.config.js @@ -239,6 +239,14 @@ function config(webpackEnv) { // match the requirements. When no loader matches it will fall // back to the "file" loader at the end of the loader list. oneOf: [ + { + resourceQuery: /raw/, + type: "asset/source", + }, + { + resourceQuery: /url/, + type: "asset/resource", + }, { test: [/\.avif$/, /\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/], type: "asset/resource", diff --git a/client/public/assets/curriculum/scrim.png b/client/public/assets/curriculum/scrim.png deleted file mode 100644 index 7c2748608888..000000000000 Binary files a/client/public/assets/curriculum/scrim.png and /dev/null differ diff --git a/client/src/assets/curriculum/scrim-bg.png b/client/src/assets/curriculum/scrim-bg.png new file mode 100644 index 000000000000..26929f273315 Binary files /dev/null and b/client/src/assets/curriculum/scrim-bg.png differ diff --git a/client/src/assets/curriculum/scrim-play.svg b/client/src/assets/curriculum/scrim-play.svg new file mode 100644 index 000000000000..1c58caf801bb --- /dev/null +++ b/client/src/assets/curriculum/scrim-play.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/curriculum/index.scss b/client/src/curriculum/index.scss index 575e4f03f3e1..6f1eedda36d3 100644 --- a/client/src/curriculum/index.scss +++ b/client/src/curriculum/index.scss @@ -287,6 +287,12 @@ list-style-type: disc; margin: 0.5rem 0; } + + scrim-inline { + display: block; + height: 14.25rem; + width: 22rem; + } } } } diff --git a/client/src/curriculum/landing.scss b/client/src/curriculum/landing.scss index 41d0b50f8298..dd9eda5615d0 100644 --- a/client/src/curriculum/landing.scss +++ b/client/src/curriculum/landing.scss @@ -265,15 +265,40 @@ grid-area: arrow; } - .scrim { + .scrim-wrapper { + display: flex; + flex-direction: column; grid-area: scrim; + justify-content: center; justify-self: center; margin-top: 1rem; + max-width: 24rem; @media (min-width: $screen-lg) { justify-self: end; margin-top: 0; } + + .scrim-border { + background-image: var(--curriculum-scrim-bg); + background-position: bottom right; + background-repeat: no-repeat; + height: 16rem; + width: 100%; + } + + scrim-inline { + background: #000; + display: block; + height: 14.25rem; + max-width: calc(100vw - var(--gutter) * 2); + width: 22rem; + } + + p { + margin: 0; + padding: 1rem 0; + } } } } diff --git a/client/src/curriculum/landing.tsx b/client/src/curriculum/landing.tsx index e247b2e7ed31..787ebf736a32 100644 --- a/client/src/curriculum/landing.tsx +++ b/client/src/curriculum/landing.tsx @@ -7,7 +7,7 @@ import { CurriculumDoc, CurriculumData } from "../../../libs/types/curriculum"; import { ModulesListList } from "./modules-list"; import { useCurriculumDoc } from "./utils"; import { RenderCurriculumBody } from "./body"; -import { useMemo } from "react"; +import { lazy, Suspense, useMemo } from "react"; import { DisplayH2 } from "../document/ingredients/utils"; import { CurriculumLayout } from "./layout"; @@ -15,7 +15,9 @@ import "./index.scss"; import "./landing.scss"; import { ProseSection } from "../../../libs/types/document"; import { PartnerBanner } from "./partner-banner"; -import { ScrimIframe } from "./scrim"; +import { useIsServer } from "../hooks"; + +const ScrimInline = lazy(() => import("./scrim-inline")); export function CurriculumLanding(appProps: HydrationData) { const doc = useCurriculumDoc(appProps as CurriculumData); @@ -129,13 +131,18 @@ const SCRIM_URL = "https://v2.scrimba.com/s06icdv?via=mdn"; function About({ section }) { const { title, content, id } = section.value; const html = useMemo(() => ({ __html: content }), [content]); + const isServer = useIsServer(); + return ( - + + + {!isServer && } + Learn our curriculum with high quality, interactive courses from our partner{" "} @@ -149,7 +156,7 @@ function About({ section }) { {" !"} - + ); diff --git a/client/src/curriculum/module.tsx b/client/src/curriculum/module.tsx index deddee4a33a9..07cf874ad478 100644 --- a/client/src/curriculum/module.tsx +++ b/client/src/curriculum/module.tsx @@ -5,12 +5,18 @@ import { PrevNext } from "./prev-next"; import { RenderCurriculumBody } from "./body"; import { CurriculumLayout } from "./layout"; import { topic2css, useCurriculumDoc } from "./utils"; +import { useEffect } from "react"; import "./index.scss"; import "./module.scss"; export function CurriculumModule(props: HydrationData) { const doc = useCurriculumDoc(props as CurriculumData); + + useEffect(() => { + import("./scrim-inline"); + }, []); + return ( ) { + if (changedProperties.has("url")) { + if (this.url) { + const url = new URL(this.url); + url.searchParams.set("via", "mdn"); + this._fullUrl = url.toString(); + + this._scrimId = url.pathname.slice(1); + } else { + this._fullUrl = undefined; + this._scrimId = undefined; + } + } + + if (changedProperties.has("img")) { + this._imgStyle = this.img + ? { + "--img": `url(${this.img})`, + } + : {}; + } + } + + render() { + if (!this.url || !this._fullUrl) { + return html``; + } + + return html` + + + + Clicking will load content from scrimba.com + + Toggle fullscreen + + + Open on Scrimba + + + ${this._scrimLoaded + ? html` + + ` + : html` + + ${unsafeHTML(playSvg)} + + "Load scrim and open dialog." + + + `} + + + `; + } + + #toggle(e: MouseEvent) { + if (e.target) { + (e.target as HTMLElement).dataset.glean = + `${CURRICULUM}: scrim fullscreen -> ${this._fullscreen ? 0 : 1} id:${this._scrimId}`; + } + if (this._fullscreen) { + this.#close(); + } else { + this.#open(); + } + } + + #open() { + const dialog = this.renderRoot.querySelector("dialog"); + if (dialog) { + dialog.showModal(); + this._scrimLoaded = true; + this._fullscreen = true; + } + } + + #close() { + const dialog = this.renderRoot.querySelector("dialog"); + dialog?.close(); + } + + #dialogClosed() { + this._fullscreen = false; + } +} + +declare module "react/jsx-runtime" { + namespace JSX { + interface IntrinsicElements { + "scrim-inline": React.DetailedHTMLProps< + React.HTMLAttributes, + HTMLElement + >; + } + } +} + +export default createComponent({ + tagName: "scrim-inline", + elementClass: ScrimInline, + react: React, +}); diff --git a/client/src/curriculum/scrim.scss b/client/src/curriculum/scrim.scss deleted file mode 100644 index 1e20cc6cc785..000000000000 --- a/client/src/curriculum/scrim.scss +++ /dev/null @@ -1,159 +0,0 @@ -.scrim { - align-items: center; - display: flex; - flex-direction: column; - height: 100%; - justify-content: center; - max-width: 24rem; - width: 100%; - - dialog { - display: contents; - - > .scrim-with-border { - background-image: var(--curriculum-scrim-bg); - background-position: bottom right; - background-repeat: no-repeat; - height: 16rem; - width: 100%; - - > .scrim-inner { - --inner-width: 22rem; - --inner-max-width: calc(100vw - 2 * var(--gutter)); - align-items: start; - background-color: #000; - display: flex; - flex-direction: column; - max-width: var(--inner-max-width); - width: var(--inner-width); - - .partner-header { - align-items: center; - display: flex; - flex-flow: row; - gap: 0.25rem; - justify-content: end; - margin: 0; - padding: 0 0.5rem; - width: 100%; - - a { - &::after { - background-color: #fff; - } - - &:hover { - &::after { - background-color: var(--curriculum-color); - } - } - } - - button { - color: #fff; - cursor: pointer; - - > div { - background-color: #fffd; - height: 1rem; - width: 1rem; - - &.fullscreen-button.enter { - mask-image: url("../assets/icons/fullscreen-enter.svg"); - } - - &.fullscreen-button.exit { - mask-image: url("../assets/icons/cancel.svg"); - } - - &:hover { - background-color: var(--curriculum-color); - } - } - - &:focus-visible { - outline-color: var(--accent-primary); - outline-offset: 1px; - outline-style: auto; - } - } - - span { - color: #fff; - font-size: var(--type-tiny-font-size); - margin-right: auto; - } - } - - .fullscreen-overlay.enter { - background-position: center; - background-repeat: no-repeat; - background-size: 7rem; - cursor: pointer; - height: 12.5rem; - margin-top: 1.75rem; - max-width: var(--inner-max-width); - position: absolute; - width: var(--inner-width); - - > svg { - color: #fff; - height: 7rem; - width: 7rem; - } - - &:hover { - > svg { - color: var(--curriculum-color); - } - } - - &:focus-visible { - > svg { - color: var(--accent-primary); - } - } - } - - .fullscreen-overlay.exit { - display: none; - } - - iframe, - img { - border: 1px solid #000; - height: 12.5rem; - width: 100%; - } - } - } - - &[open] { - background-color: #0009; - height: 90vh; - width: 90vw; - - .scrim-with-border { - background-image: none; - height: 100%; - width: 100%; - - .scrim-inner { - height: 100%; - width: 100%; - - iframe, - img { - height: 100%; - width: 100%; - } - } - } - } - } - - p { - margin: 0; - padding: 1rem 0; - } -} diff --git a/client/src/curriculum/scrim.tsx b/client/src/curriculum/scrim.tsx deleted file mode 100644 index 3a3fb9b78f0f..000000000000 --- a/client/src/curriculum/scrim.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { useState, useRef } from "react"; - -import { ReactComponent as EnterFullscreen } from "../../public/assets/curriculum/fullscreen-enter.svg"; - -import "./scrim.scss"; -import { CURRICULUM } from "../telemetry/constants"; -import { useGleanClick } from "../telemetry/glean-context"; - -export function ScrimIframe({ - url, - children, -}: { - url: string; - children?: React.ReactNode; -}) { - const [scrimLoaded, setScrimLoaded] = useState(false); - const [showDialog, setShowDialog] = useState(false); - const dialog = useRef(null); - - const gleanClick = useGleanClick(); - - return ( - - setShowDialog(false)}> - - - - Clicking will load content from scrimba.com - { - if (showDialog) { - dialog.current?.close(); - setShowDialog(false); - gleanClick(`${CURRICULUM}: scrim fullscreen -> 0`); - } else { - setScrimLoaded(true); - dialog.current?.showModal(); - setShowDialog(true); - gleanClick(`${CURRICULUM}: scrim fullscreen -> 1`); - } - }} - > - - Toggle fullscreen - - - - Open on Scrimba - - - {scrimLoaded ? ( - - ) : ( - <> - - { - setScrimLoaded(true); - dialog.current?.showModal(); - setShowDialog(true); - gleanClick(`${CURRICULUM}: scrim engage`); - }} - className={`fullscreen-overlay ${showDialog ? "exit" : "enter"}`} - > - - - {showDialog - ? "Close dialog." - : "Load scrim and open dialog."} - - - > - )} - - - - {children} - - ); -} diff --git a/client/src/react-app.d.ts b/client/src/react-app.d.ts index 09f456fa1906..5d38e0818e80 100644 --- a/client/src/react-app.d.ts +++ b/client/src/react-app.d.ts @@ -79,6 +79,16 @@ declare module "*.svg" { export default src; } +declare module "*?url" { + const src: string; + export default src; +} + +declare module "*?raw" { + const src: string; + export default src; +} + declare module "*.module.css" { const classes: { readonly [key: string]: string }; export default classes; diff --git a/client/src/telemetry/glean-context.tsx b/client/src/telemetry/glean-context.tsx index 173d071d4e8f..cee4ec3c2f4a 100644 --- a/client/src/telemetry/glean-context.tsx +++ b/client/src/telemetry/glean-context.tsx @@ -161,14 +161,16 @@ const gleanAnalytics = glean(); const GleanContext = React.createContext(gleanAnalytics); function handleButtonClick(ev: MouseEvent, click: (source: string) => void) { - const button = (ev?.target as HTMLElement | null)?.closest("button"); + const target = ev.composedPath()?.[0] || ev.target; + const button = (target as HTMLElement | null)?.closest("button"); if (button instanceof HTMLButtonElement && button.dataset.glean) { click(button.dataset.glean); } } function handleLinkClick(ev: MouseEvent, click: (source: string) => void) { - const anchor = (ev?.target as HTMLElement | null)?.closest("a"); + const target = ev.composedPath()?.[0] || ev.target; + const anchor = (target as HTMLElement | null)?.closest("a"); if (anchor instanceof HTMLAnchorElement) { if (anchor.dataset.glean) { click(anchor.dataset.glean); diff --git a/package.json b/package.json index 99b6ce56d308..acdc78a57e28 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "@codemirror/state": "^6.4.1", "@codemirror/theme-one-dark": "^6.1.2", "@fast-csv/parse": "^5.0.0", + "@lit/react": "^1.0.5", "@mdn/bcd-utils-api": "^0.0.7", "@mdn/browser-compat-data": "^5.6.2", "@mozilla/glean": "5.0.3", @@ -111,6 +112,7 @@ "inquirer": "^10.0.1", "is-svg": "^5.1.0", "js-yaml": "^4.1.0", + "lit": "^3.2.0", "loglevel": "^1.9.2", "lru-cache": "^10.4.3", "md5-file": "^5.0.0", diff --git a/ssr/react-app.d.ts b/ssr/react-app.d.ts index 8930f7c00dc1..31c25c28c1fc 100644 --- a/ssr/react-app.d.ts +++ b/ssr/react-app.d.ts @@ -75,6 +75,16 @@ declare module "*.svg" { export default src; } +declare module "*?url" { + const src: string; + export default src; +} + +declare module "*?raw" { + const src: string; + export default src; +} + declare module "*.module.css" { const classes: { readonly [key: string]: string }; export default classes; diff --git a/ssr/webpack.config.js b/ssr/webpack.config.js index 6675af4fad6f..5bbcb178f073 100644 --- a/ssr/webpack.config.js +++ b/ssr/webpack.config.js @@ -93,7 +93,15 @@ const config = { { // now our "normal", client-side emulating module rules // TODO: deduplicate with client webpack config - rules: [ + oneOf: [ + { + resourceQuery: /raw/, + type: "asset/source", + }, + { + resourceQuery: /url/, + type: "asset/resource", + }, { test: /\.tsx?$/, exclude: /node_modules/, diff --git a/yarn.lock b/yarn.lock index 806acf8d98a5..96a37733eb07 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2185,6 +2185,23 @@ dependencies: "@lezer/common" "^1.0.0" +"@lit-labs/ssr-dom-shim@^1.2.0": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.2.1.tgz#2f3a8f1d688935c704dbc89132394a41029acbb8" + integrity sha512-wx4aBmgeGvFmOKucFKY+8VFJSYZxs9poN3SDNQFF6lT6NrQUnHiPB2PWz2sc4ieEcAaYYzN+1uWahEeTq2aRIQ== + +"@lit/react@^1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@lit/react/-/react-1.0.5.tgz#9c53a8d719f91ef7edca0bdd68f5589ea579ffc1" + integrity sha512-RSHhrcuSMa4vzhqiTenzXvtQ6QDq3hSPsnHHO3jaPmmvVFeoNNm4DHoQ0zLdKAUvY3wP3tTENSUf7xpyVfrDEA== + +"@lit/reactive-element@^2.0.4": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@lit/reactive-element/-/reactive-element-2.0.4.tgz#8f2ed950a848016383894a26180ff06c56ae001b" + integrity sha512-GFn91inaUa2oHLak8awSIigYz0cU0Payr1rcFsrkf5OJ5eSPxElyZfKh0f2p9FsTiZWXQdWGJeXZICEfXXYSXQ== + dependencies: + "@lit-labs/ssr-dom-shim" "^1.2.0" + "@mdn/bcd-utils-api@^0.0.7": version "0.0.7" resolved "https://registry.yarnpkg.com/@mdn/bcd-utils-api/-/bcd-utils-api-0.0.7.tgz#555e80c33df520df068943e6b18ebc07f0e24d19" @@ -3438,6 +3455,11 @@ resolved "https://registry.yarnpkg.com/@types/triple-beam/-/triple-beam-1.3.5.tgz#74fef9ffbaa198eb8b588be029f38b00299caa2c" integrity sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw== +"@types/trusted-types@^2.0.2": + version "2.0.7" + resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11" + integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw== + "@types/unist@*", "@types/unist@^3.0.0": version "3.0.3" resolved "https://registry.yarnpkg.com/@types/unist/-/unist-3.0.3.tgz#acaab0f919ce69cce629c2d4ed2eb4adc1b6c20c" @@ -9766,6 +9788,31 @@ listr2@6.6.1: rfdc "^1.3.0" wrap-ansi "^8.1.0" +lit-element@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/lit-element/-/lit-element-4.1.0.tgz#cea3eb25f15091e3fade07c4d917fa6aaf56ba7d" + integrity sha512-gSejRUQJuMQjV2Z59KAS/D4iElUhwKpIyJvZ9w+DIagIQjfJnhR20h2Q5ddpzXGS+fF0tMZ/xEYGMnKmaI/iww== + dependencies: + "@lit-labs/ssr-dom-shim" "^1.2.0" + "@lit/reactive-element" "^2.0.4" + lit-html "^3.2.0" + +lit-html@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/lit-html/-/lit-html-3.2.0.tgz#cb09071a8a1f5f0850873f9143f18f0260be1fda" + integrity sha512-pwT/HwoxqI9FggTrYVarkBKFN9MlTUpLrDHubTmW4SrkL3kkqW5gxwbxMMUnbbRHBC0WTZnYHcjDSCM559VyfA== + dependencies: + "@types/trusted-types" "^2.0.2" + +lit@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/lit/-/lit-3.2.0.tgz#2189d72bccbc335f733a67bfbbd295f015e68e05" + integrity sha512-s6tI33Lf6VpDu7u4YqsSX78D28bYQulM+VAzsGch4fx2H0eLZnJsUBsPWmGYSGoKDNbjtRv02rio1o+UdPVwvw== + dependencies: + "@lit/reactive-element" "^2.0.4" + lit-element "^4.1.0" + lit-html "^3.2.0" + loader-runner@^4.2.0: version "4.3.0" resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.0.tgz#c1b4a163b99f614830353b16755e7149ac2314e1"
Learn our curriculum with high quality, interactive courses from our partner{" "} @@ -149,7 +156,7 @@ function About({ section }) { {" !"}