Read-only mirrors. Development, issues and pull requests are on GitHub.

summaryrefslogtreecommitdiffhomepage
path: root/apps/web/components/PreferenceControls.tsx
blob: c4f569055d39c47ad2e6561b4b554e7c9be67264 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
// SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
// SPDX-License-Identifier: AGPL-3.0-or-later WITH AdditionRef-Progmasoft-Patent-Grant-1.1

"use client";

import { useEffect, useSyncExternalStore, useTransition } from "react";
import { selectLocale } from "@/lib/locale.actions";
import {
  isLocale,
  localeNames,
  supportedLocales,
  type Locale,
} from "@/lib/localization";

interface PreferenceControlsProps {
  locale: Locale;
  labels: {
    dark: string;
    language: string;
    light: string;
    theme: string;
  };
}

type Theme = "light" | "dark";

const themeEvent = "progmasoft-theme-change";

function readTheme(): Theme {
  return window.localStorage.getItem("progmasoft_theme") === "dark"
    ? "dark"
    : "light";
}

function subscribeTheme(onStoreChange: () => void) {
  window.addEventListener("storage", onStoreChange);
  window.addEventListener(themeEvent, onStoreChange);
  return () => {
    window.removeEventListener("storage", onStoreChange);
    window.removeEventListener(themeEvent, onStoreChange);
  };
}

export function PreferenceControls({
  locale,
  labels,
}: PreferenceControlsProps) {
  const theme = useSyncExternalStore(subscribeTheme, readTheme, () => "light");
  const [switchingLocale, startLocaleSwitch] = useTransition();

  useEffect(() => {
    document.documentElement.dataset.theme = theme;
    document.documentElement.style.colorScheme = theme;
  }, [theme]);

  function toggleTheme() {
    const selected: Theme = theme === "light" ? "dark" : "light";
    window.localStorage.setItem("progmasoft_theme", selected);
    window.dispatchEvent(new Event(themeEvent));
  }

  // The server stores the preference and re-renders the current route in the
  // selected language, so the page changes in place: nothing is reloaded and
  // scroll position, theme and client state stay as they are.
  function chooseLocale(selected: string) {
    if (!isLocale(selected) || selected === locale) {
      return;
    }
    startLocaleSwitch(async () => {
      try {
        await selectLocale(selected);
      } catch {
        // The request did not reach the server. The query parameter asks the
        // proxy for the same preference with an ordinary navigation.
        const url = new URL(window.location.href);
        url.searchParams.set("lang", selected);
        window.location.assign(url);
      }
    });
  }

  return (
    <div className="preference-controls">
      <button
        type="button"
        onClick={toggleTheme}
        aria-label={labels.theme}
        title={labels.theme}
      >
        <span aria-hidden="true">{theme === "light" ? "◐" : "◑"}</span>
        <span>{theme === "light" ? labels.dark : labels.light}</span>
      </button>
      <select
        value={locale}
        onChange={(event) => chooseLocale(event.target.value)}
        aria-busy={switchingLocale}
        disabled={switchingLocale}
        aria-label={labels.language}
        title={labels.language}
      >
        {supportedLocales.map((supported) => (
          <option key={supported} value={supported} lang={supported}>
            {localeNames[supported]}
          </option>
        ))}
      </select>
    </div>
  );
}