mirror of
https://github.com/sussy-code/smov.git
synced 2024-12-29 16:07:40 +01:00
localize part of settings page
This commit is contained in:
parent
0ef492f58b
commit
5b71aae159
10 changed files with 195 additions and 130 deletions
|
@ -27,6 +27,10 @@
|
|||
},
|
||||
"migration": {
|
||||
"failed": "Failed to migrate your data."
|
||||
},
|
||||
"dmca": {
|
||||
"title": "",
|
||||
"text": ""
|
||||
}
|
||||
},
|
||||
"navigation": {
|
||||
|
@ -44,6 +48,79 @@
|
|||
"actions": {
|
||||
"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": {
|
||||
"tagline": "Watch your favorite shows and movies with this open source streaming app.",
|
||||
"links": {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/* eslint-disable react/no-unescaped-entities */
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { ThinContainer } from "@/components/layout/ThinContainer";
|
||||
import { Ol } from "@/components/utils/Ol";
|
||||
|
@ -16,95 +16,18 @@ function Question(props: { title: string; children: React.ReactNode }) {
|
|||
}
|
||||
|
||||
export function AboutPage() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<SubPageLayout>
|
||||
<ThinContainer>
|
||||
<Heading1>About us</Heading1>
|
||||
<Heading1>{t("faq.title")}</Heading1>
|
||||
<Ol
|
||||
items={[
|
||||
<Question title="What is Blue?">
|
||||
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>,
|
||||
<Question title={t("faq.q1.title")}>{t("faq.q1.body")}</Question>,
|
||||
]}
|
||||
/>
|
||||
<Paragraph>
|
||||
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. 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>
|
||||
<Heading2>{t("faq.how.title")}</Heading2>
|
||||
<Paragraph>{t("faq.how.body")}</Paragraph>
|
||||
</ThinContainer>
|
||||
</SubPageLayout>
|
||||
);
|
||||
|
|
|
@ -7,14 +7,15 @@ import { Heading1, Paragraph } from "@/components/utils/Text";
|
|||
|
||||
import { SubPageLayout } from "./layouts/SubPageLayout";
|
||||
|
||||
// TODO make email a constant
|
||||
export function DmcaPage() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<SubPageLayout>
|
||||
<ThinContainer>
|
||||
<Heading1>{t("dmca.title")}</Heading1>
|
||||
<Paragraph>{t("dmca.description")}</Paragraph>
|
||||
<Heading1>{t("screens.dmca.title")}</Heading1>
|
||||
<Paragraph>{t("screens.dmca.text")}</Paragraph>
|
||||
<Paragraph className="flex space-x-3 items-center">
|
||||
<Icon icon={Icons.MAIL} />
|
||||
<span>dmca@movie-web.app</span>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import classNames from "classnames";
|
||||
import { useCallback, useEffect, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAsyncFn } from "react-use";
|
||||
|
||||
import {
|
||||
|
@ -96,6 +97,7 @@ export function AccountSettings(props: {
|
|||
}
|
||||
|
||||
export function SettingsPage() {
|
||||
const { t } = useTranslation();
|
||||
const activeTheme = useThemeStore((s) => s.theme);
|
||||
const setTheme = useThemeStore((s) => s.setTheme);
|
||||
|
||||
|
@ -244,21 +246,21 @@ export function SettingsPage() {
|
|||
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">
|
||||
<Button
|
||||
className="w-full md:w-auto"
|
||||
theme="secondary"
|
||||
onClick={state.reset}
|
||||
>
|
||||
Reset
|
||||
{t("settings.reset")}
|
||||
</Button>
|
||||
<Button
|
||||
className="w-full md:w-auto"
|
||||
theme="purple"
|
||||
onClick={saveChanges}
|
||||
>
|
||||
Save
|
||||
{t("settings.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAsyncFn } from "react-use";
|
||||
|
||||
import { SessionResponse } from "@/backend/accounts/auth";
|
||||
|
@ -18,6 +19,7 @@ export function Device(props: {
|
|||
isCurrent?: boolean;
|
||||
onRemove?: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const url = useBackendUrl();
|
||||
const token = useAuthStore((s) => s.account?.token);
|
||||
const [result, exec] = useAsyncFn(async () => {
|
||||
|
@ -32,12 +34,14 @@ export function Device(props: {
|
|||
paddingClass="px-6 py-4"
|
||||
>
|
||||
<div className="font-medium">
|
||||
<SecondaryLabel>Device name</SecondaryLabel>
|
||||
<SecondaryLabel>
|
||||
{t("settings.account.devices.deviceNameLabel")}
|
||||
</SecondaryLabel>
|
||||
<p className="text-white">{props.name}</p>
|
||||
</div>
|
||||
{!props.isCurrent ? (
|
||||
<Button theme="danger" loading={result.loading} onClick={exec}>
|
||||
Remove
|
||||
{t("settings.account.devices.removeDevice")}
|
||||
</Button>
|
||||
) : null}
|
||||
</SettingsCard>
|
||||
|
@ -50,6 +54,7 @@ export function DeviceListPart(props: {
|
|||
sessions: SessionResponse[];
|
||||
onChange?: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const seed = useAuthStore((s) => s.account?.seed);
|
||||
const sessions = props.sessions;
|
||||
const currentSessionId = useAuthStore((s) => s.account?.sessionId);
|
||||
|
@ -75,10 +80,10 @@ export function DeviceListPart(props: {
|
|||
return (
|
||||
<div>
|
||||
<Heading2 border className="mt-0 mb-9">
|
||||
Devices
|
||||
{t("settings.account.devices.title")}
|
||||
</Heading2>
|
||||
{props.error ? (
|
||||
<p>Failed to load sessions</p>
|
||||
<p>{t("settings.account.devices.failed")}</p>
|
||||
) : props.loading ? (
|
||||
<Loading />
|
||||
) : (
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { FlagIcon } from "@/components/FlagIcon";
|
||||
import { Dropdown } from "@/components/form/Dropdown";
|
||||
import { Heading1 } from "@/components/utils/Text";
|
||||
|
@ -8,7 +10,8 @@ export function LocalePart(props: {
|
|||
language: string;
|
||||
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
|
||||
.sort((a, b) => sorted.indexOf(a.code) - sorted.indexOf(b.code))
|
||||
|
@ -18,14 +21,16 @@ export function LocalePart(props: {
|
|||
leftIcon: <FlagIcon countryCode={opt.code} />,
|
||||
}));
|
||||
|
||||
const selected = options.find((t) => t.id === props.language);
|
||||
const selected = options.find((item) => item.id === props.language);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Heading1 border>Locale</Heading1>
|
||||
<p className="text-white font-bold mb-3">Application language</p>
|
||||
<Heading1 border>{t("settings.locale.title")}</Heading1>
|
||||
<p className="text-white font-bold mb-3">
|
||||
{t("settings.locale.language")}
|
||||
</p>
|
||||
<p className="max-w-[20rem] font-medium">
|
||||
Language applied to the entire application.
|
||||
{t("settings.locale.languageDescription")}
|
||||
</p>
|
||||
<Dropdown
|
||||
options={options}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { Button } from "@/components/buttons/Button";
|
||||
import { ColorPicker } from "@/components/form/ColorPicker";
|
||||
import { IconPicker } from "@/components/form/IconPicker";
|
||||
|
@ -17,30 +19,34 @@ export interface ProfileEditModalProps {
|
|||
}
|
||||
|
||||
export function ProfileEditModal(props: ProfileEditModalProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Modal id={props.id}>
|
||||
<ModalCard>
|
||||
<Heading2 className="!mt-0">Edit profile picture</Heading2>
|
||||
<Heading2 className="!mt-0">
|
||||
{t("settings.account.profile.title")}
|
||||
</Heading2>
|
||||
<div className="space-y-6">
|
||||
<ColorPicker
|
||||
label="First color"
|
||||
label={t("settings.account.profile.firstColor")}
|
||||
value={props.colorA}
|
||||
onInput={props.setColorA}
|
||||
/>
|
||||
<ColorPicker
|
||||
label="Second color"
|
||||
label={t("settings.account.profile.secondColor")}
|
||||
value={props.colorB}
|
||||
onInput={props.setColorB}
|
||||
/>
|
||||
<IconPicker
|
||||
label="User icon"
|
||||
label={t("settings.account.profile.userIcon")}
|
||||
value={props.userIcon}
|
||||
onInput={props.setUserIcon}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-center mt-8">
|
||||
<Button theme="purple" className="!px-20" onClick={props.close}>
|
||||
Finish editing
|
||||
{t("settings.account.profile.finish")}
|
||||
</Button>
|
||||
</div>
|
||||
</ModalCard>
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { useTranslation } from "react-i18next";
|
||||
import { useHistory } from "react-router-dom";
|
||||
|
||||
import { Button } from "@/components/buttons/Button";
|
||||
|
@ -6,6 +7,7 @@ import { Heading3 } from "@/components/utils/Text";
|
|||
|
||||
export function RegisterCalloutPart() {
|
||||
const history = useHistory();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
@ -14,15 +16,14 @@ export function RegisterCalloutPart() {
|
|||
className="grid grid-cols-2 gap-12 mt-5"
|
||||
>
|
||||
<div>
|
||||
<Heading3>Sync to the cloud</Heading3>
|
||||
<Heading3>{t("settings.account.register.title")}</Heading3>
|
||||
<p className="text-type-text">
|
||||
Instantly share your watch progress between devices and keep them
|
||||
synced.
|
||||
{t("settings.account.register.text")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex justify-end items-center">
|
||||
<Button theme="purple" onClick={() => history.push("/register")}>
|
||||
Get started
|
||||
{t("settings.account.register.cta")}
|
||||
</Button>
|
||||
</div>
|
||||
</SolidSettingsCard>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Sticky from "react-sticky-el";
|
||||
import { useAsync } from "react-use";
|
||||
|
||||
|
@ -14,16 +15,22 @@ import { useAuthStore } from "@/stores/auth";
|
|||
const rem = 16;
|
||||
|
||||
function SecureBadge(props: { url: string }) {
|
||||
const { t } = useTranslation();
|
||||
const secure = props.url.startsWith("https://");
|
||||
return (
|
||||
<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} />
|
||||
Secure
|
||||
{t(
|
||||
secure
|
||||
? "settings.sidebar.info.secure"
|
||||
: "settings.sidebar.info.insecure"
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function SidebarPart() {
|
||||
const { t } = useTranslation();
|
||||
const { isMobile } = useIsMobile();
|
||||
const { account } = useAuthStore();
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
|
@ -31,11 +38,31 @@ export function SidebarPart() {
|
|||
const [activeLink, setActiveLink] = useState("");
|
||||
|
||||
const settingLinks = [
|
||||
{ text: "Account", id: "settings-account", icon: Icons.USER },
|
||||
{ text: "Locale", id: "settings-locale", icon: Icons.BOOKMARK },
|
||||
{ text: "Appearance", id: "settings-appearance", icon: Icons.GITHUB },
|
||||
{ text: "Captions", id: "settings-captions", icon: Icons.CAPTIONS },
|
||||
{ text: "Connections", id: "settings-connection", icon: Icons.LINK },
|
||||
{
|
||||
textKey: "settings.account.title",
|
||||
id: "settings-account",
|
||||
icon: Icons.USER,
|
||||
},
|
||||
{
|
||||
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();
|
||||
|
@ -103,24 +130,29 @@ export function SidebarPart() {
|
|||
onClick={() => scrollTo(v.id)}
|
||||
key={v.id}
|
||||
>
|
||||
{v.text}
|
||||
{t(v.textKey)}
|
||||
</SidebarLink>
|
||||
))}
|
||||
</SidebarSection>
|
||||
<Divider />
|
||||
</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">
|
||||
{/* Hostname */}
|
||||
<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>
|
||||
</div>
|
||||
|
||||
{/* Backend URL */}
|
||||
<div className="col-span-2 space-y-1">
|
||||
<p className="text-type-dimmed font-medium flex items-center">
|
||||
Backend URL
|
||||
{t("settings.sidebar.info.backendUrl")}
|
||||
<SecureBadge url={backendUrl} />
|
||||
</p>
|
||||
<p className="text-white">
|
||||
|
@ -130,13 +162,19 @@ export function SidebarPart() {
|
|||
|
||||
{/* User ID */}
|
||||
<div className="col-span-2 space-y-1">
|
||||
<p className="text-type-dimmed font-medium">User ID</p>
|
||||
<p className="text-white">{account?.userId ?? "Not logged in"}</p>
|
||||
<p className="text-type-dimmed font-medium">
|
||||
{t("settings.sidebar.info.userId")}
|
||||
</p>
|
||||
<p className="text-white">
|
||||
{account?.userId ?? t("settings.sidebar.info.notLoggedIn")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* App version */}
|
||||
<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">
|
||||
{conf().APP_VERSION}
|
||||
</p>
|
||||
|
@ -144,7 +182,9 @@ export function SidebarPart() {
|
|||
|
||||
{/* Backend version */}
|
||||
<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">
|
||||
{backendMeta.error ? (
|
||||
<Icon
|
||||
|
@ -155,7 +195,8 @@ export function SidebarPart() {
|
|||
{backendMeta.loading ? (
|
||||
<div className="h-4 w-12 bg-type-dimmed/20 rounded" />
|
||||
) : (
|
||||
backendMeta?.value?.version || "Unknown"
|
||||
backendMeta?.value?.version ||
|
||||
t("settings.sidebar.info.unknownVersion")
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import classNames from "classnames";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { Icon, Icons } from "@/components/Icon";
|
||||
import { Heading1 } from "@/components/utils/Text";
|
||||
|
@ -6,19 +7,19 @@ import { Heading1 } from "@/components/utils/Text";
|
|||
const availableThemes = [
|
||||
{
|
||||
id: "blue",
|
||||
name: "Blue",
|
||||
key: "settings.themes.blue",
|
||||
},
|
||||
{
|
||||
id: "teal",
|
||||
name: "Teal",
|
||||
key: "settings.themes.teal",
|
||||
},
|
||||
{
|
||||
id: "red",
|
||||
name: "Red",
|
||||
key: "settings.themes.red",
|
||||
},
|
||||
{
|
||||
id: "gray",
|
||||
name: "Gray",
|
||||
key: "settings.themes.gray",
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -28,6 +29,8 @@ function ThemePreview(props: {
|
|||
name: string;
|
||||
onClick?: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(props.selector, "cursor-pointer group tabbable")}
|
||||
|
@ -58,7 +61,6 @@ function ThemePreview(props: {
|
|||
)}
|
||||
/>
|
||||
{/* 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="relative w-full h-full">
|
||||
{/* Background color */}
|
||||
|
@ -106,7 +108,7 @@ function ThemePreview(props: {
|
|||
props.active ? "opacity-100" : "opacity-0 pointer-events-none"
|
||||
)}
|
||||
>
|
||||
Active
|
||||
{t("settings.appearance.activeTheme")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -117,13 +119,15 @@ export function ThemePart(props: {
|
|||
active: string | null;
|
||||
setTheme: (theme: string | null) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<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]">
|
||||
{/* default theme */}
|
||||
<ThemePreview
|
||||
name="Default"
|
||||
name={t("settings.appearance.themes.default")}
|
||||
selector="theme-default"
|
||||
active={props.active === null}
|
||||
onClick={() => props.setTheme(null)}
|
||||
|
@ -132,7 +136,7 @@ export function ThemePart(props: {
|
|||
<ThemePreview
|
||||
selector={`theme-${v.id}`}
|
||||
active={props.active === v.id}
|
||||
name={v.name}
|
||||
name={t(v.key)}
|
||||
key={v.id}
|
||||
onClick={() => props.setTheme(v.id)}
|
||||
/>
|
||||
|
|
Loading…
Reference in a new issue