diff --git a/.vscode/settings.json b/.vscode/settings.json index 3f0e5a50f8..a41347b23c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,7 +2,7 @@ "typescript.tsdk": "node_modules\\typescript\\lib", "html.autoClosingTags": false, "editor.wordWrapColumn": 80, - "editor.wordBasedSuggestions": false, + "editor.wordBasedSuggestions": "off", "[mdx]": { "editor.wordWrap": "bounded", "editor.wordWrapColumn": 80 @@ -22,7 +22,7 @@ "eslint.format.enable": true, "eslint.onIgnoredFiles": "warn", "editor.codeActionsOnSave": { - "source.fixAll.eslint": true + "source.fixAll.eslint": "explicit" }, "go.goroot": "${workspaceFolder}/bazel-${workspaceFolderBasename}/external/go_sdk", "go.toolsEnvVars": { diff --git a/project/zemn.me/BUILD.bazel b/project/zemn.me/BUILD.bazel index 784ef6993d..ab932b1648 100644 --- a/project/zemn.me/BUILD.bazel +++ b/project/zemn.me/BUILD.bazel @@ -14,10 +14,10 @@ ts_project( "//:node_modules/react", "//:node_modules/seedrandom", "//project/zemn.me/bio", - "//project/zemn.me/assets/kenwood", "//project/zemn.me/components", "//project/zemn.me/components/TimeEye", "//project/zemn.me/components/ZemnmezLogo", + "//project/zemn.me/components/HeroLayout", "//ts/next.js", "//ts/react/lang", "//ts/iter", diff --git a/project/zemn.me/app/ClientLayout.tsx b/project/zemn.me/app/ClientLayout.tsx deleted file mode 100644 index 698365320f..0000000000 --- a/project/zemn.me/app/ClientLayout.tsx +++ /dev/null @@ -1,36 +0,0 @@ -'use client'; -import 'project/zemn.me/app/base.css'; - -import Head from 'next/head'; -import { ReactNode } from 'react'; -import { HeaderTags } from 'ts/next.js'; - -export interface Props { - readonly children?: ReactNode; -} - -export function ClientLayout({ children }: Props) { - return ( - <> - - {children} - - - - - - - - - - ); -} - -export default ClientLayout; diff --git a/project/zemn.me/app/layout.tsx b/project/zemn.me/app/layout.tsx index 84772cd636..b8f43a026a 100644 --- a/project/zemn.me/app/layout.tsx +++ b/project/zemn.me/app/layout.tsx @@ -1,4 +1,6 @@ -import ClientLayout from 'project/zemn.me/app/ClientLayout'; +import 'project/zemn.me/app/base.css'; + +import { HeroLayout } from 'project/zemn.me/components/HeroLayout/HeroLayout'; import { ReactNode } from 'react'; export interface Props { @@ -9,6 +11,10 @@ export function RootLayout({ children }: Props) { return ( + - {children} + {children} ); diff --git a/project/zemn.me/app/page.tsx b/project/zemn.me/app/page.tsx index dd672da19d..56b0b63b74 100644 --- a/project/zemn.me/app/page.tsx +++ b/project/zemn.me/app/page.tsx @@ -1,240 +1,180 @@ 'use client'; -import Head from 'next/head'; import style from 'project/zemn.me/app/style.module.css'; -import * as kenwood from 'project/zemn.me/assets/kenwood'; import * as bio from 'project/zemn.me/bio'; import { dividerHeadingClass } from 'project/zemn.me/components/DividerHeading'; import Link from 'project/zemn.me/components/Link'; import { Prose } from 'project/zemn.me/components/Prose/prose'; import { Q } from 'project/zemn.me/components/Q'; -import { TimeEye } from 'project/zemn.me/components/TimeEye'; +import TimeEye from 'project/zemn.me/components/TimeEye'; import Timeline from 'project/zemn.me/components/timeline'; -import ZemnmezLogo from 'project/zemn.me/components/ZemnmezLogo/ZemnmezLogo'; +import { ZemnmezLogo } from 'project/zemn.me/components/ZemnmezLogo'; import * as lang from 'ts/react/lang'; -function ZemnmezLogoInline() { - return ; -} - -function TimeEyeInline() { - return ; -} - -/** - * LetterHead is the inner part of the heading with the name and logo. - */ -function LetterHead() { - return ( -
-
{lang.text(bio.Bio.who.handle)}
- -
Thomas NJ Shadwell
-
- ); -} - export default function Main() { return ( -
- - - {lang.text(bio.Bio.who.handle)} - - - - - -
- + <> +
+ +

+ I am an internationally recognised expert on computer + security, with specialisms in web security, security + program (SSDLC) construction, and automated security + analysis. +

+

+ I am a Member of Technical Staff at{' '} + OpenAI, where I + work on computer security. +

+

+ I am interested in consulting on legal cases. For + business, email me at{' '} + + thomas@shadwell.im + + . +

+

+ A selection of my work over the years can be found + below. +

+
+ <> + {bio.Bio.links !== undefined ? ( + + ) : null} +
-
-
- -

- I am an internationally recognised expert on - computer security, with specialisms in web security, - security program (SSDLC) construction, and automated - security analysis. -

-

- I am a Member of Technical Staff at{' '} - OpenAI, where - I work on computer security. -

-

- I am interested in consulting on legal cases. For - business, email me at{' '} - - thomas@shadwell.im - - . -

-

- A selection of my work over the years can be found - below. -

-
- <> - {bio.Bio.links !== undefined ? ( - - ) : null} - -
-
- -
-
-

- About. -

- -

The design of this website.

-

- This website is a direct descendant of one I made in - 2019. The core ideas come from very early on when I - was using the internet, and I didn't want to tell - people with my chosen username what kind of person I - was. I picked the username zemnmez to - be something meaningless that people could fill with - their own ideas of who I was. -

-

- Similarly, when I made the website, I didn't want to - tell people directly about myself, so instead I made - this timeline to keep track of what I had done every - year. The number in roman numerals is my age that - year. It fulfilled another role as I was collecting - my work to apply for my US O1 visa, which requires - proving that you've done a lot of interesting - things! -

-

- The background video (hero video) is - of a hidden area in the gardens of{' '} - - Kenwood House - - , a beautiful stately home sandwiched between - Highgate and Hampstead in London where I grew up. - It's located at about{' '} - - 51.57139601074658°N, -0.16924392259112794°E - - . -

-

- It used to be that there was a bench hidden under - overgrown bushes and a tree near the hydrangeas past - the orangery. I took a video from there one summer – - I was collecting photos and videos to remind me of - home because I knew I'd leave it behind someday to - move to the US. -

-

- The type and style itself was inspired by older, - pre-computer era typsetting such as the{' '} - - Lloyd's Act 1871 - - . Particular effort was put into trying to have - content fill horizontal space automatically, as seen - in older documents that try to make the most of the - paper they're printed on. -

-

- What's the difference between {' '} - and ? -

-

- The diamond logo () came out of - several years of wanting a way to express myself in - art. For a few years following, I changed logo - annually based how I'd felt the year prior, making - logos with geometry and construction lines. -

-

- When I eventually made the diamond logo, it ended up - looking a like an eye logo I'd made very early on in - 2012. I liked it so much it came to represent the - persona I had since 2009. The logo itself is from - much later, probably around 2015. -

-

- The time eye logo () was the later - (2019) creation, coming out of a specific need to - disambiguate between the published work I had as{' '} - Thomas Shadwell, my real name, versus{' '} - zemnmez, the persona I had used since - 2009. It became necessary after I made the Forbes - Under 30 list for my tax system hack in 2018. Before - this point I'd worked hard to try to keep the two - identities separate, but Forbes lists aren't really - for online personas. -

-

- The eye logo is a reference to the well-known{' '} - - eye of providence - - , a symbol that represents human achievement as - being incomplete without God. I wanted it to reflect - the idea that, in a universe that might not have a - God, we as people have a responsibility to care for - each other. -

-

- In having to make this distinction, for a short time - the work published as zemnmez{' '} - continued to represent the things I was most proud - of – an idealised kind of self. But at Google, I - started to publish security research I was really - proud of as both zemnmez and{' '} - Thomas Shadwell The abstract ideas are - still there, but now I'm more Thomas{' '} - than I ever was. ☺ -

-
-
-
-

- -

- - - This is what we become, when our eyes are open. - -
+
+ +
+
+

+ About. +

+ +

The design of this website.

+

+ This website is a direct descendant of one I made in + 2019. The core ideas come from very early on when I was + using the internet, and I didn't want to tell people + with my chosen username what kind of person I was. I + picked the username zemnmez to be + something meaningless that people could fill with their + own ideas of who I was. +

+

+ Similarly, when I made the website, I didn't want to + tell people directly about myself, so instead I made + this timeline to keep track of what I had done every + year. The number in roman numerals is my age that year. + It fulfilled another role as I was collecting my work to + apply for my US O1 visa, which requires proving that + you've done a lot of interesting things! +

+

+ The background video (hero video) is of a + hidden area in the gardens of{' '} + + Kenwood House + + , a beautiful stately home sandwiched between Highgate + and Hampstead in London where I grew up. It's located at + about{' '} + + 51.57139601074658°N, -0.16924392259112794°E + + . +

+

+ It used to be that there was a bench hidden under + overgrown bushes and a tree near the hydrangeas past the + orangery. I took a video from there one summer – I was + collecting photos and videos to remind me of home + because I knew I'd leave it behind someday to move to + the US. +

+

+ The type and style itself was inspired by older, + pre-computer era typsetting such as the{' '} + + Lloyd's Act 1871 + + . Particular effort was put into trying to have content + fill horizontal space automatically, as seen in older + documents that try to make the most of the paper they're + printed on. +

+

+ What's the difference between and{' '} + ? +

+

+ The diamond logo () came out of + several years of wanting a way to express myself in art. + For a few years following, I changed logo annually based + how I'd felt the year prior, making logos with geometry + and construction lines. +

+

+ When I eventually made the diamond logo, it ended up + looking a like an eye logo I'd made very early on in + 2012. I liked it so much it came to represent the + persona I had since 2009. The logo itself is from much + later, probably around 2015. +

+

+ The time eye logo () was the later + (2019) creation, coming out of a specific need to + disambiguate between the published work I had as{' '} + Thomas Shadwell, my real name, versus{' '} + zemnmez, the persona I had used since + 2009. It became necessary after I made the Forbes Under + 30 list for my tax system hack in 2018. Before this + point I'd worked hard to try to keep the two identities + separate, but Forbes lists aren't really for online + personas. +

+

+ The eye logo is a reference to the well-known{' '} + + eye of providence + + , a symbol that represents human achievement as being + incomplete without God. I wanted it to reflect the idea + that, in a universe that might not have a God, we as + people have a responsibility to care for each other. +

+

+ In having to make this distinction, for a short time the + work published as zemnmez continued to + represent the things I was most proud of – an idealised + kind of self. But at Google, I started to publish + security research I was really proud of as both{' '} + zemnmez and Thomas Shadwell{' '} + The abstract ideas are still there, but now I'm more{' '} + Thomas than I ever was. ☺ +

+
-
+ ); } diff --git a/project/zemn.me/app/style.module.css b/project/zemn.me/app/style.module.css index deec041d70..e69de29bb2 100644 --- a/project/zemn.me/app/style.module.css +++ b/project/zemn.me/app/style.module.css @@ -1,135 +0,0 @@ -.main { - display: grid; -} - -.banner, .headerBgv { - grid-area: banner; - width: 100%; - height: 100%; -} - -.banner { display: grid } - -.letterHead { - padding: 2em; - text-align: center; - background-color: var(--background-color); - margin: auto; - display: grid; - grid: - " logo " 2em - " handle " auto - " fullName " auto / auto; -} - -.letterHead { - font-size: large; -} - -.letterHead > .handle { - grid-area: handle; -} - -.letterHead > .fullName { - grid-area: fullName; -} - -.letterHead > .logo { - width: 100%; - height: 100%; - grid-area: logo; -} - -.banner h1 { - background-color: white; - margin: auto; -} - -.content { - grid-area: content; - padding-top: 4em; -} - -.headerBgv { - z-index: -1; - object-fit: cover; -} - -@media (orientation: landscape) { - .main { - grid: " banner content " 100vh - " xxxxxx content " 1fr - / 1fr 1fr ; - } - - /* - in this case, the banner should have a fixed - position, otherwise we'll scroll past it and the layout - will look weird. - */ - - .headerBgv, .banner { position: sticky; top: 0 } -} - -@media (orientation: portrait) { - .main { - grid: "banner" 100vh - "content" auto - /1fr ; - } -} - - -.footer { - display: grid; - grid: - "title title title title title" - "....... left-spacer future right-spacer ......." 6rem - "tagline tagline tagline tagline tagline" - "....... ....... ....... ....... ......." 4rem/1fr 2rem 3rem 2rem 1fr -} - -.footer > h2 { - grid-area: title; -} - -.future { - grid-area: future; - width: 100%; - height: 100% -} - -.footer:before { - content: "\2013"; - grid-area: left-spacer; - margin: auto; -} - -.tagline { - grid-area: tagline; - max-width: 15em; - margin: auto; - text-align: center; -} - -.footer:after { - content: "\2013"; - grid-area: right-spacer; - margin: auto; -} - - -.logoInline { - display: inline-block; - height: 1em; - width: auto; - vertical-align: middle; -} - -.links { - display: flex; -} - -.links > * { - margin: auto; -} diff --git a/project/zemn.me/components/HeroLayout/BUILD.bazel b/project/zemn.me/components/HeroLayout/BUILD.bazel new file mode 100644 index 0000000000..89b663f367 --- /dev/null +++ b/project/zemn.me/components/HeroLayout/BUILD.bazel @@ -0,0 +1,17 @@ +load("//ts:rules.bzl", "ts_project") + +ts_project( + name = "HeroLayout", + assets = glob([ "**/*.css" ]), + visibility = ["//project/zemn.me:__subpackages__"], + deps = [ + "//project/zemn.me/components", + "//:node_modules/@types/react", + "//:node_modules/react", + "//project/zemn.me/components/TimeEye", + "//project/zemn.me/components/ZemnmezLogo", + "//ts/react/lang", + "//project/zemn.me/bio", + "//project/zemn.me/assets/kenwood" + ] +) diff --git a/project/zemn.me/components/HeroLayout/HeroLayout.module.css b/project/zemn.me/components/HeroLayout/HeroLayout.module.css new file mode 100644 index 0000000000..f16cee9005 --- /dev/null +++ b/project/zemn.me/components/HeroLayout/HeroLayout.module.css @@ -0,0 +1,129 @@ + +.main { + display: grid; +} + +.banner, .headerBgv { + grid-area: banner; + width: 100%; + height: 100%; +} + +.banner { display: grid } + +.letterHead { + padding: 2em; + text-align: center; + background-color: var(--background-color); + margin: auto; + display: grid; + grid: + " logo " 2em + " handle " auto + " fullName " auto / auto; +} + +.letterHead { + font-size: large; +} + +.letterHead > .handle { + grid-area: handle; +} + +.letterHead > .fullName { + grid-area: fullName; +} + +.letterHead > .logo { + width: 100%; + height: 100%; + grid-area: logo; +} + +.banner h1 { + background-color: white; + margin: auto; +} + +.content { + grid-area: content; + padding-top: 4em; +} + +.headerBgv { + z-index: -1; + object-fit: cover; +} + +@media (orientation: landscape) { + .main { + grid: " banner content " 100vh + " xxxxxx content " 1fr + / 1fr 1fr ; + } + + /* + in this case, the banner should have a fixed + position, otherwise we'll scroll past it and the layout + will look weird. + */ + + .headerBgv, .banner { position: sticky; top: 0 } +} + +@media (orientation: portrait) { + .main { + grid: "banner" 100vh + "content" auto + /1fr ; + } +} + +.future { + grid-area: future; + width: 100%; + height: 100% +} + + +.tagline { + grid-area: tagline; + max-width: 15em; + margin: auto; + text-align: center; +} + +.links { + display: flex; +} + +.links > * { + margin: auto; +} + +.footer { + display: grid; + grid: + "title title title title title" + "....... left-spacer future right-spacer ......." 6rem + "tagline tagline tagline tagline tagline" + "....... ....... ....... ....... ......." 4rem/1fr 2rem 3rem 2rem 1fr +} + + +.footer > h2 { + grid-area: title; +} + +.footer:after { + content: "\2013"; + grid-area: right-spacer; + margin: auto; +} + +.footer:before { + content: "\2013"; + grid-area: left-spacer; + margin: auto; +} diff --git a/project/zemn.me/components/HeroLayout/HeroLayout.tsx b/project/zemn.me/components/HeroLayout/HeroLayout.tsx new file mode 100644 index 0000000000..aaf2b9a286 --- /dev/null +++ b/project/zemn.me/components/HeroLayout/HeroLayout.tsx @@ -0,0 +1,67 @@ +import classNames from 'classnames'; +import * as kenwood from 'project/zemn.me/assets/kenwood'; +import { Bio } from 'project/zemn.me/bio'; +import { dividerHeadingClass } from 'project/zemn.me/components/DividerHeading'; +import style from 'project/zemn.me/components/HeroLayout/HeroLayout.module.css'; +import TimeEye from 'project/zemn.me/components/TimeEye'; +import ZemnmezLogo from 'project/zemn.me/components/ZemnmezLogo'; +import { FC, ReactNode } from 'react'; +import { text } from 'ts/react/lang'; + +export interface FooterProps { + readonly className?: string; +} + +export const Footer: FC = function Footer(props) { + return ( +
+

+ +

+ + + This is what we become, when our eyes are open. + +
+ ); +}; + +/** + * LetterHead is the inner part of the heading with the name and logo. + */ +function LetterHead() { + return ( +
+
{text(Bio.who.handle)}
+ +
Thomas NJ Shadwell
+
+ ); +} + +export interface Props { + readonly className?: string; + readonly children?: ReactNode; +} + +export const HeroLayout: FC = function HeroLayout({ children }) { + return ( +
+ +
+ +
+
{children}
+
+
+ ); +}; diff --git a/project/zemn.me/components/TimeEye/BUILD.bazel b/project/zemn.me/components/TimeEye/BUILD.bazel index 117c00a93a..f893598c53 100644 --- a/project/zemn.me/components/TimeEye/BUILD.bazel +++ b/project/zemn.me/components/TimeEye/BUILD.bazel @@ -10,5 +10,6 @@ ts_project( "//:node_modules/@types/react", "//:node_modules/classnames", "//:node_modules/react", + "//project/zemn.me/css/svg", ], ) diff --git a/project/zemn.me/components/TimeEye/TimeEye.tsx b/project/zemn.me/components/TimeEye/TimeEye.tsx index 0d7b7fcc4f..a0db3dfc14 100644 --- a/project/zemn.me/components/TimeEye/TimeEye.tsx +++ b/project/zemn.me/components/TimeEye/TimeEye.tsx @@ -1,16 +1,28 @@ import classNames from 'classnames'; +import { inlineSVGClass } from 'project/zemn.me/css/svg/inline'; import React from 'react'; import style from './TimeEye.module.css'; -export const TimeEye: React.FC = ({ +type svgProps = JSX.IntrinsicElements['svg']; + +export interface Props extends svgProps { + readonly inline?: boolean; +} + +export const TimeEye: React.FC = ({ + inline = false, className, ...props }) => ( Thomas Shadwell Logo diff --git a/project/zemn.me/components/ZemnmezLogo/BUILD.bazel b/project/zemn.me/components/ZemnmezLogo/BUILD.bazel index b74f40b106..1c1ddc6b4a 100644 --- a/project/zemn.me/components/ZemnmezLogo/BUILD.bazel +++ b/project/zemn.me/components/ZemnmezLogo/BUILD.bazel @@ -16,5 +16,6 @@ ts_project( "//:node_modules/react", "//:node_modules/react-dom", "//:node_modules/react-router-dom", + "//project/zemn.me/css/svg", ], ) diff --git a/project/zemn.me/components/ZemnmezLogo/ZemnmezLogo.tsx b/project/zemn.me/components/ZemnmezLogo/ZemnmezLogo.tsx index e4a06112df..630f4fb7e7 100644 --- a/project/zemn.me/components/ZemnmezLogo/ZemnmezLogo.tsx +++ b/project/zemn.me/components/ZemnmezLogo/ZemnmezLogo.tsx @@ -1,30 +1,46 @@ import classes from 'classnames'; import style from 'project/zemn.me/components/ZemnmezLogo/ZemnmezLogo.module.css'; -import React from 'react'; +import { inlineSVGClass } from 'project/zemn.me/css/svg/inline'; +import React, { FC, SVGProps } from 'react'; -export default ({ +type svgElementProps = SVGProps; + +export interface Props extends svgElementProps { + readonly inline?: boolean; +} + +export const ZemnmezLogo: FC = function ZemnmezLogo({ className, children, + inline = false, ...props -}: React.SVGProps) => ( - - Zemnmez Logo - - One big square, two small squares and 4 rectangles make up a shape - that resembles a stylised, angular eye. A square, rotated 45° so - that its corners point up, down, left and right. The square has on - either side of it two similar smaller squares, separated by a small - gap. Each of the four square's sides have a rectangle following - their edges with the same small gap. - - {children} - - -); +}) { + return ( + + Zemnmez Logo + + One big square, two small squares and 4 rectangles make up a + shape that resembles a stylised, angular eye. A square, rotated + 45° so that its corners point up, down, left and right. The + square has on either side of it two similar smaller squares, + separated by a small gap. Each of the four square's sides have a + rectangle following their edges with the same small gap. + + {children} + + + ); +}; + +export default ZemnmezLogo; diff --git a/project/zemn.me/components/timeline/timeline.tsx b/project/zemn.me/components/timeline/timeline.tsx index f0427d97b3..920ffc677d 100644 --- a/project/zemn.me/components/timeline/timeline.tsx +++ b/project/zemn.me/components/timeline/timeline.tsx @@ -4,6 +4,7 @@ import Link from 'project/zemn.me/components/Link'; import style from 'project/zemn.me/components/timeline/timeline.module.css'; import React from 'react'; import * as lang from 'ts/react/lang'; +import { useLocale } from 'ts/react/lang/locale'; interface MutableText { corpus: string; @@ -127,7 +128,7 @@ function Month({ readonly month: ImmutableText; readonly events: Iterable; }) { - const locales = [...lang.useLocale()]; + const locales = [...useLocale()]; const locale = new Intl.Locale( Intl.DateTimeFormat.supportedLocalesOf(locales)[0] ); @@ -188,7 +189,7 @@ function Year({ export default function Timeline() { // this spread is just because Intl.DateTimeFormat expects a mutable array. - const locales = [...lang.useLocale()]; + const locales = [...useLocale()]; const locale = Intl.DateTimeFormat.supportedLocalesOf(locales)[0]; diff --git a/project/zemn.me/css/svg/BUILD.bazel b/project/zemn.me/css/svg/BUILD.bazel new file mode 100644 index 0000000000..2880ca8419 --- /dev/null +++ b/project/zemn.me/css/svg/BUILD.bazel @@ -0,0 +1,13 @@ +load("//ts:rules.bzl", "ts_project") + +package(default_visibility = [ + "//project/zemn.me:__subpackages__", +]) + +ts_project( + name = "svg", + assets = glob(["**/*.css"]), + deps = [ + "//:base_defs", + ] +) diff --git a/project/zemn.me/css/svg/inline.module.css b/project/zemn.me/css/svg/inline.module.css new file mode 100644 index 0000000000..2def0648f1 --- /dev/null +++ b/project/zemn.me/css/svg/inline.module.css @@ -0,0 +1,6 @@ +.inline { + display: inline-block; + height: 1em; + width: auto; + vertical-align: middle; +} diff --git a/project/zemn.me/css/svg/inline.ts b/project/zemn.me/css/svg/inline.ts new file mode 100644 index 0000000000..11d72003f8 --- /dev/null +++ b/project/zemn.me/css/svg/inline.ts @@ -0,0 +1,6 @@ +import style from 'project/zemn.me/css/svg/inline.module.css'; +/** + * CSS class name that puts an svg inline in text, as + * though it's a character. + */ +export const inlineSVGClass = style.inline; diff --git a/ts/react/lang/index.tsx b/ts/react/lang/index.tsx index 9a605105b0..523966fd23 100644 --- a/ts/react/lang/index.tsx +++ b/ts/react/lang/index.tsx @@ -1,6 +1,4 @@ -import React from 'react'; - -type Language = string; +export type Language = string; export class TextType { constructor( @@ -40,44 +38,3 @@ export const get = (v: Text): L => v.language; // https://github.com/typescript-eslint/typescript-eslint/issues/4062 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-constraint, @typescript-eslint/no-explicit-any export const text = (v: Text): T => v.text; - -/** - * The user's set locale (the user's language preference) - */ -export const locale = React.createContext(['en-GB']); - -const getLangs = () => { - if (typeof navigator !== 'undefined') - return navigator?.languages ?? [navigator?.language]; - - return []; -}; - -export function useLocale() { - const [languages, setLanguages] = React.useState([ - 'en-GB', - ]); - - React.useEffect(() => { - const listener = () => { - setLanguages(() => getLangs()); - }; - window.addEventListener('languagechange', listener); - listener(); - return () => window.removeEventListener('languagechange', listener); - }, [setLanguages]); - - return languages; -} - -export const LocaleProvider: React.FC<{ - readonly children?: React.ReactNode; -}> = ({ children }) => { - const languages = useLocale(); - return {children}; -}; - -/** - * The contextual lang (the content's language) - */ -export const lang = React.createContext('en-GB'); diff --git a/ts/react/lang/locale.tsx b/ts/react/lang/locale.tsx new file mode 100644 index 0000000000..b49c1cea5f --- /dev/null +++ b/ts/react/lang/locale.tsx @@ -0,0 +1,41 @@ +import { createContext, useEffect, useState } from 'react'; +import { Language } from 'ts/react/lang'; + +/** + * The user's set locale (the user's language preference) + */ +export const locale = createContext(['en-GB']); + +const getLangs = () => { + if (typeof navigator !== 'undefined') + return navigator?.languages ?? [navigator?.language]; + + return []; +}; + +export function useLocale() { + const [languages, setLanguages] = useState(['en-GB']); + + useEffect(() => { + const listener = () => { + setLanguages(() => getLangs()); + }; + window.addEventListener('languagechange', listener); + listener(); + return () => window.removeEventListener('languagechange', listener); + }, [setLanguages]); + + return languages; +} + +export const LocaleProvider: React.FC<{ + readonly children?: React.ReactNode; +}> = ({ children }) => { + const languages = useLocale(); + return {children}; +}; + +/** + * The contextual lang (the content's language) + */ +export const lang = createContext('en-GB');