diff --git a/.env-dist b/.env-dist index fde17baddea9..bcb00ecc0760 100644 --- a/.env-dist +++ b/.env-dist @@ -1,6 +1,7 @@ CONTENT_ROOT=../content/files #CONTENT_TRANSLATED_ROOT=../translated-content/files #CONTRIBUTOR_SPOTLIGHT_ROOT=../mdn-contributor-spotlight/contributors +#GENERIC_CONTENT_ROOT=../generic-content/files REACT_APP_DEV_MODE=true diff --git a/.github/workflows/prod-build.yml b/.github/workflows/prod-build.yml index d574a7231d20..99d983398106 100644 --- a/.github/workflows/prod-build.yml +++ b/.github/workflows/prod-build.yml @@ -89,6 +89,12 @@ jobs: lfs: true token: ${{ secrets.MDN_STUDIO_PAT }} + - uses: actions/checkout@v4 + if: ${{ ! vars.SKIP_BUILD }} + with: + repository: mdn/generic-content + path: mdn/generic-content + - uses: actions/checkout@v4 if: ${{ ! vars.SKIP_BUILD }} with: @@ -189,6 +195,7 @@ jobs: CONTRIBUTOR_SPOTLIGHT_ROOT: ${{ github.workspace }}/mdn/mdn-contributor-spotlight/contributors BLOG_ROOT: ${{ github.workspace }}/mdn/mdn-studio/content/posts CURRICULUM_ROOT: ${{ github.workspace }}/mdn/curriculum + GENERIC_CONTENT_ROOT: ${{ github.workspace }}/mdn/generic-content/files BASE_URL: "https://developer.mozilla.org" # The default for this environment variable is geared for writers @@ -239,7 +246,7 @@ jobs: # Surveys. REACT_APP_SURVEY_START_HOMEPAGE_FEEDBACK_2024: 1731369600000 # new Date("2024-11-12Z").getTime() - REACT_APP_SURVEY_END_HOMEPAGE_FEEDBACK_2024: 1733184000000 # new Date("2024-12-03Z").getTime() + REACT_APP_SURVEY_END_HOMEPAGE_FEEDBACK_2024: 1733616000000 # new Date("2024-12-08Z").getTime() REACT_APP_SURVEY_RATE_FROM_HOMEPAGE_FEEDBACK_2024: 0.0 REACT_APP_SURVEY_RATE_TILL_HOMEPAGE_FEEDBACK_2024: 1 # 100% REACT_APP_SURVEY_START_WEBDX_EDITING_2024: 1731628800000 # new Date("2024-11-15Z").getTime() diff --git a/.github/workflows/stage-build.yml b/.github/workflows/stage-build.yml index 2a0d475cb36b..ce8d912a774e 100644 --- a/.github/workflows/stage-build.yml +++ b/.github/workflows/stage-build.yml @@ -124,6 +124,12 @@ jobs: lfs: true token: ${{ secrets.MDN_STUDIO_PAT }} + - uses: actions/checkout@v4 + if: ${{ ! vars.SKIP_BUILD }} + with: + repository: mdn/generic-content + path: mdn/generic-content + - uses: actions/checkout@v4 if: ${{ ! vars.SKIP_BUILD }} with: @@ -220,11 +226,11 @@ jobs: CONTRIBUTOR_SPOTLIGHT_ROOT: ${{ github.workspace }}/mdn/mdn-contributor-spotlight/contributors BLOG_ROOT: ${{ github.workspace }}/mdn/mdn-studio/content/posts CURRICULUM_ROOT: ${{ github.workspace }}/mdn/curriculum + GENERIC_CONTENT_ROOT: ${{ github.workspace }}/mdn/generic-content/files BASE_URL: "https://developer.allizom.org" # rari BUILD_OUT_ROOT: "client/build" - GENERIC_CONTENT_ROOT: "copy" LIVE_SAMPLES_BASE_URL: https://live.mdnyalp.dev INTERACTIVE_EXAMPLES_BASE_URL: https://interactive-examples.mdn.allizom.net ADDITIONAL_LOCALES_FOR_GENERICS_AND_SPAS: de @@ -312,7 +318,7 @@ jobs: yarn rari content sync-translated-content yarn rari git-history - yarn rari build --issues client/build/issues.json --templ-stats + yarn rari build --all --issues client/build/issues.json --templ-stats # Sort DE search index by en-US popularity. node scripts/reorder-search-index.mjs client/build/en-us/search-index.json client/build/de/search-index.json diff --git a/.github/workflows/test-build.yml b/.github/workflows/test-build.yml index 1e665a62e80a..68aed969c984 100644 --- a/.github/workflows/test-build.yml +++ b/.github/workflows/test-build.yml @@ -58,6 +58,12 @@ jobs: lfs: true token: ${{ secrets.MDN_STUDIO_PAT }} + - uses: actions/checkout@v4 + if: ${{ ! vars.SKIP_BUILD }} + with: + repository: mdn/generic-content + path: mdn/generic-content + - uses: actions/checkout@v4 if: ${{ ! vars.SKIP_BUILD }} with: @@ -130,11 +136,11 @@ jobs: CONTRIBUTOR_SPOTLIGHT_ROOT: ${{ github.workspace }}/mdn/mdn-contributor-spotlight/contributors BLOG_ROOT: ${{ github.workspace }}/mdn/mdn-studio/content/posts CURRICULUM_ROOT: ${{ github.workspace }}/mdn/curriculum + GENERIC_CONTENT_ROOT: ${{ github.workspace }}/mdn/generic-content/files BASE_URL: "https://test.developer.allizom.org" # rari BUILD_OUT_ROOT: "client/build" - GENERIC_CONTENT_ROOT: "copy" LIVE_SAMPLES_BASE_URL: https://live.test.mdnyalp.dev INTERACTIVE_EXAMPLES_BASE_URL: https://interactive-examples.mdn.allizom.net ADDITIONAL_LOCALES_FOR_GENERICS_AND_SPAS: de @@ -203,7 +209,7 @@ jobs: yarn rari content sync-translated-content yarn rari git-history - yarn rari build --issues client/build/issues.json --templ-stats + yarn rari build --all --issues client/build/issues.json --templ-stats # SSR all pages yarn render:html diff --git a/build/spas.ts b/build/spas.ts index 7076f3f4c014..6d500d2877b6 100644 --- a/build/spas.ts +++ b/build/spas.ts @@ -1,6 +1,5 @@ import fs from "node:fs"; import path from "node:path"; -import { fileURLToPath } from "node:url"; import frontmatter from "front-matter"; import { fdir, PathsOutput } from "fdir"; @@ -22,10 +21,16 @@ import { CONTRIBUTOR_SPOTLIGHT_ROOT, BUILD_OUT_ROOT, DEV_MODE, + GENERIC_CONTENT_ROOT, } from "../libs/env/index.js"; import { isValidLocale } from "../libs/locale-utils/index.js"; import { DocFrontmatter, DocParent, NewsItem } from "../libs/types/document.js"; -import { getSlugByBlogPostUrl, makeTOC } from "./utils.js"; +import { + getSlugByBlogPostUrl, + injectLoadingLazyAttributes, + makeTOC, + postProcessExternalLinks, +} from "./utils.js"; import { findByURL } from "../content/document.js"; import { buildDocument } from "./index.js"; import { findPostBySlug } from "./blog.js"; @@ -145,9 +150,9 @@ export async function buildSPAs(options: { // The URL isn't very important as long as it triggers the right route in the const locale = DEFAULT_LOCALE; - const url = `/${locale}/404.html`; + const url = `/${locale}/404/index.html`; const context: HydrationData = { url, pageNotFound: true }; - const outPath = path.join(BUILD_OUT_ROOT, locale.toLowerCase(), "_spas"); + const outPath = path.join(BUILD_OUT_ROOT, locale.toLowerCase(), "404"); fs.mkdirSync(outPath, { recursive: true }); const jsonFilePath = path.join( outPath, @@ -271,7 +276,7 @@ export async function buildSPAs(options: { async function buildStaticPages( dirpath: string, slugPrefix?: string, - title = "MDN" + title?: string ) { const crawler = new fdir() .withFullPaths() @@ -282,7 +287,7 @@ export async function buildSPAs(options: { for (const filepath of filepaths) { const file = filepath.replace(dirpath, ""); - const page = file.split(".")[0]; + const page = file.split(".")[0].slice(1); const locale = DEFAULT_LOCALE; const pathLocale = locale.toLowerCase(); @@ -309,6 +314,8 @@ export async function buildSPAs(options: { }; const [$] = await kumascript.render(url, {}, d); wrapTables($); + postProcessExternalLinks($); + injectLoadingLazyAttributes($); const [sections] = await extractSections($); const toc = makeTOC({ body: sections }); @@ -320,9 +327,9 @@ export async function buildSPAs(options: { }; const context: HydrationData = { hyData, - pageTitle: frontMatter.attributes.title + pageTitle: title ? `${frontMatter.attributes.title} | ${title}` - : title, + : frontMatter.attributes.title, url, }; @@ -343,21 +350,20 @@ export async function buildSPAs(options: { } } - await buildStaticPages( - fileURLToPath(new URL("../copy/plus/", import.meta.url)), - "plus/docs", - "MDN Plus" - ); - await buildStaticPages( - fileURLToPath(new URL("../copy/observatory/", import.meta.url)), - "observatory/docs", - OBSERVATORY_TITLE - ); - await buildStaticPages( - fileURLToPath(new URL("../copy/community/", import.meta.url)), - "", - "Contribute to MDN" - ); + if (GENERIC_CONTENT_ROOT) { + await buildStaticPages( + path.join(GENERIC_CONTENT_ROOT, "plus"), + "plus/docs", + "MDN Plus" + ); + await buildStaticPages( + path.join(GENERIC_CONTENT_ROOT, "observatory"), + "observatory/docs", + OBSERVATORY_TITLE + ); + await buildStaticPages(path.join(GENERIC_CONTENT_ROOT, "community")); + await buildStaticPages(path.join(GENERIC_CONTENT_ROOT, "about")); + } // Build all the home pages in all locales. // Fetch merged content PRs for the latest contribution section. diff --git a/client/pwa/package.json b/client/pwa/package.json index 6e4b39da57ff..603a29b34c89 100644 --- a/client/pwa/package.json +++ b/client/pwa/package.json @@ -12,14 +12,14 @@ "dev": "webpack-cli --watch" }, "dependencies": { - "@zip.js/zip.js": "2.7.53", + "@zip.js/zip.js": "2.7.54", "dexie": "4.0.10" }, "devDependencies": { "@types/dexie": "1.3.35", "ts-loader": "^9.5.1", "typescript": "^5.7.2", - "webpack": "^5.97.0", + "webpack": "^5.97.1", "webpack-cli": "^5.1.4", "workers-preview": "^1.0.6" } diff --git a/client/pwa/tsconfig.json b/client/pwa/tsconfig.json index 1f3d0ed8ddf0..0ce2a63fef89 100644 --- a/client/pwa/tsconfig.json +++ b/client/pwa/tsconfig.json @@ -2,6 +2,7 @@ "extends": "../tsconfig.json", "compilerOptions": { "lib": ["dom", "dom.iterable", "es2021", "webworker"], + "allowImportingTsExtensions": false, "noEmit": false, "preserveConstEnums": true, "strictNullChecks": false, diff --git a/client/pwa/yarn.lock b/client/pwa/yarn.lock index 0fd670b2b48f..c7b9d697ee91 100644 --- a/client/pwa/yarn.lock +++ b/client/pwa/yarn.lock @@ -254,10 +254,10 @@ resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== -"@zip.js/zip.js@2.7.53": - version "2.7.53" - resolved "https://registry.yarnpkg.com/@zip.js/zip.js/-/zip.js-2.7.53.tgz#bf88e90d8eed562182c01339643bc405446b0578" - integrity sha512-G6Bl5wN9EXXVaTUIox71vIX5Z454zEBe+akKpV4m1tUboIctT5h7ID3QXCJd/Lfy2rSvmkTmZIucf1jGRR4f5A== +"@zip.js/zip.js@2.7.54": + version "2.7.54" + resolved "https://registry.yarnpkg.com/@zip.js/zip.js/-/zip.js-2.7.54.tgz#ef0f46644b1a084155473b0d7710c80a892c2687" + integrity sha512-qMrJVg2hoEsZJjMJez9yI2+nZlBUxgYzGV3mqcb2B/6T1ihXp0fWBDYlVHlHquuorgNUQP5a8qSmX6HF5rFJNg== acorn@^8.14.0, acorn@^8.8.2: version "8.14.0" @@ -902,10 +902,10 @@ webpack-sources@^3.2.3: resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== -webpack@^5.97.0: - version "5.97.0" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.97.0.tgz#1c5e3b9319f8c6decb19b142e776d90e629d5c40" - integrity sha512-CWT8v7ShSfj7tGs4TLRtaOLmOCPWhoKEvp+eA7FVx8Xrjb3XfT0aXdxDItnRZmE8sHcH+a8ayDrJCOjXKxVFfQ== +webpack@^5.97.1: + version "5.97.1" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.97.1.tgz#972a8320a438b56ff0f1d94ade9e82eac155fa58" + integrity sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg== dependencies: "@types/eslint-scope" "^3.7.7" "@types/estree" "^1.0.6" diff --git a/client/src/about/_mixins.scss b/client/src/about/_mixins.scss new file mode 100644 index 000000000000..eb3522309b7e --- /dev/null +++ b/client/src/about/_mixins.scss @@ -0,0 +1,282 @@ +@use "../ui/vars" as *; +@use "../ui/atoms/button/mixins" as button; + +@mixin theme-setup { + .light & { + @content (light); + } + + .dark & { + @content (dark); + } + + // OS Default. + :root:not(.light):not(.dark) & { + @media (prefers-color-scheme: light) { + @content (light); + } + + @media (prefers-color-scheme: dark) { + @content (dark); + } + } +} + +@mixin layout { + h2, + h3, + p { + margin: 0; + } + + h2, + h3 { + color: var(--layout-text-primary); + + a { + color: unset; + text-decoration: none; + } + } + + a { + text-decoration: underline; + + &:hover { + text-decoration: none; + } + } + + p + p { + margin-top: 1.5rem; + } + + section { + margin-left: auto; + margin-right: auto; + max-width: var(--max-width); + padding-left: var(--gutter); + padding-right: var(--gutter); + width: 100%; + } + + h2 { + font-size: 2rem; + font-weight: 600; + margin-bottom: 1rem; + + @media (max-width: $screen-md) { + font-size: 1.375rem; + } + } +} + +@mixin header { + background: linear-gradient( + to top, + var(--header-next-section-bg, transparent) 0%, + var(--header-next-section-bg, transparent) + calc(var(--header-stats-height) / 2), + var(--header-bg) calc(var(--header-stats-height) / 2), + var(--header-bg) 100% + ); + + @media (max-width: $screen-md) { + padding-top: 1rem; + } + + section { + padding-top: 5rem; + + @media (max-width: $screen-md) { + padding-top: 0; + } + } + + h1 { + color: var(--header-text-primary); + font-size: 2.5rem; + margin-bottom: 1rem; + + @media (max-width: $screen-md) { + font-size: 2rem; + } + } + + p { + color: var(--header-text-secondary); + margin-bottom: 1.5rem; + } + + + section { + margin-top: 4.56rem; + + @media (max-width: $screen-md) { + margin-top: 2rem; + } + } +} + +@mixin stats { + background: var(--stats-bg); + border-radius: 0.5rem; + box-shadow: var(--stats-box-shadow); + color: var(--stats-text-primary); + display: flex; + gap: 1rem; + justify-content: space-around; + margin-top: 5em; + padding: 1rem; + position: relative; + text-align: center; + z-index: 2; + + @media (max-width: $screen-md) { + flex-wrap: wrap; + margin-top: 2rem; + } + + li { + align-items: baseline; + column-gap: 1rem; + display: flex; + flex-wrap: wrap; + justify-content: center; + min-width: 7.75rem; + overflow-wrap: anywhere; + + @media (max-width: $screen-md) { + align-items: center; + flex: 1; + flex-direction: column; + justify-content: flex-start; + } + + strong { + align-items: center; + background: var(--stats-stat-bg); + border-radius: 50%; + color: var(--stats-stat-text); + display: inline-flex; + height: 3.75rem; + justify-content: center; + width: 3.75rem; + } + } +} + +@mixin section { + column-gap: min(5rem, 5vw); + display: grid; + grid-template-columns: 4fr 6fr; + + @media (max-width: $screen-md) { + display: block; + } + + > * { + min-width: 0; + } +} + +@mixin boxes { + display: grid; + gap: 2rem; + grid-auto-rows: 1fr; + grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr)); + + li { + align-items: center; + background: var(--boxes-bg); + border: 1px solid var(--boxes-border); + border-radius: 0.5rem; + box-shadow: var(--boxes-shadow); + display: flex; + flex-direction: column; + gap: 1.5rem; + justify-content: space-between; + padding: 1.5rem; + text-align: center; + + h3, + h4, + h5, + h6 { + align-self: stretch; + background: var(--boxes-header-bg); + border-radius: 0.5rem 0.5rem 0 0; + color: var(--boxes-header-color); + font-size: 1.25rem; + font-weight: 500; + margin: -1.5rem; + margin-bottom: 0; + padding: 1.5rem; + } + + p { + color: var(--boxes-main-color); + margin: 0; + } + } +} + +@mixin stairs { + --stairs-icon-size: 3.125rem; + --stairs-step-indent: 3.125rem; + --stairs-step-gap: 4rem; + + li { + --stairs-padding-left: calc(var(--stairs-step-indent) * var(--nth-child)); + align-items: center; + display: flex; + gap: 1rem; + padding-left: var(--stairs-padding-left); + position: relative; + + @media (max-width: $screen-md) { + --stairs-step-indent: 0; + --stairs-padding-left: var(--stairs-icon-size); + --stairs-step-gap: 1rem; + } + + &:not(:last-of-type) { + margin-bottom: var(--stairs-step-gap); + } + + &::before { + background: var(--stairs-color); + content: ""; + display: block; + flex-shrink: 0; + height: var(--stairs-icon-size); + margin-left: calc(-1 * var(--stairs-icon-size)); + mask-image: var(--stairs-icon); + mask-position: center; + mask-repeat: no-repeat; + mask-size: 80%; + width: var(--stairs-icon-size); + } + + &:not(:last-of-type)::after { + --height: calc(var(--stairs-step-gap) * 1.2); + background: linear-gradient(to bottom, var(--stairs-color), transparent); + bottom: calc(-1 * var(--height)); + content: ""; + display: block; + height: var(--height); + left: calc(var(--stairs-padding-left) - 50px); + mask: url("../assets/lines.svg"); + mask-position: center; + mask-repeat: no-repeat; + mask-size: contain; + position: absolute; + width: calc(var(--stairs-icon-size) + var(--stairs-step-indent)); + } + } + + @for $i from 1 through 4 { + li:nth-child(#{$i}) { + --nth-child: #{$i}; + } + } +} diff --git a/client/src/about/custom-elements.js b/client/src/about/custom-elements.js new file mode 100644 index 000000000000..35df7bcbb4d9 --- /dev/null +++ b/client/src/about/custom-elements.js @@ -0,0 +1,76 @@ +import { LitElement } from "lit"; + +export class MDNImageHistory extends LitElement { + createRenderRoot() { + return this; + } + + firstUpdated() { + this.renderRoot.querySelectorAll("img").forEach((img) => { + const regex = /@([0-9]+(?:\.[0-9]+)?)(?=x\.[a-z]+$)/; + const match = img.src.match(regex); + if (match?.[1]) { + const baseRes = parseFloat(match[1]); + const dpis = [1, 2]; + img.srcset = dpis + .map( + (dpi) => `${img.src.replace(regex, `@${baseRes * dpi}`)} ${dpi}x` + ) + .join(", "); + } + }); + } +} + +customElements.define("mdn-image-history", MDNImageHistory); + +export class TeamMember extends LitElement { + _setID() { + const hx = this.querySelector("h4, h5"); + const panel = hx?.closest(".tabpanel"); + if (hx && panel) { + const id = `${panel.id.replace("-panel", "")}_${hx.id}`; + if (this.id !== id) { + this.id = id; + } + } + } + + /** @param {FocusEvent} ev */ + _focusin({ currentTarget }) { + if (currentTarget instanceof HTMLElement) { + window.history.pushState({}, "", `#${currentTarget.id}`); + this.scrollIntoView({ block: "nearest", inline: "nearest" }); + } + } + + /** @param {MouseEvent} ev */ + _mousedown(ev) { + if (ev.target instanceof HTMLAnchorElement) { + ev.preventDefault(); + } + } + + createRenderRoot() { + return this; + } + + connectedCallback() { + super.connectedCallback(); + this.tabIndex = 0; + this._setID(); + this.addEventListener("mousedown", this._mousedown); + this.addEventListener("focusin", this._focusin); + if (window.location.hash === `#${this.id}`) { + setTimeout(() => this.focus(), 0); + } + } + + disconnectedCallback() { + super.disconnectedCallback(); + this.removeEventListener("mousedown", this._mousedown); + this.removeEventListener("focusin", this._focusin); + } +} + +customElements.define("team-member", TeamMember); diff --git a/client/src/about/index.scss b/client/src/about/index.scss index 3388f85691c9..c34f309458a3 100644 --- a/client/src/about/index.scss +++ b/client/src/about/index.scss @@ -1,55 +1,822 @@ @use "../ui/vars" as *; +@use "../ui/mixins" as *; +@use "../about/mixins" as about; -.about { - width: 100%; +main.about-container { + --about-stats-height: 5.75rem; + --about-section-gap: 5rem; + --max-width: 74rem; + --inner-width: calc(min(var(--max-width), 100vw) - 2 * var(--gutter)); - .about-container { - margin: 0 auto; - max-width: 52rem; - padding: 0 1rem 2rem; + background: var(--about-bg-primary); + color: var(--about-color); - h1 { - margin-top: 3rem; + @media (max-width: $screen-md) { + --about-section-gap: 3rem; + } + + @include about.layout; + + @include about.theme-setup using ($mode) { + @if $mode == light { + --header-next-section-bg: #f2f2f5; + --header-text-primary: #000; + --header-text-secondary: #343434; + --header-bg: #fff; + --stats-bg: #fff; + --stats-box-shadow: 4px -2px 15px 0 rgba(179, 179, 179, 0.2), + 4px -4px 15px 0 rgba(179, 179, 179, 0.15); + --stats-text-primary: #000; + --stats-stat-bg: #d7f5dc; + --stats-stat-text: #007936; + --boxes-bg: #fff; + --boxes-border: none; + --boxes-shadow: -4px 4px 8px 0 rgba(179, 179, 179, 0.15), + 4px 4px 8px 0 rgba(179, 179, 179, 0.18); + --boxes-header-bg-1: #d5e8fb; + --boxes-header-color-1: inherit; + --boxes-header-bg-2: #fbe3d5; + --boxes-header-color-2: inherit; + --boxes-header-bg-3: #fbf7d5; + --boxes-header-color-3: inherit; + --boxes-header-bg-4: #fbd5d5; + --boxes-header-color-4: inherit; + --boxes-header-bg-5: #dfd5fb; + --boxes-header-color-5: inherit; + --boxes-main-color: #000; + --stairs-color: #007936; + --about-bg-primary: #fff; + --about-bg-secondary: #f2f2f5; + --about-join-us-bg: #f2f2f5; + --about-join-us-border: none; + --about-join-us-color: #343434; + --about-join-us-image: url("../assets/about/building.svg"); + --about-tablist-border: #858585; + --about-tablist-color: rgba(0, 0, 0, 0.6); + --about-tablist-active-border: #007936; + --about-tablist-active-color: #000; + --about-color: #000; + --about-heading-color: #000; + --about-core-values-bg: #fff; + --about-core-values-bg-secondary: #d7f5dc; + --about-core-values-color: #000; + --about-core-values-shadow: 0 4px 15px 0 rgba(179, 179, 179, 0.2); + --about-team-title-color: #007936; + --about-team-bg: #fff; + --about-team-github-bg: rgba(255, 255, 255, 0.75); + --about-team-shadow: 0 4px 15px 0 rgba(179, 179, 179, 0.2); + --about-team-color: #000; + --about-journey-line-color: #007936; + --about-journey-image-border: #fff; + --about-journey-shadow: 4px -4px 15px 0 rgba(179, 179, 179, 0.25), + 4px 4px 15px 0 rgba(179, 179, 179, 0.25); + --about-journey-dot: url("../assets/about/dot.svg"); + --about-global-impact-image: url("../assets/about/global-impact.svg"); } - @media (min-width: $screen-md) { - h1 { - font-size: 3.5rem; + @if $mode == dark { + --header-next-section-bg: #1b1b1b; + --header-text-primary: #fff; + --header-text-secondary: #b3b3b3; + --header-bg: #101010; + --stats-bg: #000; + --stats-box-shadow: 4px -2px 15px 0 rgba(38, 38, 38, 0.2), + 4px -4px 15px 0 rgba(38, 38, 38, 0.15); + --stats-text-primary: #b3b3b3; + --stats-stat-bg: #394035; + --stats-stat-text: #8ff295; + --boxes-bg: #000; + --boxes-border: #4e4e4e; + --boxes-shadow: none; + --boxes-header-bg-1: #141e34; + --boxes-header-color-1: #9bb6f2; + --boxes-header-bg-2: #321d13; + --boxes-header-color-2: #e3642a; + --boxes-header-bg-3: #343114; + --boxes-header-color-3: #d4c53b; + --boxes-header-bg-4: #341419; + --boxes-header-color-4: #f19ca1; + --boxes-header-bg-5: #1d1434; + --boxes-header-color-5: #bf94ec; + --boxes-main-color: #b3b3b3; + --stairs-color: #8ff295; + --about-bg-primary: #1b1b1b; + --about-bg-secondary: #1b1b1b; + --about-join-us-bg: #000; + --about-join-us-border: 1px solid #4e4e4e; + --about-join-us-color: #b3b3b3; + --about-join-us-image: url("../assets/about/building-dark.svg"); + --about-tablist-border: #858585; + --about-tablist-color: #b3b3b3; + --about-tablist-active-border: #8ff295; + --about-tablist-active-color: #fff; + --about-color: #b3b3b3; + --about-heading-color: #fff; + --about-core-values-bg: #000; + --about-core-values-bg-secondary: #007936; + --about-core-values-color: #fff; + --about-core-values-shadow: 0 4px 15px 0 rgba(179, 179, 179, 0.2); + --about-team-title-color: #8ff295; + --about-team-bg: #000; + --about-team-github-bg: rgba(0, 0, 0, 0.7); + --about-team-shadow: 0 4px 15px 0 rgba(179, 179, 179, 0.2); + --about-team-color: #fff; + --about-journey-line-color: #8ff295; + --about-journey-image-border: #000; + --about-journey-shadow: 4px -4px 15px 0 rgba(179, 179, 179, 0.25), + 4px 4px 15px 0 rgba(179, 179, 179, 0.25); + --about-journey-dot: url("../assets/about/dot-dark.svg"); + --about-global-impact-image: url("../assets/about/global-impact-dark.svg"); + } + } + + h1, + h2, + h3, + h4, + h5, + h6 { + color: var(--about-heading-color); + letter-spacing: 0; + } + + strong { + letter-spacing: 0; + } + + > header { + --header-stats-height: var(--about-stats-height); + @include about.header; + + h1 + p { + font-size: 2rem; + font-weight: 500; + line-height: 120%; + max-width: 43rem; + + @media (max-width: $screen-md) { + font-size: 1.25rem; } } - header { - align-items: center; - display: flex; - flex-direction: column; - gap: 1rem; - padding: 2rem 0; + ul { + @include about.stats; + } + } + + > section { + --center-padding: max( + calc((100vw - var(--max-width)) / 2 + var(--gutter)), + var(--gutter) + ); + @include about.section; - .headline { - font-family: var(--font-heading); - font-size: 1.313rem; - font-style: normal; - font-variation-settings: normal; - font-weight: normal; - line-height: 175%; - margin: 0; + &[aria-labelledby="who_we_are"] { + background: var(--about-bg-secondary); + margin: 0; + max-width: none; + padding: 4rem var(--center-padding); + } + + .tabs { + grid-column: 1 / -1; + padding-top: 2rem; + position: relative; + + .tablist-wrapper { + background: var(--about-bg-secondary); + margin: 0 calc(-1 * var(--center-padding)); + margin-bottom: 2rem; + padding: 0 var(--center-padding); + position: sticky; + top: var(--top-nav-height); + z-index: 2; + } + + .tablist { + border-bottom: 1px solid var(--about-tablist-border); + display: flex; + gap: 3rem; + overflow-x: auto; + + @media (max-width: $screen-md) { + margin-left: calc(-1 * var(--gutter)); + margin-right: calc(-1 * var(--gutter)); + padding: 0 var(--gutter); + } + + a { + color: var(--about-tablist-color); + flex-shrink: 0; + text-decoration: none; + } + + .active { + border-bottom: 2px solid var(--about-tablist-active-border); + color: var(--about-tablist-active-color); + } + } + + > .tabpanel:not(.active) { + display: none !important; } } - p { - font-size: 1rem; - font-weight: 350; - line-height: 175%; + #what_we_offer-panel { + ul { + @include about.boxes; + + @media (min-width: $screen-md) { + gap: 5rem; + } + + @media (min-width: $screen-lg) { + gap: 5rem 8.5rem; + } + + li { + justify-content: flex-start; + } + + @for $i from 1 through 5 { + li:nth-of-type(#{$i}) { + --boxes-header-bg: var(--boxes-header-bg-#{$i}); + --boxes-header-color: var(--boxes-header-color-#{$i}); + } + } + } } - h2 { - font-size: 1.3rem; + #our_journey-panel { + --image-inner-height: calc(720px * 0.45); + --image-border-size: 4px; + --image-height: calc( + var(--image-inner-height) + var(--image-border-size) * 2 + ); + --image-width: calc( + var(--image-inner-height) * 16 / 9 + var(--image-border-size) * 2 + ); + --dot-height: 3rem; + --separator-width: 10rem; + --list-width: calc( + var(--inner-width) - var(--separator-width) - var(--image-width) + ); + + @media (max-width: $screen-xl) { + --separator-width: 6rem; + } + + > p { + margin-bottom: 1rem; + } + + > ul { + border-right: var(--about-journey-line-color) dashed 0.0625rem; + margin-top: 3rem; + width: var(--list-width); + + @media (max-width: $screen-lg) { + border-left: var(--about-journey-line-color) dashed 0.0625rem; + border-right: none; + margin-left: 1rem; + width: auto; + } + + li { + margin: 8rem 0; + min-height: var(--image-height); + padding-right: 2rem; + position: relative; + + &:first-of-type { + margin-top: 2rem; + } + + &:last-of-type { + margin-bottom: 2rem; + } + + @media (max-width: $screen-lg) { + padding-left: 2rem; + } + + &::after { + // dot on vertical dashed line + background-image: var(--about-journey-dot); + background-position: center center; + background-repeat: no-repeat; + content: ""; + display: block; + height: var(--dot-height); + position: absolute; + right: calc(var(--dot-height) / -2); + top: calc(var(--dot-height) / -2); + width: var(--dot-height); + + @media (max-width: $screen-lg) { + --dot-height: 2rem; + left: calc(var(--dot-height) / -2); + right: auto; + } + } + + &:has([id="2017"], [id="20232024"]) { + min-height: 0; + } + } + + h4 { + font-size: 1.375rem; + font-weight: 600; + line-height: 1.5; + margin: 0; + position: relative; + text-decoration-line: underline; + top: -0.75em; + + @media (max-width: $screen-lg) { + font-size: 1.25rem; + margin-bottom: -0.75em; + } + + &::before { + // horizontal line to dot + background: var(--about-journey-line-color); + content: ""; + display: none; + height: 10px; + left: calc(100% + 2rem); + mask-image: url("../assets/about/line-dot.svg"); + mask-position: center right; + mask-repeat: no-repeat; + mask-size: cover; + position: absolute; + top: calc(0.75em - 5px); + width: calc(var(--separator-width) - var(--dot-height) / 2); + + @media (max-width: $screen-lg) { + content: none; + } + } + + ~ p:last-of-type > img { + // image + background-color: var(--about-journey-image-border); + background-position: center center; + background-repeat: no-repeat; + background-size: contain; + border: var(--image-border-size) solid + var(--about-journey-image-border); + box-shadow: var(--about-journey-shadow); + height: var(--image-height); + left: calc(var(--inner-width) - var(--image-width)); + max-width: var(--image-width); + position: absolute; + top: calc(var(--dot-height) / -2); + width: var(--image-width); + + @media (max-width: $screen-lg) { + aspect-ratio: 16 / 9; + box-sizing: content-box; + height: auto; + left: auto; + margin: 2rem auto; + max-height: var(--image-inner-height); + max-width: 100%; + position: relative; + top: auto; + width: auto; + } + } + + &[id="2005"] { + &::before, + ~ p:last-of-type::after { + display: block; + } + } + + &[id="2010"] { + &::before, + ~ p:last-of-type::after { + display: block; + } + } + + &[id="2020"] { + &::before, + ~ p:last-of-type::after { + display: block; + } + } + + &[id="2022"] { + &::before, + ~ p:last-of-type::after { + display: block; + } + } + } + } + + h4 { + font-size: 1.75rem; + font-weight: 600; + } + + mdn-image-history ul { + display: flex; + gap: 1rem; + margin-bottom: -1rem; + overflow-x: auto; + padding-bottom: 1rem; // space for scrollbar + + li { + flex-shrink: 0; + } + + img { + aspect-ratio: 16 / 9; + border: var(--image-border-size) solid + var(--about-journey-image-border); + box-sizing: content-box; + height: calc(720px / 4); + } + } } - .heading-break { - display: none; - @media (min-width: $screen-md) { - display: block; + #our_core_values-panel { + li { + border-radius: 0.5rem; + color: var(--about-core-values-color); + + h4 { + color: var(--about-core-values-color); + font-size: 1rem; + font-weight: 600; + letter-spacing: -0.03125rem; + margin: 0; + margin-bottom: 0.5rem; + } + + &:not(:last-of-type) { + background: var(--about-core-values-bg-secondary); + margin: 1rem auto; + padding: 2rem; + + h4 { + --icon-size: 45px; + align-items: center; + background-position: left center; + background-repeat: no-repeat; + background-size: var(--icon-size) var(--icon-size); + display: flex; + min-height: var(--icon-size); + padding-left: calc(var(--icon-size) + 1rem); + + &#accurate_and_reliable { + background-image: url("../assets/about/accurate-sm.svg"); + } + + &#collaborative_and_community-driven { + background-image: url("../assets/about/collaborative-sm.svg"); + } + + &#inclusive_and_dynamic { + background-image: url("../assets/about/inclusive-sm.svg"); + } + } + } + + &:last-of-type { + background: var(--about-core-values-bg); + margin-top: 2rem; + padding: 2rem; + } + + @media (min-width: $screen-lg) { + display: grid; + grid-auto-flow: dense; + grid-template-columns: [left-start] 40% [left-end right-start] 1fr [right-end]; + padding: 2rem; + + h4 { + font-size: 1.75rem; + margin: 0; + max-width: 70%; + } + + p { + margin-left: var(--gutter); + } + + &:not(:last-of-type) { + background: var(--about-core-values-bg); + margin: 0 auto; + + &:not(:first-of-type) { + border-top-left-radius: 0; + border-top-right-radius: 0; + } + + &:not(:nth-last-of-type(2)) { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + } + + h4 { + --icon-size: 60px; + + align-self: center; + + &#accurate_and_reliable { + background-image: url("../assets/about/accurate.svg"); + } + + &#collaborative_and_community-driven { + background-image: url("../assets/about/collaborative.svg"); + } + + &#inclusive_and_dynamic { + background-image: url("../assets/about/inclusive.svg"); + } + } + + p { + background: var(--about-core-values-bg-secondary); + border-radius: 0.5rem; + padding: 2rem; + } + } + + &:nth-of-type(even):not(:last-of-type) { + h4 { + grid-column: right; + margin: 0 auto; + } + + p { + grid-column: left; + margin-left: 0; + margin-right: var(--gutter); + } + } + + &:last-of-type { + margin-top: 4rem; + } + } + } + } + + #our_team-panel, + #our_partners-panel > div { + --team-grid-gap: 2.5rem; + --team-card-padding: 1.5rem; + + display: grid; + gap: 0 var(--team-grid-gap); + grid-auto-flow: dense; + grid-template-columns: [full-start] 1fr 1fr 1fr [full-end]; + + @media (max-width: $screen-lg) { + --team-grid-gap: 2rem; + grid-template-columns: [full-start] 1fr 1fr [full-end]; + } + + > * { + margin: calc(var(--team-grid-gap) / 2) 0; + scroll-margin-top: calc( + var(--sticky-header-without-actions-height) + 1.5rem + 1rem + ); + } + + > h4, + p { + grid-column: full; + + &:first-child { + margin-top: 0; + } + + &:last-child { + margin-bottom: 0; + } + } + + h4, + h5 { + font-size: 1.75rem; + font-weight: 600; + text-transform: none; + + @media (max-width: $screen-md) { + font-size: 1rem; + } + } + + team-member { + align-content: start; + background: var(--about-team-bg); + border: 1px solid var(--about-team-bg); + box-shadow: var(--about-team-shadow); + color: var(--about-team-color); + cursor: pointer; + display: grid; + gap: 0; + grid-row: span 5; + grid-template-areas: + "name" + "title" + "picture" + "bio" + "."; + grid-template-rows: subgrid; + padding: calc(var(--team-card-padding) - 1px); + user-select: none; + + @media (max-width: $screen-sm) { + grid-column: full; + } + + &:hover { + border-color: var(--text-inactive); + } + + h4, + h5 { + grid-area: name; + margin: 0; + margin-bottom: 0.5rem; + } + + ul { + display: contents; + } + + li:first-of-type { + color: var(--about-team-title-color); + font-weight: 600; + grid-area: title; + margin-bottom: 0.75rem; + } + + li:nth-of-type(2) { + grid-area: picture; + } + + li:nth-of-type(3):not(:last-of-type) { + align-self: end; + grid-area: picture; + } + + li:last-of-type { + grid-area: bio; + line-height: 1.75; + @include line-clamp(3, 1.75, var(--about-team-bg)); + } + + img { + aspect-ratio: 1; + margin-bottom: 0.75rem; + width: 100%; + } + + a[href^="https://github.com"] + { + background: var(--about-team-github-bg); + border-top-right-radius: 0.5rem; + color: var(--about-team-color); + margin-top: -2.9rem; + padding: 0.25rem 0.5rem; + padding-right: 0.7rem; + position: absolute; + + &::before { + background: var(--about-team-color); + content: ""; + display: inline-block; + height: 1.2em; + margin-bottom: 0.2em; + margin-right: 0.2em; + mask-image: url("../assets/icons/github-mark-small.svg"); + mask-repeat: no-repeat; + vertical-align: middle; + width: 1.2em; + } + } + + &:focus-within { + align-content: start; + cursor: unset; + display: grid; + gap: 0 var(--team-card-padding); + grid-column: span 2; + grid-template-areas: + "name name" + "title title" + "picture bio" + ". bio" + ". bio"; + grid-template-columns: + calc( + (100% - var(--team-grid-gap) - 2 * var(--team-card-padding)) / 2 + ) + 1fr; + user-select: auto; + + li:last-of-type { + max-height: unset; + + &::after { + display: none; + } + } + + @media (max-width: $screen-sm) { + grid-template-areas: + "name" + "title" + "picture" + "bio"; + grid-template-columns: 1fr; + + ul li:nth-of-type(3):not(:last-of-type) { + align-self: end; + grid-area: picture; + + a { + position: absolute; + } + } + } + } + } + } + + #our_partners-panel > div { + margin-top: 2.5rem; + } + + &[aria-labelledby="global_impact"] { + display: block; + margin: var(--about-section-gap) auto; + + @media (min-width: $screen-lg) { + .section-content { + background: var(--about-global-impact-image); + background-position: right; + background-repeat: no-repeat; + background-size: 20rem 100%; + padding-right: 30rem; + } + } + + ul { + margin-top: 1rem; + @include about.stairs; + + li:nth-child(1) { + --stairs-icon: url("../assets/about/education.svg"); + } + + li:nth-child(2) { + --stairs-icon: url("../assets/about/text-box-check-outline.svg"); + } + + li:nth-child(3) { + --stairs-icon: url("../assets/about/web-check.svg"); + } + + li:nth-child(4) { + --stairs-icon: url("../assets/about/handshake.svg"); + } + } + } + + &[aria-labelledby="join_us_in_building_a_better_web"] { + background: var(--about-join-us-image) var(--about-join-us-bg); + background-position: 1.5rem; + background-repeat: no-repeat; + background-size: 20rem calc(100% - 3rem); + border: var(--about-join-us-border); + border-radius: 0.5rem; + color: var(--about-join-us-color); + display: flex; + flex-direction: column; + justify-content: center; + margin-bottom: var(--about-section-gap); + max-width: calc(100% - 2 * var(--center-padding)); + padding: 4rem 1rem; + padding-left: min(30rem, 50%); + + @media (max-width: $screen-md) { + background-color: transparent; + background-position: center bottom; + background-size: 100% 10rem; + border: none; + padding: 1rem 0; + padding-bottom: 12rem; } } } diff --git a/client/src/about/index.tsx b/client/src/about/index.tsx index 760c7bc0d3d5..92055d8c82f2 100644 --- a/client/src/about/index.tsx +++ b/client/src/about/index.tsx @@ -1,74 +1,201 @@ -import { useLocale } from "../hooks"; -import { GetInvolved } from "../ui/molecules/get_involved"; +import { HydrationData } from "../../../libs/types/hydration"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { ProseSection } from "../../../libs/types/document"; +import useSWR from "swr"; +import { HTTPError } from "../document"; +import { WRITER_MODE } from "../env"; +import { Prose } from "../document/ingredients/prose"; + import "./index.scss"; +import "./custom-elements"; +import { useGleanClick } from "../telemetry/glean-context"; +import { ABOUT } from "../telemetry/constants"; + +export interface AboutSection extends ProseSection { + H3s?: AboutSection[]; +} + +export interface AboutDoc { + title: string; + sections: AboutSection[]; +} + +export function About(appProps: HydrationData) { + const doc = useAboutDoc(appProps); -export function About() { - const locale = useLocale(); return ( -
-
-

Build it better

-

- MDN Web Docs is an open-source, collaborative project documenting Web - platform technologies, including{" "} - CSS,{" "} - HTML,{" "} - JavaScript, and{" "} - Web APIs. We also provide an - extensive set of{" "} - learning resources for beginning - developers and students. -

+
+ { + if (i === 0) { + return
; + } else if (section.H3s) { + return ; + } + return null; + }} + /> +
+ ); +} + +export function useAboutDoc( + appProps?: HydrationData +): AboutDoc | undefined { + const { data } = useSWR( + "index.json", + async () => { + const url = new URL( + `${window.location.pathname.replace(/\/$/, "")}/index.json`, + window.location.origin + ).toString(); + const response = await fetch(url); + + if (!response.ok) { + switch (response.status) { + case 404: + throw new HTTPError(response.status, url, "Page not found"); + } + + const text = await response.text(); + throw new HTTPError(response.status, url, text); + } + + return (await response.json())?.hyData; + }, + { + fallbackData: appProps?.hyData, + revalidateOnFocus: WRITER_MODE, + revalidateOnMount: true, + } + ); + const doc: AboutDoc | undefined = data || appProps?.hyData || undefined; + return doc; +} + +function RenderAboutBody({ + doc, + renderer = () => null, +}: { + doc?: AboutDoc; + renderer?: (section: AboutSection, i: number) => null | JSX.Element; +}) { + const sections = Array.from(doc?.sections || []).reduce( + (acc, curr) => { + if (curr.value.isH3) { + const prev = acc.at(-1); + if (prev) { + prev.H3s ? prev.H3s.push(curr) : (prev.H3s = [curr]); + } + } else { + acc.push(Object.assign({}, curr)); + } + return acc; + }, + [] + ); + return sections.map((section, i) => { + return ( + renderer(section, i) || ( + + ) + ); + }); +} + +export function Header({ section }: { section: AboutSection }) { + return section.value.content ? ( +
+
+
+ ) : null; +} + +function Tabs({ section }: { section: AboutSection }) { + const [activeTab, setActiveTab] = useState(0); + const tabs = useRef(null); + const gleanClick = useGleanClick(); + + const changeTab = useCallback( + (i: number) => { + const id = section.H3s?.[i].value.id; + if (id) { + setActiveTab(i); + if (tabs.current && tabs.current.getBoundingClientRect().top < 0) { + tabs.current.scrollIntoView({ block: "start", inline: "nearest" }); + } + gleanClick(`${ABOUT}: tab -> ${id}`); + } + }, + [section.H3s, gleanClick] + ); + + useEffect(() => { + const hash = document.location.hash.startsWith("#our_team") + ? "#our_team" + : document.location.hash.startsWith("#our_partners") || + document.location.hash === "#pab" || + document.location.hash === "#owd" + ? "#our_partners" + : document.location.hash; + const tab = section.H3s?.findIndex(({ value }) => `#${value.id}` === hash); + if (tab && tab > 0) { + setActiveTab(tab); + } + }, [section.H3s, changeTab]); -
- - - MDN's mission is to{" "} - provide a blueprint for a better internet and empower a new - generation of developers and content creators to build it. - - -
-

- We're always striving to connect developers more seamlessly with the - tools and information they need to easily build projects on the{" "} - open Web. Since our beginnings in 2005, Mozilla and - the community have amassed around 45,000 pages of free, open-source - content. -

- {/**/} -

- Independent and unbiased - across browsers and technologies -

-

- This guiding principle has made MDN Web Docs the go-to repository of - independent information for developers, regardless of brand, browser - or platform. We are an open community of devs, writers, and other - technologists building resources for a better Web, with over 17 - million monthly MDN users from all over the world. Anyone can - contribute, and each of the 45,000 individuals who have done so over - the past decades has strengthened and improved the resource. We also - receive content contributions from our partners, including Microsoft, - Google, Samsung, Igalia, W3C and others. Together we continue to drive - innovation on the Web and serve the common good. -

-

Accurate and vetted for quality

-

- Through our GitHub documentation repository, contributors can make - changes, submit pull requests, have their contributions reviewed and - then merged with existing content. Through{" "} - - this workflow - - , we welcome the vast knowledge and experience of our developer - community while maintaining a high level of quality, accurate content. -

-
- -
+ return ( + section.value.id && + section.value.content && ( +
+

{section.value.title}

+
+ {section.H3s && ( +
+
+
+ {section.H3s?.map( + ({ value }, i) => + value.id && + value.content && ( + changeTab(i)} + > + {value.title} + + ) + )} +
+
+ {section.H3s?.map( + ({ value }, i) => + value.id && + value.content && ( +
+ ) + )} +
+ )} +
+ ) ); } diff --git a/client/src/about/testimonial/index.scss b/client/src/about/testimonial/index.scss deleted file mode 100644 index 2a245a2bb3e9..000000000000 --- a/client/src/about/testimonial/index.scss +++ /dev/null @@ -1,64 +0,0 @@ -@use "../../ui/vars" as *; - -.testimonial { - display: flex; - flex-direction: column; - gap: 2rem; - padding: 2rem 0; - width: 100%; - - @media (min-width: $screen-md) { - flex-direction: row; - } - - iframe { - height: 50vw; - padding: 0; - - @media (min-width: $screen-md) { - height: initial; - width: 48%; - } - } - - .testimonial-copy { - margin: 0 auto; - max-width: 18rem; - text-align: center; - - @media (min-width: $screen-md) { - margin: initial; - text-align: initial; - } - - img { - margin: 0 auto; - - @media (min-width: $screen-md) { - margin: initial; - } - } - - p { - font-size: 18px; - line-height: 120%; - } - - .author-name, - .author-title { - display: block; - /* MDN UI / Body / M */ - font-size: 13px; - line-height: 1.2; - } - } - - .quotation-mark { - background-color: var(--icon-primary); - height: 10px; - margin-left: 4px; - mask-image: url("./quote.svg"); - mask-size: cover; - width: 10px; - } -} diff --git a/client/src/about/testimonial/index.tsx b/client/src/about/testimonial/index.tsx deleted file mode 100644 index 9d0695f0ee92..000000000000 --- a/client/src/about/testimonial/index.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import "./index.scss"; -import { ReactComponent as Quote } from "./quote.svg"; - -export function Testimonial() { - return ( -
- -
- -

- Condimentum donec quam odio viverra erat mi mae-cenas odio. Tempus - arcu tincidunt tortor placerat tempor pharetra. -

- - First Name - - Title goes here -
-
- ); -} diff --git a/client/src/about/testimonial/quote.svg b/client/src/about/testimonial/quote.svg deleted file mode 100644 index 19dcfe170995..000000000000 --- a/client/src/about/testimonial/quote.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/app.scss b/client/src/app.scss index f0e38641c4cf..6281566b0080 100644 --- a/client/src/app.scss +++ b/client/src/app.scss @@ -157,7 +157,6 @@ main { min-height: 80vh; } -.about-container, .main-page-content { a { &:link, @@ -355,7 +354,8 @@ sup.new { .sticky-header-container { position: sticky; - top: 0; + // Avoid gap on certain zoom levels. + top: -1px; z-index: var(--z-index-main-header); } diff --git a/client/src/app.tsx b/client/src/app.tsx index ff38d1aafa04..e452d6c89a3f 100644 --- a/client/src/app.tsx +++ b/client/src/app.tsx @@ -229,7 +229,7 @@ export function App(appProps: HydrationData) { to simulate it. */} @@ -322,7 +322,7 @@ export function App(appProps: HydrationData) { path="/about/*" element={ - + } /> diff --git a/client/src/assets/about/accurate-sm.svg b/client/src/assets/about/accurate-sm.svg new file mode 100644 index 000000000000..1bfb970504aa --- /dev/null +++ b/client/src/assets/about/accurate-sm.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/assets/about/accurate.svg b/client/src/assets/about/accurate.svg new file mode 100644 index 000000000000..a8fc0113c5ea --- /dev/null +++ b/client/src/assets/about/accurate.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/assets/about/building-dark.svg b/client/src/assets/about/building-dark.svg new file mode 100644 index 000000000000..76e77083c4d1 --- /dev/null +++ b/client/src/assets/about/building-dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/assets/about/building.svg b/client/src/assets/about/building.svg new file mode 100644 index 000000000000..848d7e6395b6 --- /dev/null +++ b/client/src/assets/about/building.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/assets/about/collaborative-sm.svg b/client/src/assets/about/collaborative-sm.svg new file mode 100644 index 000000000000..09cbf91b4ea6 --- /dev/null +++ b/client/src/assets/about/collaborative-sm.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/assets/about/collaborative.svg b/client/src/assets/about/collaborative.svg new file mode 100644 index 000000000000..56b62cce0fd3 --- /dev/null +++ b/client/src/assets/about/collaborative.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/assets/about/dot-dark.svg b/client/src/assets/about/dot-dark.svg new file mode 100644 index 000000000000..15aed46ddf1a --- /dev/null +++ b/client/src/assets/about/dot-dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/assets/about/dot.svg b/client/src/assets/about/dot.svg new file mode 100644 index 000000000000..9af28548b671 --- /dev/null +++ b/client/src/assets/about/dot.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/assets/about/education.svg b/client/src/assets/about/education.svg new file mode 100644 index 000000000000..8d4393d8497a --- /dev/null +++ b/client/src/assets/about/education.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/assets/about/global-impact-dark.svg b/client/src/assets/about/global-impact-dark.svg new file mode 100644 index 000000000000..f51d35856b88 --- /dev/null +++ b/client/src/assets/about/global-impact-dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/assets/about/global-impact.svg b/client/src/assets/about/global-impact.svg new file mode 100644 index 000000000000..96c6848942b1 --- /dev/null +++ b/client/src/assets/about/global-impact.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/assets/about/handshake.svg b/client/src/assets/about/handshake.svg new file mode 100644 index 000000000000..680d0ee30401 --- /dev/null +++ b/client/src/assets/about/handshake.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/assets/about/inclusive-sm.svg b/client/src/assets/about/inclusive-sm.svg new file mode 100644 index 000000000000..d85a3e471d74 --- /dev/null +++ b/client/src/assets/about/inclusive-sm.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/assets/about/inclusive.svg b/client/src/assets/about/inclusive.svg new file mode 100644 index 000000000000..db690b38296d --- /dev/null +++ b/client/src/assets/about/inclusive.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/assets/about/line-dot.svg b/client/src/assets/about/line-dot.svg new file mode 100644 index 000000000000..894138b85160 --- /dev/null +++ b/client/src/assets/about/line-dot.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/assets/about/sparkle.svg b/client/src/assets/about/sparkle.svg new file mode 100644 index 000000000000..87bdd6cb9b55 --- /dev/null +++ b/client/src/assets/about/sparkle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/assets/about/text-box-check-outline.svg b/client/src/assets/about/text-box-check-outline.svg new file mode 100644 index 000000000000..af670b7d9c92 --- /dev/null +++ b/client/src/assets/about/text-box-check-outline.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/assets/about/web-check.svg b/client/src/assets/about/web-check.svg new file mode 100644 index 000000000000..2d5f0e3c2e0e --- /dev/null +++ b/client/src/assets/about/web-check.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/assets/lines.svg b/client/src/assets/lines.svg new file mode 100644 index 000000000000..20189e58c466 --- /dev/null +++ b/client/src/assets/lines.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/community/index.scss b/client/src/community/index.scss index e82e3e6b4645..e73d9749105f 100644 --- a/client/src/community/index.scss +++ b/client/src/community/index.scss @@ -1,5 +1,6 @@ @use "../ui/vars" as *; @use "../ui/atoms/button/mixins" as button; +@use "../about/mixins" as about; @mixin light-theme { --community-bg-primary: #fcfcfc; @@ -76,71 +77,22 @@ main.community-container { --community-stats-height: 5.75rem; --community-section-gap: 5rem; --max-width: 74rem; - --negative-space: calc( - max(0px, 100vw - var(--max-width)) * -0.5 - var(--gutter) - ); + --layout-text-primary: var(--community-text-primary); background: var(--community-bg-secondary); color: var(--community-text-secondary); - h2, - h3, - p { - margin: 0; - } - - h2, - h3 { - color: var(--community-text-primary); - - a { - color: unset; - text-decoration: none; - } - } - - a { - text-decoration: underline; - - &:hover { - text-decoration: none; - } - } - - p + p { - margin-top: 1.5rem; - } - - section { - margin-left: auto; - margin-right: auto; - max-width: var(--max-width); - padding-left: var(--gutter); - padding-right: var(--gutter); - width: 100%; - } - - h2 { - font-size: 2rem; - font-weight: 600; - margin-bottom: 1rem; - - @media (max-width: $screen-md) { - font-size: 1.375rem; - } - } + @include about.layout; > header { - background: linear-gradient( - to top, - transparent 0%, - transparent calc(var(--community-stats-height) / 2), - var(--community-bg-primary) calc(var(--community-stats-height) / 2), - var(--community-bg-primary) 100% - ); + --header-text-primary: var(--community-text-primary); + --header-text-secondary: var(--community-header-text); + --header-stats-height: var(--community-stats-height); + --header-bg: var(--community-bg-primary); + + @include about.header; @media (max-width: $screen-md) { - padding-top: 1rem; text-align: center; } @@ -150,7 +102,6 @@ main.community-container { right; background-repeat: no-repeat; background-size: 50%; - padding-top: var(--community-section-gap); @media (max-width: $screen-md) { background-position: top center; @@ -159,21 +110,6 @@ main.community-container { } } - h1 { - color: var(--community-text-primary); - font-size: 2.5rem; - margin-bottom: 1rem; - - @media (max-width: $screen-md) { - font-size: 2rem; - } - } - - p { - color: var(--community-header-text); - margin-bottom: 1.5rem; - } - ul:first-of-type { display: flex; flex-wrap: wrap; @@ -194,73 +130,24 @@ main.community-container { } ul:last-of-type { - background: var(--community-card-bg); - border-radius: 0.5rem; - box-shadow: var(--community-box-shadow); - color: var(--community-text-primary); - display: flex; - gap: 1rem; - justify-content: space-around; - margin-top: var(--community-section-gap); - padding: 1rem; - - @media (max-width: $screen-md) { - flex-wrap: wrap; - margin-top: 2rem; - } - - li { - align-items: baseline; - column-gap: 1rem; - display: flex; - flex-wrap: wrap; - justify-content: center; - min-width: 7.75rem; - overflow-wrap: anywhere; - - @media (max-width: $screen-md) { - align-items: center; - flex: 1; - flex-direction: column; - justify-content: flex-start; - } - - strong { - align-items: center; - background: var(--community-header-stats-bg); - border-radius: 50%; - color: var(--community-text-success); - display: inline-flex; - height: 3.75rem; - justify-content: center; - width: 3.75rem; - } - } - } - } + --stats-bg: var(--community-card-bg); + --stats-box-shadow: var(--community-box-shadow); + --stats-text-primary: var(--community-text-primary); + --stats-stat-bg: var(--community-header-stats-bg); + --stats-stat-text: var(--community-text-success); - > header + section { - margin-top: 4.56rem; - - @media (max-width: $screen-md) { - margin-top: 2rem; + @include about.stats; } } > section { --community-circle-height: 57rem; - column-gap: min(5rem, 5vw); - display: grid; - grid-template-columns: 4fr 6fr; + + @include about.section; @media (max-width: $screen-md) { /* stylelint-disable-next-line length-zero-no-unit */ --community-circle-height: 0rem; - display: block; - } - - > * { - min-width: 0; } &[aria-labelledby="meet_our_contributors"] { @@ -492,6 +379,7 @@ main.community-container { &[aria-labelledby="join_us_in_shaping_a_better_web"] { display: block; + margin-bottom: var(--community-section-gap); margin-top: var(--community-section-gap); p { @@ -499,43 +387,16 @@ main.community-container { } ul { - display: grid; - gap: 2rem; - grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr)); - margin-bottom: var(--community-section-gap); - } + --boxes-bg: var(--community-card-bg); + --boxes-border: var(--community-card-border); + --boxes-shadow: var(--community-box-shadow); + --boxes-header-bg: var(--community-card-header-bg); - li { - align-items: center; - background: var(--community-card-bg); - border: 1px solid var(--community-card-border); - border-radius: 0.5rem; - box-shadow: var(--community-box-shadow); - display: flex; - flex-direction: column; - gap: 1.5rem; - justify-content: space-between; - padding: 1.5rem; - text-align: center; - - h3 { - align-self: stretch; - background: var(--community-card-header-bg); - border-radius: 0.5rem 0.5rem 0 0; - font-size: 1.25rem; - font-weight: 500; - margin: -1.5rem; - margin-bottom: 0; - padding: 1.5rem; - } - - p { - margin: 0; - } + @include about.boxes; + } - a { - @include button.primary; - } + li a { + @include button.primary; } } diff --git a/client/src/community/index.tsx b/client/src/community/index.tsx index 26b9e6526f81..e2290d123a52 100644 --- a/client/src/community/index.tsx +++ b/client/src/community/index.tsx @@ -1,21 +1,14 @@ import "./index.scss"; import { HydrationData } from "../../../libs/types/hydration"; -import { useEffect, useMemo } from "react"; -import { Section } from "../../../libs/types/document"; +import { useEffect } from "react"; import useSWR, { SWRConfig } from "swr"; -import { HTTPError } from "../document"; -import { WRITER_MODE } from "../env"; import { Prose } from "../document/ingredients/prose"; import { SWRLocalStorageCache } from "../utils"; import { useIsServer } from "../hooks"; +import { AboutDoc, AboutSection, Header, useAboutDoc } from "../about"; -interface CommunityDoc { - title: string; - sections: Section[]; -} - -export function Community(appProps: HydrationData) { - const doc = useCommunityDoc(appProps); +export function Community(appProps: HydrationData) { + const doc = useAboutDoc(appProps); useEffect(() => { import("./contributor-list"); @@ -25,18 +18,12 @@ export function Community(appProps: HydrationData) { new SWRLocalStorageCache("community") }} > -
+
{ if (i === 0) { - return ( -
- ); + return
; } else if (section.value.id === "help_us_fix_open_issues") { return ; } @@ -48,46 +35,12 @@ export function Community(appProps: HydrationData) { ); } -function useCommunityDoc( - appProps?: HydrationData -): CommunityDoc | undefined { - const { data } = useSWR( - "index.json", - async () => { - const url = new URL( - `${window.location.pathname.replace(/\/$/, "")}/index.json`, - window.location.origin - ).toString(); - const response = await fetch(url); - - if (!response.ok) { - switch (response.status) { - case 404: - throw new HTTPError(response.status, url, "Page not found"); - } - - const text = await response.text(); - throw new HTTPError(response.status, url, text); - } - - return (await response.json())?.hyData; - }, - { - fallbackData: appProps?.hyData, - revalidateOnFocus: WRITER_MODE, - revalidateOnMount: true, - } - ); - const doc: CommunityDoc | undefined = data || appProps?.hyData || undefined; - return doc; -} - function RenderCommunityBody({ doc, renderer = () => null, }: { - doc?: CommunityDoc; - renderer?: (section: Section, i: number) => null | JSX.Element; + doc?: AboutDoc; + renderer?: (section: AboutSection, i: number) => null | JSX.Element; }) { return doc?.sections.map((section, i) => { return ( @@ -98,23 +51,7 @@ function RenderCommunityBody({ }); } -function Header({ section, h1 }: { section: any; h1?: string }) { - const html = useMemo( - () => ({ __html: section.value?.content }), - [section.value?.content] - ); - return ( -
-
-
- ); -} - -function Issues({ section }: { section: any }) { - const html = useMemo( - () => ({ __html: section.value?.content }), - [section.value?.content] - ); +function Issues({ section }: { section: AboutSection }) { const isServer = useIsServer(); const LABELS = ["good first issue", "accepting PR"]; const { data } = useSWR( @@ -134,10 +71,13 @@ function Issues({ section }: { section: any }) { revalidateOnFocus: false, } ); - return ( + return section.value.id && section.value.content ? (

{section.value.title}

-
+
@@ -184,5 +124,5 @@ function Issues({ section }: { section: any }) {
- ); + ) : null; } diff --git a/client/src/homepage/homepage-hero/index.scss b/client/src/homepage/homepage-hero/index.scss index 1b118ee103bc..6cbb207f1bb3 100644 --- a/client/src/homepage/homepage-hero/index.scss +++ b/client/src/homepage/homepage-hero/index.scss @@ -99,6 +99,7 @@ .search-input-field { background-color: rgba(1, 1, 1, 0.5); + border-color: var(--border-primary); border-radius: 10rem; padding: 2rem; width: 100%; diff --git a/client/src/observatory/results.scss b/client/src/observatory/results.scss index cc1bacbfcf52..956f588b5163 100644 --- a/client/src/observatory/results.scss +++ b/client/src/observatory/results.scss @@ -99,6 +99,35 @@ margin-top: 1rem; } + .detail-header { + display: flex; + gap: 0.5rem; + padding: 0 1.5rem 0 0; + + .arrow { + color: var(--observatory-color-secondary); + } + + .detail-header-title { + font-weight: 600; + padding-right: 0.2rem; + } + + p { + margin: 1rem 0; + } + } + + .iso-date { + code { + font-weight: initial; + } + } + + .humanized-duration { + font-size: var(--type-smaller-font-size); + } + table { background: var(--observatory-table-bg); border: none; @@ -213,6 +242,16 @@ width: 1.3rem; } + @media (max-width: #{$screen-sm - 0.02}) { + td { + .iso-date { + code { + font-size: x-small; + } + } + } + } + @media (max-width: #{$screen-lg - 0.02}) { // responsive table min-width: 0; @@ -240,6 +279,10 @@ grid-auto-flow: column; grid-column: span 2; grid-template-columns: subgrid; + + .humanized-duration { + display: none; + } } td:before { diff --git a/client/src/observatory/results.tsx b/client/src/observatory/results.tsx index 5a2002c2b9fd..ab7d7e5a1431 100644 --- a/client/src/observatory/results.tsx +++ b/client/src/observatory/results.tsx @@ -132,16 +132,16 @@ function ObservatoryScanResults({ result, host }) { key: "csp", element: , }, - { - label: "Raw server headers", - key: "headers", - element: , - }, { label: "Cookies", key: "cookies", element: , }, + { + label: "Raw server headers", + key: "headers", + element: , + }, { label: "Scan history", key: "history", diff --git a/client/src/observatory/results/cookies.tsx b/client/src/observatory/results/cookies.tsx index c72c647b44c3..dd0deacbe6a6 100644 --- a/client/src/observatory/results/cookies.tsx +++ b/client/src/observatory/results/cookies.tsx @@ -1,93 +1,115 @@ import { ObservatoryResult } from "../types"; -import { formatDateTime, PassIcon } from "../utils"; +import { PassIcon, Timestamp } from "../utils"; export function ObservatoryCookies({ result }: { result: ObservatoryResult }) { const cookies = result.tests["cookies"]?.data; + const pass = result.tests["cookies"]?.pass; return cookies && Object.keys(cookies).length !== 0 ? ( - - - - - - - - - - - - - - {Object.entries(cookies).map(([key, value]) => ( - - - - - - - - + <> +
+

+ +

+
None

`, + }} + /> +
+
Name - - Expires - - - - Path - - - - Secure - - - - HttpOnly - - - - SameSite - - - - Prefix - -
- {key} - - {value.expires - ? formatDateTime(new Date(value.expires)) - : "Session"} - - {value.path} - - - - - - {value.samesite ? {capitalize(value.samesite)} : "-"} - - -
+ + + + + + + + + - ))} - -
Name + + Expires + + + + Path + + + + Secure + + + + HttpOnly + + + + SameSite + + + + Prefix + +
+ + + {Object.entries(cookies).map(([key, value]) => ( + + + {key} + + + {value.expires ? ( + + ) : ( + "Session" + )} + + + {value.path} + + + + + + + + + {value.samesite ? ( + {capitalize(value.samesite)} + ) : ( + "-" + )} + + + + + + ))} + + + ) : ( diff --git a/client/src/observatory/results/csp.tsx b/client/src/observatory/results/csp.tsx index 93f61265420f..13a187c91aea 100644 --- a/client/src/observatory/results/csp.tsx +++ b/client/src/observatory/results/csp.tsx @@ -36,72 +36,93 @@ export default function ObservatoryCSP({ "unsafeObjects", ]; - return ( -
- {policy ? ( - <> - - - - - - - - - {policyTests.map((pt) => { - return policy[pt] ? ( - - - - - ) : ( - [] - ); - })} - - - ) : ( - + const pass = result.tests["content-security-policy"]?.pass; + + // cookies && Object.keys(cookies).length !== 0 ? + return policy ? ( + <> +
+

+ +

+
None

`, + }} + /> +
+ +
TestResultInfo
- - -
+ - + + + + + + {policyTests.map((pt) => { + return policy[pt] ? ( + + + + + ) : ( + [] + ); + })} - )} +
-

- {result.tests["content-security-policy"]?.result === - "csp-not-implemented-but-reporting-enabled" ? ( - <> - Content-Security-Policy-Report-Only header - detected. Implement an enforced policy; see{" "} - - MDN's Content Security Policy (CSP) documentation - - . - - ) : ( - "No CSP headers detected" - )} -

-
TestResultInfo
+ + +
+ + ) : result.tests["content-security-policy"]?.result === + "csp-not-implemented-but-reporting-enabled" ? ( + + + + + + +
+

+ Content-Security-Policy-Report-Only header detected. + Implement an enforced policy; see{" "} + + MDN's Content Security Policy (CSP) documentation + + . +

+
+ ) : ( + + + + + +
+

No CSP headers detected

+
); } diff --git a/client/src/observatory/results/human-duration.tsx b/client/src/observatory/results/human-duration.tsx index 5eb5dacdadae..fed9748eeee8 100644 --- a/client/src/observatory/results/human-duration.tsx +++ b/client/src/observatory/results/human-duration.tsx @@ -1,5 +1,4 @@ import { useEffect, useState } from "react"; - import { formatDateTime } from "../utils"; export function HumanDuration({ date }: { date: Date }) { @@ -8,7 +7,7 @@ export function HumanDuration({ date }: { date: Date }) { useEffect(() => { const interval = setInterval(() => { setText(displayString(date)); - }, 1000); + }, 10000); return () => clearInterval(interval); }); @@ -20,32 +19,32 @@ export function HumanDuration({ date }: { date: Date }) { ); } +// breakpoints for humanized time durations +const MINUTE = 60; +const HOUR = MINUTE * 60; +const DAY = HOUR * 24; +const MONTH = DAY * 30; +const YEAR = DAY * 364; + function displayString(date: Date) { const currentTime = new Date().getTime(); const targetTime = date.getTime(); - const diffSecs = Math.round((currentTime - targetTime) / 1000); - - if (diffSecs < 0) { - return formatDateTime(date); - } - - if (diffSecs < 60) { - return `Just now`; + const diffSecs = Math.round((targetTime - currentTime) / 1000); + + const rtf = new Intl.RelativeTimeFormat("en", { style: "long" }); + const absSecs = Math.abs(diffSecs); + + if (absSecs < MINUTE) { + return diffSecs < 0 ? "Just now" : "Very soon"; + } else if (absSecs < HOUR) { + return rtf.format(Math.floor(diffSecs / MINUTE), "minute"); + } else if (absSecs < DAY) { + return rtf.format(Math.floor(diffSecs / HOUR), "hour"); + } else if (absSecs < MONTH) { + return rtf.format(Math.floor(diffSecs / DAY), "day"); + } else if (absSecs < YEAR) { + return rtf.format(Math.floor(diffSecs / MONTH), "month"); + } else { + return rtf.format(Math.floor(diffSecs / YEAR), "year"); } - if (diffSecs < 60 * 60) { - const minutes = Math.floor(diffSecs / 60); - return minutes === 1 ? `1 minute ago` : `${minutes} minutes ago`; - } - if (diffSecs < 60 * 60 * 24) { - const hours = Math.floor(diffSecs / 3600); - return hours === 1 ? `1 hour ago` : `${hours} hours ago`; - } - // up to 30 days as days - if (diffSecs < 60 * 60 * 24 * 30) { - const days = Math.floor(diffSecs / 86400); - return days === 1 ? `1 day ago` : `${days} days ago`; - } - - // after a week, return the formatted date - return formatDateTime(date); } diff --git a/client/src/observatory/types.ts b/client/src/observatory/types.ts index a262b1831305..7c1bc604740c 100644 --- a/client/src/observatory/types.ts +++ b/client/src/observatory/types.ts @@ -96,7 +96,7 @@ export type ObservatoryCookiesData = Record< export interface ObservatoryIndividualCookie { domain: string; - expires: number; + expires: string; httponly: boolean; path: string; samesite: string; diff --git a/client/src/observatory/utils.tsx b/client/src/observatory/utils.tsx index f05232c7570d..734b067c4e33 100644 --- a/client/src/observatory/utils.tsx +++ b/client/src/observatory/utils.tsx @@ -6,6 +6,7 @@ import { OBSERVATORY_API_URL } from "../env"; import { ObservatoryResult } from "./types"; import { ReactComponent as PassSVG } from "../../public/assets/observatory/pass-icon.svg"; import { ReactComponent as FailSVG } from "../../public/assets/observatory/fail-icon.svg"; +import { HumanDuration } from "./results/human-duration"; export function Link({ href, children }: { href: string; children: any }) { return ( @@ -123,6 +124,27 @@ export async function handleJsonResponse(res: Response): Promise { return await res.json(); } +export function Timestamp({ expires }: { expires: string }) { + const d = new Date(expires); + if (d.toString() === "Invalid Date") { + return
{expires}
; + } + const ts = d + .toISOString() + .replace("T", " ") + .replace(/\....Z/, " UTC"); + return ( + <> +
+ {ts} +
+
+ () +
+ + ); +} + export function formatDateTime(date: Date): string { return date.toLocaleString([], { dateStyle: "medium", @@ -130,7 +152,7 @@ export function formatDateTime(date: Date): string { }); } -export function hostAsRedirectChain(host, result: ObservatoryResult) { +export function hostAsRedirectChain(host: string, result: ObservatoryResult) { const chain = result.tests.redirection?.route; if (!chain || chain.length < 1) { return host; diff --git a/client/src/page-not-found/fallback-link.tsx b/client/src/page-not-found/fallback-link.tsx index d2cecdf8af8e..ada1741d01fd 100644 --- a/client/src/page-not-found/fallback-link.tsx +++ b/client/src/page-not-found/fallback-link.tsx @@ -67,10 +67,10 @@ export default function FallbackLink({ url }: { url: string }) { // What if we attempt to see if it would be something there in English? // We'll use the `index.json` version of the URL let enUSURL = url.replace(`/${locale}/`, "/en-US/"); - // But of the benefit of local development, devs can use `/_404/` + // But of the benefit of local development, devs can use `/404/` // instead of `/docs/` to simulate getting to the Page not found page. // So remove that when constructing the English index.json URL. - enUSURL = enUSURL.replace("/_404/", "/docs/"); + enUSURL = enUSURL.replace("/en-US/404/", "/en-US/docs/"); // The fallback check URL should not force append index.json so it can // follow any redirects diff --git a/client/src/page-not-found/index.tsx b/client/src/page-not-found/index.tsx index 46011ea4d253..caf611c52ead 100644 --- a/client/src/page-not-found/index.tsx +++ b/client/src/page-not-found/index.tsx @@ -8,7 +8,7 @@ const FallbackLink = React.lazy(() => import("./fallback-link")); // NOTE! To hack on this component, you have to use a trick to even get to this // unless you use the Express server on localhost:5042. -// To get here, use http://localhost:3000/en-US/_404/Whatever/you/like +// To get here, use http://localhost:3000/en-US/404/Whatever/you/like // Now hot-reloading works and you can iterate faster. // Otherwise, you can use http://localhost:5042/en-US/docs/Whatever/you/like // (note the :5042 port) and that'll test it a bit more realistically. diff --git a/client/src/playground/index.tsx b/client/src/playground/index.tsx index 0f2265e83112..67d54f06ab42 100644 --- a/client/src/playground/index.tsx +++ b/client/src/playground/index.tsx @@ -128,11 +128,13 @@ export default function Playground() { // We're using a random subdomain for origin isolation. const url = new URL( - `${window.location.protocol}//${ - PLAYGROUND_BASE_HOST.startsWith("localhost") - ? "" - : `${subdomain.current}.` - }${PLAYGROUND_BASE_HOST}` + window.location.hostname.endsWith("localhost") + ? window.location.origin + : `${window.location.protocol}//${ + PLAYGROUND_BASE_HOST.startsWith("localhost") + ? "" + : `${subdomain.current}.` + }${PLAYGROUND_BASE_HOST}` ); setVConsole([]); url.searchParams.set("state", state); diff --git a/client/src/playground/utils.ts b/client/src/playground/utils.ts index ee0bf1bfa887..dc053d6d8ba4 100644 --- a/client/src/playground/utils.ts +++ b/client/src/playground/utils.ts @@ -35,15 +35,17 @@ export async function initPlayIframe( JSON.stringify(editorContent) ); const path = iframe.getAttribute("data-live-path"); - const host = PLAYGROUND_BASE_HOST.startsWith("localhost") - ? PLAYGROUND_BASE_HOST - : `${hash}.${PLAYGROUND_BASE_HOST}`; const url = new URL( `${path || ""}${path?.endsWith("/") ? "" : "/"}runner.html`, window.location.origin ); - url.port = ""; - url.host = host; + if (!window.location.hostname.endsWith("localhost")) { + const host = PLAYGROUND_BASE_HOST.startsWith("localhost") + ? PLAYGROUND_BASE_HOST + : `${hash}.${PLAYGROUND_BASE_HOST}`; + url.port = ""; + url.host = host; + } url.search = ""; url.searchParams.set("state", state); iframe.src = url.href; @@ -90,6 +92,9 @@ function base64ToBytes(base64: string): ArrayBuffer { return bytes.buffer; } +/* + * This is the browser verision of `libs/play/index.js`. Keep in sync! + */ export async function decompressFromBase64(base64String: string) { if (!base64String) { return { state: null, hash: null }; diff --git a/client/src/setupProxy.js b/client/src/setupProxy.js index 8ab9ed5a2c2b..4edaa6930fb6 100644 --- a/client/src/setupProxy.js +++ b/client/src/setupProxy.js @@ -20,6 +20,8 @@ function config(app) { app.use(`**/*.(gif|jpeg|jpg|mp3|mp4|ogg|png|svg|webm|webp|woff2)`, proxy); // All those root-level images like /favicon-48x48.png app.use("/*.(png|webp|gif|jpe?g|svg)", proxy); + // Proxy play runner + app.use("**/runner.html", proxy); } export default config; diff --git a/client/src/telemetry/constants.ts b/client/src/telemetry/constants.ts index 1f6735dd726a..c2942fe6f269 100644 --- a/client/src/telemetry/constants.ts +++ b/client/src/telemetry/constants.ts @@ -29,6 +29,7 @@ export const SETTINGS = "settings"; export const OBSERVATORY = "observatory"; export const CURRICULUM = "curriculum"; export const BCD_TABLE = "bcd"; +export const ABOUT = "about"; export const A11Y_MENU = "a11y_menu"; diff --git a/client/src/ui/organisms/footer/index.tsx b/client/src/ui/organisms/footer/index.tsx index c39a46ffb7e3..40edce33855c 100644 --- a/client/src/ui/organisms/footer/index.tsx +++ b/client/src/ui/organisms/footer/index.tsx @@ -24,7 +24,7 @@ export function Footer() {