1
0
Fork 0
mirror of https://github.com/sussy-code/smov.git synced 2024-12-30 16:17:41 +01:00

localize part of settings page

This commit is contained in:
mrjvs 2023-11-26 16:33:04 +01:00
parent 0ef492f58b
commit 5b71aae159
10 changed files with 195 additions and 130 deletions

View file

@ -27,6 +27,10 @@
}, },
"migration": { "migration": {
"failed": "Failed to migrate your data." "failed": "Failed to migrate your data."
},
"dmca": {
"title": "",
"text": ""
} }
}, },
"navigation": { "navigation": {
@ -44,6 +48,79 @@
"actions": { "actions": {
"copy": "Copy" "copy": "Copy"
}, },
"settings": {
"unsaved": "You have unsaved changes",
"reset": "Reset",
"save": "Save",
"sidebar": {
"info": {
"title": "App information",
"hostname": "Hostname",
"backendUrl": "Backend URL",
"userId": "User ID",
"notLoggedIn": "Not logged in",
"appVersion": "App version",
"backendVersion": "Backend version",
"unknownVersion": "Unknown",
"secure": "Secure",
"insecure": "Insecure"
}
},
"appearance": {
"title": "Appearance",
"activeTheme": "Active",
"themes": {
"default": "Default",
"blue": "Blue",
"teal": "Teal",
"red": "Red",
"gray": "Gray"
}
},
"account": {
"title": "Account",
"register": {
"title": "Sync to the cloud",
"text": "Instantly share your watch progress between devices and keep them synced.",
"cta": "Get started"
},
"profile": {
"title": "Edit profile picture",
"firstColor": "First color",
"secondColor": "Second color",
"userIcon": "User icon",
"finish": "Finish editing"
},
"devices": {
"title": "Devices",
"failed": "Failed to load sessions",
"deviceNameLabel": "Device name",
"removeDevice": "Remove"
}
},
"locale": {
"title": "Locale",
"language": "Application language",
"languageDescription": "Language applied to the entire application."
},
"captions": {
"title": "Captions"
},
"connections": {
"title": "Connections"
}
},
"faq": {
"title": "About us",
"q1": {
"title": "1",
"body": "Body of 1"
},
"how": {
"title": "1",
"body": "Body of 1"
}
},
"footer": { "footer": {
"tagline": "Watch your favorite shows and movies with this open source streaming app.", "tagline": "Watch your favorite shows and movies with this open source streaming app.",
"links": { "links": {

View file

@ -1,4 +1,4 @@
/* eslint-disable react/no-unescaped-entities */ import { useTranslation } from "react-i18next";
import { ThinContainer } from "@/components/layout/ThinContainer"; import { ThinContainer } from "@/components/layout/ThinContainer";
import { Ol } from "@/components/utils/Ol"; import { Ol } from "@/components/utils/Ol";
@ -16,95 +16,18 @@ function Question(props: { title: string; children: React.ReactNode }) {
} }
export function AboutPage() { export function AboutPage() {
const { t } = useTranslation();
return ( return (
<SubPageLayout> <SubPageLayout>
<ThinContainer> <ThinContainer>
<Heading1>About us</Heading1> <Heading1>{t("faq.title")}</Heading1>
<Ol <Ol
items={[ items={[
<Question title="What is Blue?"> <Question title={t("faq.q1.title")}>{t("faq.q1.body")}</Question>,
Blue, oh so blue, like the tranquil sky on a summer's day. It's
the color of calm and serenity, a gentle embrace for your senses.
When you think of blue, you think of the vast ocean stretching
endlessly, inviting you to dive deep into its azure depths.
</Question>,
<Question title="Huh?">
Blue is the color of dreams, where the world slows down, and you
can hear the whispers of the wind in the tall grass. It's a
symphony of peacefulness that resonates with your soul, like a
melody that lingers in your heart.
</Question>,
<Question title="What the hell are you talking about?">
Blue, like, it's totally, um, the essence of like, everything, you
know? It's like, you look at it, and it's like, it's there, but
it's also not there, and it's like, you're trying to grasp the
concept of blue, but it's like trying to catch a dream in a net
made of spaghetti, you know? It's like, it's the ultimate paradox,
and it's like, it's just blowing your mind, man, like, it's like
trying to find the meaning of life in a jar of peanut butter, but
the peanut butter is made of pure energy, man, and it's like,
whoa.
</Question>,
]} ]}
/> />
<Paragraph> <Heading2>{t("faq.how.title")}</Heading2>
Blue, oh so blue, like the tranquil sky on a summer's day. It's the <Paragraph>{t("faq.how.body")}</Paragraph>
color of calm and serenity, a gentle embrace for your senses. When you
think of blue, you think of the vast ocean stretching endlessly,
inviting you to dive deep into its azure depths. Blue is the color of
dreams, where the world slows down, and you can hear the whispers of
the wind in the tall grass. It's a symphony of peacefulness that
resonates with your soul, like a melody that lingers in your heart.
</Paragraph>
<Heading2>How does it work?</Heading2>
<Paragraph>
Blue, well, it's like this cosmic wavelength, man, and it's like the
universe is just vibin', you know? It's like, when you stare at the
blue, it's like you're staring at the secrets of the cosmos, like,
whoa, it's like a trippy trip to another dimension where time doesn't
even matter, and you're just floating in a sea of, like, blue, man.
And it's like, it's not just a color, it's a whole experience, like,
you're in this cosmic rollercoaster ride through the quantum soup of
existence, and you're just riding the blue wave, man.
</Paragraph>
<Paragraph>
Blue, like, it's totally, um, the essence of like, everything, you
know? It's like, you look at it, and it's like, it's there, but it's
also not there, and it's like, you're trying to grasp the concept of
blue, but it's like trying to catch a dream in a net made of
spaghetti, you know? It's like, it's the ultimate paradox, and it's
like, it's just blowing your mind, man, like, it's like trying to find
the meaning of life in a jar of peanut butter, but the peanut butter
is made of pure energy, man, and it's like, whoa.
</Paragraph>
<Heading2>Frequently asked questions</Heading2>
<Paragraph>
Blue, blue, b-b-b-bluuuuuueeeeeeeee, zippity zappity zoooooo, it's
like, you know, it's like, blue is like, um, you know, it's like, um,
like a thing, but it's also not a thing, and it's like, whoa, dude,
it's like, it's like trying to juggle invisible watermelons while
riding a unicycle made of rubber bands and ketchup, and it's like,
you're just floating in the cosmic jellyfish of existence, and the
jellyfish are like, playing the accordion, man, and it's like, the
accordion is made of, like, spaghetti and, like, um, interdimensional
cheese, and it's like, whoa, dude, like, whoa.
</Paragraph>
<Paragraph>
Bloo-bloo-bloo, bleepity-bloop, blibber-blabber, blarble-blurble, blue
is like, um, you know, flibberflabberfloober, like,
zoomity-zamity-zoom, and it's like, um, sproingity-sproing, like, uh,
gibber-gabber-gobblygook, you know, it's like, um,
jibber-jabber-jibberish, like, whatchamacallit, thingamajig,
doodad-doodad-dingdong, like, ploopity-ploop, um, blibbity-blam,
flibbity-floo, like, gobbledygook-gobbledygook,
whoopsy-daisy-dingleberry, and it's like, uh,
flibberflabberflooberzoomity-sproing, um, like,
blibber-gibber-jibber-jabber, thingamajig-whatchamacallit, like, you
know, thingamajig-doodad-doodledee, and it's like, um,
doodad-gobbledygook-doodley-doo, like,
ploopity-whoopsy-doodleberry-flibber, you know, it's like, uh, blue,
man, like, totally, um, blue.
</Paragraph>
</ThinContainer> </ThinContainer>
</SubPageLayout> </SubPageLayout>
); );

View file

@ -7,14 +7,15 @@ import { Heading1, Paragraph } from "@/components/utils/Text";
import { SubPageLayout } from "./layouts/SubPageLayout"; import { SubPageLayout } from "./layouts/SubPageLayout";
// TODO make email a constant
export function DmcaPage() { export function DmcaPage() {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<SubPageLayout> <SubPageLayout>
<ThinContainer> <ThinContainer>
<Heading1>{t("dmca.title")}</Heading1> <Heading1>{t("screens.dmca.title")}</Heading1>
<Paragraph>{t("dmca.description")}</Paragraph> <Paragraph>{t("screens.dmca.text")}</Paragraph>
<Paragraph className="flex space-x-3 items-center"> <Paragraph className="flex space-x-3 items-center">
<Icon icon={Icons.MAIL} /> <Icon icon={Icons.MAIL} />
<span>dmca@movie-web.app</span> <span>dmca@movie-web.app</span>

View file

@ -1,5 +1,6 @@
import classNames from "classnames"; import classNames from "classnames";
import { useCallback, useEffect, useMemo } from "react"; import { useCallback, useEffect, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { useAsyncFn } from "react-use"; import { useAsyncFn } from "react-use";
import { import {
@ -96,6 +97,7 @@ export function AccountSettings(props: {
} }
export function SettingsPage() { export function SettingsPage() {
const { t } = useTranslation();
const activeTheme = useThemeStore((s) => s.theme); const activeTheme = useThemeStore((s) => s.theme);
const setTheme = useThemeStore((s) => s.setTheme); const setTheme = useThemeStore((s) => s.setTheme);
@ -244,21 +246,21 @@ export function SettingsPage() {
state.changed ? "opacity-100" : "opacity-0" state.changed ? "opacity-100" : "opacity-0"
}`} }`}
> >
<p className="text-type-danger">You have unsaved changes</p> <p className="text-type-danger">{t("settings.unsaved")}</p>
<div className="space-x-3 w-full md:w-auto flex"> <div className="space-x-3 w-full md:w-auto flex">
<Button <Button
className="w-full md:w-auto" className="w-full md:w-auto"
theme="secondary" theme="secondary"
onClick={state.reset} onClick={state.reset}
> >
Reset {t("settings.reset")}
</Button> </Button>
<Button <Button
className="w-full md:w-auto" className="w-full md:w-auto"
theme="purple" theme="purple"
onClick={saveChanges} onClick={saveChanges}
> >
Save {t("settings.save")}
</Button> </Button>
</div> </div>
</div> </div>

View file

@ -1,4 +1,5 @@
import { useMemo } from "react"; import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { useAsyncFn } from "react-use"; import { useAsyncFn } from "react-use";
import { SessionResponse } from "@/backend/accounts/auth"; import { SessionResponse } from "@/backend/accounts/auth";
@ -18,6 +19,7 @@ export function Device(props: {
isCurrent?: boolean; isCurrent?: boolean;
onRemove?: () => void; onRemove?: () => void;
}) { }) {
const { t } = useTranslation();
const url = useBackendUrl(); const url = useBackendUrl();
const token = useAuthStore((s) => s.account?.token); const token = useAuthStore((s) => s.account?.token);
const [result, exec] = useAsyncFn(async () => { const [result, exec] = useAsyncFn(async () => {
@ -32,12 +34,14 @@ export function Device(props: {
paddingClass="px-6 py-4" paddingClass="px-6 py-4"
> >
<div className="font-medium"> <div className="font-medium">
<SecondaryLabel>Device name</SecondaryLabel> <SecondaryLabel>
{t("settings.account.devices.deviceNameLabel")}
</SecondaryLabel>
<p className="text-white">{props.name}</p> <p className="text-white">{props.name}</p>
</div> </div>
{!props.isCurrent ? ( {!props.isCurrent ? (
<Button theme="danger" loading={result.loading} onClick={exec}> <Button theme="danger" loading={result.loading} onClick={exec}>
Remove {t("settings.account.devices.removeDevice")}
</Button> </Button>
) : null} ) : null}
</SettingsCard> </SettingsCard>
@ -50,6 +54,7 @@ export function DeviceListPart(props: {
sessions: SessionResponse[]; sessions: SessionResponse[];
onChange?: () => void; onChange?: () => void;
}) { }) {
const { t } = useTranslation();
const seed = useAuthStore((s) => s.account?.seed); const seed = useAuthStore((s) => s.account?.seed);
const sessions = props.sessions; const sessions = props.sessions;
const currentSessionId = useAuthStore((s) => s.account?.sessionId); const currentSessionId = useAuthStore((s) => s.account?.sessionId);
@ -75,10 +80,10 @@ export function DeviceListPart(props: {
return ( return (
<div> <div>
<Heading2 border className="mt-0 mb-9"> <Heading2 border className="mt-0 mb-9">
Devices {t("settings.account.devices.title")}
</Heading2> </Heading2>
{props.error ? ( {props.error ? (
<p>Failed to load sessions</p> <p>{t("settings.account.devices.failed")}</p>
) : props.loading ? ( ) : props.loading ? (
<Loading /> <Loading />
) : ( ) : (

View file

@ -1,3 +1,5 @@
import { useTranslation } from "react-i18next";
import { FlagIcon } from "@/components/FlagIcon"; import { FlagIcon } from "@/components/FlagIcon";
import { Dropdown } from "@/components/form/Dropdown"; import { Dropdown } from "@/components/form/Dropdown";
import { Heading1 } from "@/components/utils/Text"; import { Heading1 } from "@/components/utils/Text";
@ -8,7 +10,8 @@ export function LocalePart(props: {
language: string; language: string;
setLanguage: (l: string) => void; setLanguage: (l: string) => void;
}) { }) {
const sorted = sortLangCodes(appLanguageOptions.map((t) => t.code)); const { t } = useTranslation();
const sorted = sortLangCodes(appLanguageOptions.map((item) => item.code));
const options = appLanguageOptions const options = appLanguageOptions
.sort((a, b) => sorted.indexOf(a.code) - sorted.indexOf(b.code)) .sort((a, b) => sorted.indexOf(a.code) - sorted.indexOf(b.code))
@ -18,14 +21,16 @@ export function LocalePart(props: {
leftIcon: <FlagIcon countryCode={opt.code} />, leftIcon: <FlagIcon countryCode={opt.code} />,
})); }));
const selected = options.find((t) => t.id === props.language); const selected = options.find((item) => item.id === props.language);
return ( return (
<div> <div>
<Heading1 border>Locale</Heading1> <Heading1 border>{t("settings.locale.title")}</Heading1>
<p className="text-white font-bold mb-3">Application language</p> <p className="text-white font-bold mb-3">
{t("settings.locale.language")}
</p>
<p className="max-w-[20rem] font-medium"> <p className="max-w-[20rem] font-medium">
Language applied to the entire application. {t("settings.locale.languageDescription")}
</p> </p>
<Dropdown <Dropdown
options={options} options={options}

View file

@ -1,3 +1,5 @@
import { useTranslation } from "react-i18next";
import { Button } from "@/components/buttons/Button"; import { Button } from "@/components/buttons/Button";
import { ColorPicker } from "@/components/form/ColorPicker"; import { ColorPicker } from "@/components/form/ColorPicker";
import { IconPicker } from "@/components/form/IconPicker"; import { IconPicker } from "@/components/form/IconPicker";
@ -17,30 +19,34 @@ export interface ProfileEditModalProps {
} }
export function ProfileEditModal(props: ProfileEditModalProps) { export function ProfileEditModal(props: ProfileEditModalProps) {
const { t } = useTranslation();
return ( return (
<Modal id={props.id}> <Modal id={props.id}>
<ModalCard> <ModalCard>
<Heading2 className="!mt-0">Edit profile picture</Heading2> <Heading2 className="!mt-0">
{t("settings.account.profile.title")}
</Heading2>
<div className="space-y-6"> <div className="space-y-6">
<ColorPicker <ColorPicker
label="First color" label={t("settings.account.profile.firstColor")}
value={props.colorA} value={props.colorA}
onInput={props.setColorA} onInput={props.setColorA}
/> />
<ColorPicker <ColorPicker
label="Second color" label={t("settings.account.profile.secondColor")}
value={props.colorB} value={props.colorB}
onInput={props.setColorB} onInput={props.setColorB}
/> />
<IconPicker <IconPicker
label="User icon" label={t("settings.account.profile.userIcon")}
value={props.userIcon} value={props.userIcon}
onInput={props.setUserIcon} onInput={props.setUserIcon}
/> />
</div> </div>
<div className="flex justify-center mt-8"> <div className="flex justify-center mt-8">
<Button theme="purple" className="!px-20" onClick={props.close}> <Button theme="purple" className="!px-20" onClick={props.close}>
Finish editing {t("settings.account.profile.finish")}
</Button> </Button>
</div> </div>
</ModalCard> </ModalCard>

View file

@ -1,3 +1,4 @@
import { useTranslation } from "react-i18next";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
import { Button } from "@/components/buttons/Button"; import { Button } from "@/components/buttons/Button";
@ -6,6 +7,7 @@ import { Heading3 } from "@/components/utils/Text";
export function RegisterCalloutPart() { export function RegisterCalloutPart() {
const history = useHistory(); const history = useHistory();
const { t } = useTranslation();
return ( return (
<div> <div>
@ -14,15 +16,14 @@ export function RegisterCalloutPart() {
className="grid grid-cols-2 gap-12 mt-5" className="grid grid-cols-2 gap-12 mt-5"
> >
<div> <div>
<Heading3>Sync to the cloud</Heading3> <Heading3>{t("settings.account.register.title")}</Heading3>
<p className="text-type-text"> <p className="text-type-text">
Instantly share your watch progress between devices and keep them {t("settings.account.register.text")}
synced.
</p> </p>
</div> </div>
<div className="flex justify-end items-center"> <div className="flex justify-end items-center">
<Button theme="purple" onClick={() => history.push("/register")}> <Button theme="purple" onClick={() => history.push("/register")}>
Get started {t("settings.account.register.cta")}
</Button> </Button>
</div> </div>
</SolidSettingsCard> </SolidSettingsCard>

View file

@ -1,4 +1,5 @@
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import Sticky from "react-sticky-el"; import Sticky from "react-sticky-el";
import { useAsync } from "react-use"; import { useAsync } from "react-use";
@ -14,16 +15,22 @@ import { useAuthStore } from "@/stores/auth";
const rem = 16; const rem = 16;
function SecureBadge(props: { url: string }) { function SecureBadge(props: { url: string }) {
const { t } = useTranslation();
const secure = props.url.startsWith("https://"); const secure = props.url.startsWith("https://");
return ( return (
<div className="flex items-center gap-1 -mx-1 ml-3 px-1 rounded bg-largeCard-background font-bold"> <div className="flex items-center gap-1 -mx-1 ml-3 px-1 rounded bg-largeCard-background font-bold">
<Icon icon={secure ? Icons.LOCK : Icons.UNLOCK} /> <Icon icon={secure ? Icons.LOCK : Icons.UNLOCK} />
Secure {t(
secure
? "settings.sidebar.info.secure"
: "settings.sidebar.info.insecure"
)}
</div> </div>
); );
} }
export function SidebarPart() { export function SidebarPart() {
const { t } = useTranslation();
const { isMobile } = useIsMobile(); const { isMobile } = useIsMobile();
const { account } = useAuthStore(); const { account } = useAuthStore();
// eslint-disable-next-line no-restricted-globals // eslint-disable-next-line no-restricted-globals
@ -31,11 +38,31 @@ export function SidebarPart() {
const [activeLink, setActiveLink] = useState(""); const [activeLink, setActiveLink] = useState("");
const settingLinks = [ const settingLinks = [
{ text: "Account", id: "settings-account", icon: Icons.USER }, {
{ text: "Locale", id: "settings-locale", icon: Icons.BOOKMARK }, textKey: "settings.account.title",
{ text: "Appearance", id: "settings-appearance", icon: Icons.GITHUB }, id: "settings-account",
{ text: "Captions", id: "settings-captions", icon: Icons.CAPTIONS }, icon: Icons.USER,
{ text: "Connections", id: "settings-connection", icon: Icons.LINK }, },
{
textKey: "settings.locale.title",
id: "settings-locale",
icon: Icons.BOOKMARK,
},
{
textKey: "settings.appearance.title",
id: "settings-appearance",
icon: Icons.GITHUB,
},
{
textKey: "settings.captions.title",
id: "settings-captions",
icon: Icons.CAPTIONS,
},
{
textKey: "settings.connections.title",
id: "settings-connection",
icon: Icons.LINK,
},
]; ];
const backendUrl = useBackendUrl(); const backendUrl = useBackendUrl();
@ -103,24 +130,29 @@ export function SidebarPart() {
onClick={() => scrollTo(v.id)} onClick={() => scrollTo(v.id)}
key={v.id} key={v.id}
> >
{v.text} {t(v.textKey)}
</SidebarLink> </SidebarLink>
))} ))}
</SidebarSection> </SidebarSection>
<Divider /> <Divider />
</div> </div>
<SidebarSection className="text-sm" title="App information"> <SidebarSection
className="text-sm"
title={t("settings.sidebar.info.title")}
>
<div className="px-3 py-3.5 rounded-lg bg-largeCard-background bg-opacity-50 grid grid-cols-2 gap-4"> <div className="px-3 py-3.5 rounded-lg bg-largeCard-background bg-opacity-50 grid grid-cols-2 gap-4">
{/* Hostname */} {/* Hostname */}
<div className="col-span-2 space-y-1"> <div className="col-span-2 space-y-1">
<p className="text-type-dimmed font-medium">Hostname</p> <p className="text-type-dimmed font-medium">
{t("settings.sidebar.info.hostname")}
</p>
<p className="text-white">{hostname}</p> <p className="text-white">{hostname}</p>
</div> </div>
{/* Backend URL */} {/* Backend URL */}
<div className="col-span-2 space-y-1"> <div className="col-span-2 space-y-1">
<p className="text-type-dimmed font-medium flex items-center"> <p className="text-type-dimmed font-medium flex items-center">
Backend URL {t("settings.sidebar.info.backendUrl")}
<SecureBadge url={backendUrl} /> <SecureBadge url={backendUrl} />
</p> </p>
<p className="text-white"> <p className="text-white">
@ -130,13 +162,19 @@ export function SidebarPart() {
{/* User ID */} {/* User ID */}
<div className="col-span-2 space-y-1"> <div className="col-span-2 space-y-1">
<p className="text-type-dimmed font-medium">User ID</p> <p className="text-type-dimmed font-medium">
<p className="text-white">{account?.userId ?? "Not logged in"}</p> {t("settings.sidebar.info.userId")}
</p>
<p className="text-white">
{account?.userId ?? t("settings.sidebar.info.notLoggedIn")}
</p>
</div> </div>
{/* App version */} {/* App version */}
<div className="col-span-1 space-y-1"> <div className="col-span-1 space-y-1">
<p className="text-type-dimmed font-medium">App version</p> <p className="text-type-dimmed font-medium">
{t("settings.sidebar.info.appVersion")}
</p>
<p className="text-type-dimmed px-2 py-1 rounded bg-settings-sidebar-badge inline-block"> <p className="text-type-dimmed px-2 py-1 rounded bg-settings-sidebar-badge inline-block">
{conf().APP_VERSION} {conf().APP_VERSION}
</p> </p>
@ -144,7 +182,9 @@ export function SidebarPart() {
{/* Backend version */} {/* Backend version */}
<div className="col-span-1 space-y-1"> <div className="col-span-1 space-y-1">
<p className="text-type-dimmed font-medium">Backend version</p> <p className="text-type-dimmed font-medium">
{t("settings.sidebar.info.backendVersion")}
</p>
<p className="text-type-dimmed px-2 py-1 rounded bg-settings-sidebar-badge inline-flex items-center gap-1"> <p className="text-type-dimmed px-2 py-1 rounded bg-settings-sidebar-badge inline-flex items-center gap-1">
{backendMeta.error ? ( {backendMeta.error ? (
<Icon <Icon
@ -155,7 +195,8 @@ export function SidebarPart() {
{backendMeta.loading ? ( {backendMeta.loading ? (
<div className="h-4 w-12 bg-type-dimmed/20 rounded" /> <div className="h-4 w-12 bg-type-dimmed/20 rounded" />
) : ( ) : (
backendMeta?.value?.version || "Unknown" backendMeta?.value?.version ||
t("settings.sidebar.info.unknownVersion")
)} )}
</p> </p>
</div> </div>

View file

@ -1,4 +1,5 @@
import classNames from "classnames"; import classNames from "classnames";
import { useTranslation } from "react-i18next";
import { Icon, Icons } from "@/components/Icon"; import { Icon, Icons } from "@/components/Icon";
import { Heading1 } from "@/components/utils/Text"; import { Heading1 } from "@/components/utils/Text";
@ -6,19 +7,19 @@ import { Heading1 } from "@/components/utils/Text";
const availableThemes = [ const availableThemes = [
{ {
id: "blue", id: "blue",
name: "Blue", key: "settings.themes.blue",
}, },
{ {
id: "teal", id: "teal",
name: "Teal", key: "settings.themes.teal",
}, },
{ {
id: "red", id: "red",
name: "Red", key: "settings.themes.red",
}, },
{ {
id: "gray", id: "gray",
name: "Gray", key: "settings.themes.gray",
}, },
]; ];
@ -28,6 +29,8 @@ function ThemePreview(props: {
name: string; name: string;
onClick?: () => void; onClick?: () => void;
}) { }) {
const { t } = useTranslation();
return ( return (
<div <div
className={classNames(props.selector, "cursor-pointer group tabbable")} className={classNames(props.selector, "cursor-pointer group tabbable")}
@ -58,7 +61,6 @@ function ThemePreview(props: {
)} )}
/> />
{/* Mini movie-web. So Kawaiiiii! */} {/* Mini movie-web. So Kawaiiiii! */}
{/* ^ can we keep this comment in forever please? - Jip */}
<div className="absolute bottom-0 left-1/2 transform -translate-x-1/2 w-3/5 h-4/5 rounded-t-lg -mb-px bg-background-main overflow-hidden"> <div className="absolute bottom-0 left-1/2 transform -translate-x-1/2 w-3/5 h-4/5 rounded-t-lg -mb-px bg-background-main overflow-hidden">
<div className="relative w-full h-full"> <div className="relative w-full h-full">
{/* Background color */} {/* Background color */}
@ -106,7 +108,7 @@ function ThemePreview(props: {
props.active ? "opacity-100" : "opacity-0 pointer-events-none" props.active ? "opacity-100" : "opacity-0 pointer-events-none"
)} )}
> >
Active {t("settings.appearance.activeTheme")}
</span> </span>
</div> </div>
</div> </div>
@ -117,13 +119,15 @@ export function ThemePart(props: {
active: string | null; active: string | null;
setTheme: (theme: string | null) => void; setTheme: (theme: string | null) => void;
}) { }) {
const { t } = useTranslation();
return ( return (
<div> <div>
<Heading1 border>Appearance</Heading1> <Heading1 border>{t("settings.appearance.title")}</Heading1>
<div className="grid grid-cols-[repeat(auto-fill,minmax(160px,1fr))] gap-6 max-w-[700px]"> <div className="grid grid-cols-[repeat(auto-fill,minmax(160px,1fr))] gap-6 max-w-[700px]">
{/* default theme */} {/* default theme */}
<ThemePreview <ThemePreview
name="Default" name={t("settings.appearance.themes.default")}
selector="theme-default" selector="theme-default"
active={props.active === null} active={props.active === null}
onClick={() => props.setTheme(null)} onClick={() => props.setTheme(null)}
@ -132,7 +136,7 @@ export function ThemePart(props: {
<ThemePreview <ThemePreview
selector={`theme-${v.id}`} selector={`theme-${v.id}`}
active={props.active === v.id} active={props.active === v.id}
name={v.name} name={t(v.key)}
key={v.id} key={v.id}
onClick={() => props.setTheme(v.id)} onClick={() => props.setTheme(v.id)}
/> />