feat: migrate settings screen to i18n

This commit is contained in:
reya 2024-01-29 14:57:00 +07:00
parent cfda9ba899
commit 23482531c5
18 changed files with 175 additions and 490 deletions

View File

@ -59,15 +59,6 @@ export default function Router() {
return { Component: ProfileSettingScreen };
},
},
{
path: "edit-contact",
async lazy() {
const { EditContactScreen } = await import(
"./routes/settings/editContact"
);
return { Component: EditContactScreen };
},
},
{
path: "backup",
async lazy() {

View File

@ -2,10 +2,12 @@ import { getVersion } from "@tauri-apps/api/app";
import { relaunch } from "@tauri-apps/plugin-process";
import { Update, check } from "@tauri-apps/plugin-updater";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { toast } from "sonner";
export function AboutScreen() {
const [t] = useTranslation();
const [version, setVersion] = useState("");
const [newUpdate, setNewUpdate] = useState<Update>(null);
@ -34,7 +36,7 @@ export function AboutScreen() {
<div className="flex flex-col items-center">
<h1 className="leading-tight text-xl font-semibold">Lume</h1>
<p className="text-sm font-medium text-neutral-700 dark:text-neutral-300">
Version {version}
{t("settings.about.version")} {version}
</p>
</div>
<div className="mx-auto mt-4 flex w-full max-w-xs flex-col gap-2">
@ -44,7 +46,7 @@ export function AboutScreen() {
onClick={() => checkUpdate()}
className="inline-flex h-9 w-full items-center justify-center rounded-lg bg-blue-500 text-sm font-medium text-white hover:bg-blue-600"
>
Check for update
{t("settings.about.checkUpdate")}
</button>
) : (
<button
@ -52,7 +54,7 @@ export function AboutScreen() {
onClick={() => installUpdate()}
className="inline-flex h-9 w-full items-center justify-center rounded-lg bg-blue-500 text-sm font-medium text-white hover:bg-blue-600"
>
Install {newUpdate.version}
{t("settings.about.installUpdate")} {newUpdate.version}
</button>
)}
<Link

View File

@ -1,7 +1,9 @@
import { useStorage } from "@lume/storage";
import { useTranslation } from "react-i18next";
export function AdvancedSettingScreen() {
const storage = useStorage();
const { t } = useTranslation();
const clearCache = async () => {
await storage.clearCache();
@ -13,16 +15,18 @@ export function AdvancedSettingScreen() {
<div className="flex w-full items-center justify-between">
<div className="flex items-center gap-8">
<div className="w-24 shrink-0 text-end text-sm font-semibold">
Cache
{t("settings.advanced.cache.title")}
</div>
<div className="text-sm">
{t("settings.advanced.cache.subtitle")}
</div>
<div className="text-sm">Use for boost up nostr connection</div>
</div>
<button
type="button"
onClick={() => clearCache()}
className="h-8 w-max rounded-lg px-3 text-sm font-semibold text-blue-500 bg-blue-100 hover:bg-blue-200"
>
Clear
{t("settings.advanced.cache.button")}
</button>
</div>
</div>

View File

@ -3,11 +3,13 @@ import { EyeOffIcon } from "@lume/icons";
import { useStorage } from "@lume/storage";
import { nip19 } from "nostr-tools";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
export function BackupSettingScreen() {
const ark = useArk();
const storage = useStorage();
const [t] = useTranslation();
const [privkey, setPrivkey] = useState(null);
const [showPassword, setShowPassword] = useState(false);
@ -29,7 +31,9 @@ export function BackupSettingScreen() {
<div>
{privkey ? (
<div>
<div className="mb-2 text-sm font-semibold">Private key</div>
<div className="mb-2 text-sm font-semibold">
{t("settings.backup.privkey.title")}
</div>
<div className="relative">
<input
readOnly
@ -50,7 +54,7 @@ export function BackupSettingScreen() {
onClick={() => removePrivkey()}
className="mt-2 inline-flex h-11 w-full items-center justify-center gap-2 rounded-lg bg-red-200 dark:bg-red-800 px-6 font-medium text-red-500 hover:bg-red-500 hover:text-white focus:outline-none dark:hover:text-white"
>
Remove private key
{t("settings.backup.privkey.button")}
</button>
</div>
) : null}

View File

@ -1,10 +1,13 @@
import { useArk } from "@lume/ark";
import { LoaderIcon } from "@lume/icons";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
export function AvatarUpload({ setPicture }) {
const ark = useArk();
const [t] = useTranslation();
const [loading, setLoading] = useState(false);
const upload = async () => {
@ -36,7 +39,7 @@ export function AvatarUpload({ setPicture }) {
{loading ? (
<LoaderIcon className="size-4 animate-spin" />
) : (
"Change avatar"
t("user.avatarButton")
)}
</button>
);

View File

@ -1,45 +0,0 @@
import { useArk } from "@lume/ark";
import { EditIcon, LoaderIcon } from "@lume/icons";
import { compactNumber } from "@lume/utils";
import { useQuery } from "@tanstack/react-query";
import { Link } from "react-router-dom";
export function ContactCard() {
const ark = useArk();
const { status, data } = useQuery({
queryKey: ["contacts"],
queryFn: async () => {
const contacts = await ark.getUserContacts();
return contacts;
},
refetchOnWindowFocus: false,
});
return (
<div className="col-span-1 h-44 rounded-2xl bg-neutral-100 transition-all duration-150 ease-smooth hover:scale-105 dark:bg-neutral-900">
{status === "pending" ? (
<div className="flex h-full w-full items-center justify-center">
<LoaderIcon className="h-4 w-4 animate-spin" />
</div>
) : (
<div className="flex h-full w-full flex-col justify-between p-4">
<h3 className="pt-1 text-5xl font-semibold tabular-nums text-neutral-900 dark:text-neutral-100">
{compactNumber.format(data.length)}
</h3>
<div className="mt-auto flex h-6 w-full items-center justify-between">
<p className="text-xl font-medium leading-none text-neutral-600 dark:text-neutral-400">
Contacts
</p>
<Link
to="/settings/edit-contact"
className="inline-flex h-6 w-max items-center gap-1 rounded-full bg-neutral-200 px-2.5 text-sm font-medium hover:bg-neutral-300 dark:bg-neutral-800 dark:hover:bg-neutral-700"
>
<EditIcon className="h-3 w-3" />
Edit
</Link>
</div>
</div>
)}
</div>
);
}

View File

@ -1,10 +1,13 @@
import { useArk } from "@lume/ark";
import { LoaderIcon } from "@lume/icons";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
export function CoverUpload({ setBanner }) {
const ark = useArk();
const [t] = useTranslation();
const [loading, setLoading] = useState(false);
const upload = async () => {
@ -36,7 +39,7 @@ export function CoverUpload({ setBanner }) {
{loading ? (
<LoaderIcon className="size-4 animate-spin" />
) : (
"Change cover"
t("user.coverButton")
)}
</button>
);

View File

@ -1,60 +0,0 @@
import { useArk } from "@lume/ark";
import { LoaderIcon } from "@lume/icons";
import { compactNumber } from "@lume/utils";
import { useQuery } from "@tanstack/react-query";
import { fetch } from "@tauri-apps/plugin-http";
import { Link } from "react-router-dom";
export function PostCard() {
const ark = useArk();
const { status, data } = useQuery({
queryKey: ["user-stats", ark.account.pubkey],
queryFn: async ({ signal }: { signal: AbortSignal }) => {
const res = await fetch(
`https://api.nostr.band/v0/stats/profile/${ark.account.pubkey}`,
{
signal,
},
);
if (!res.ok) {
throw new Error("Error");
}
return await res.json();
},
refetchOnWindowFocus: false,
refetchOnMount: false,
refetchOnReconnect: false,
staleTime: Infinity,
});
return (
<div className="col-span-1 h-44 rounded-2xl bg-neutral-100 transition-all duration-150 ease-smooth hover:scale-105 dark:bg-neutral-900">
{status === "pending" ? (
<div className="flex h-full w-full items-center justify-center">
<LoaderIcon className="h-4 w-4 animate-spin" />
</div>
) : (
<div className="flex h-full w-full flex-col justify-between p-4">
<h3 className="pt-1 text-5xl font-semibold tabular-nums text-neutral-900 dark:text-neutral-100">
{compactNumber.format(
data.stats[ark.account.pubkey].pub_note_count,
)}
</h3>
<div className="mt-auto flex h-6 w-full items-center justify-between">
<p className="text-xl font-medium leading-none text-neutral-600 dark:text-neutral-400">
Posts
</p>
<Link
to={`/users/${ark.account.pubkey}`}
className="inline-flex h-6 w-max items-center gap-1 rounded-full bg-neutral-200 px-2.5 text-sm font-medium hover:bg-neutral-300 dark:bg-neutral-800 dark:hover:bg-neutral-700"
>
View
</Link>
</div>
</div>
)}
</div>
);
}

View File

@ -1,77 +0,0 @@
import { useArk, useProfile } from "@lume/ark";
import { EditIcon, LoaderIcon } from "@lume/icons";
import { displayNpub } from "@lume/utils";
import * as Avatar from "@radix-ui/react-avatar";
import { writeText } from "@tauri-apps/plugin-clipboard-manager";
import { minidenticon } from "minidenticons";
import { nip19 } from "nostr-tools";
import { Link } from "react-router-dom";
export function ProfileCard() {
const ark = useArk();
const svgURI = `data:image/svg+xml;utf8,${encodeURIComponent(
minidenticon(ark.account.pubkey, 90, 50),
)}`;
const { isLoading, user } = useProfile(ark.account.pubkey);
const copyNpub = async () => {
return await writeText(nip19.npubEncode(ark.account.pubkey));
};
return (
<div className="mb-4 h-56 w-full rounded-2xl bg-neutral-100 transition-all duration-150 ease-smooth hover:scale-105 dark:bg-neutral-900">
{isLoading ? (
<div className="flex h-full w-full items-center justify-center">
<LoaderIcon className="h-4 w-4 animate-spin" />
</div>
) : (
<div className="flex h-full w-full flex-col justify-between p-4">
<div className="flex h-10 w-full justify-end gap-3">
<button
type="button"
onClick={copyNpub}
className="inline-flex h-8 w-28 transform items-center justify-center gap-1.5 rounded-full bg-neutral-200 text-sm font-medium hover:bg-neutral-400 active:translate-y-1 dark:bg-neutral-800 dark:hover:bg-neutral-600"
>
Copy NPUB
</button>
<Link
to="/settings/edit-profile"
className="inline-flex h-8 w-20 items-center justify-center gap-1.5 rounded-full bg-neutral-200 text-sm font-medium hover:bg-neutral-400 dark:bg-neutral-800 dark:hover:bg-neutral-600"
>
<EditIcon className="h-4 w-4" />
Edit
</Link>
</div>
<div className="flex flex-col gap-2.5">
<Avatar.Root className="shrink-0">
<Avatar.Image
src={user?.picture || user?.image}
alt={ark.account.pubkey}
loading="lazy"
decoding="async"
style={{ contentVisibility: "auto" }}
className="h-16 w-16 rounded-xl border border-neutral-200/50 shadow-[rgba(17,_17,_26,_0.1)_0px_0px_16px] dark:border-neutral-800/50"
/>
<Avatar.Fallback delayMs={300}>
<img
src={svgURI}
alt={ark.account.pubkey}
className="h-16 w-16 rounded-xl bg-black dark:bg-white"
/>
</Avatar.Fallback>
</Avatar.Root>
<div>
<h3 className="text-3xl font-semibold leading-8 text-neutral-900 dark:text-neutral-100">
{user?.display_name || user?.name}
</h3>
<p className="text-lg text-neutral-700 dark:text-neutral-300">
{user?.nip05 || displayNpub(ark.account.pubkey, 16)}
</p>
</div>
</div>
</div>
)}
</div>
);
}

View File

@ -1,46 +0,0 @@
import { useArk } from "@lume/ark";
import { EditIcon, LoaderIcon } from "@lume/icons";
import { compactNumber } from "@lume/utils";
import { useQuery } from "@tanstack/react-query";
import { Link } from "react-router-dom";
export function RelayCard() {
const ark = useArk();
const { status, data } = useQuery({
queryKey: ["relays", ark.account.pubkey],
queryFn: async () => {
const relays = await ark.getUserRelays({});
return relays;
},
refetchOnWindowFocus: false,
});
return (
<div className="col-span-1 h-44 rounded-2xl bg-neutral-100 transition-all duration-150 ease-smooth hover:scale-105 dark:bg-neutral-900">
{status === "pending" ? (
<div className="flex h-full w-full items-center justify-center">
<LoaderIcon className="h-4 w-4 animate-spin" />
</div>
) : (
<div className="flex h-full w-full flex-col justify-between p-4">
<h3 className="pt-1 text-5xl font-semibold tabular-nums text-neutral-900 dark:text-neutral-100">
{compactNumber.format(data?.relays?.length || 0)}
</h3>
<div className="mt-auto flex h-6 w-full items-center justify-between">
<p className="text-xl font-medium leading-none text-neutral-600 dark:text-neutral-400">
Relays
</p>
<Link
to="/relays"
className="inline-flex h-6 w-max items-center gap-1 rounded-full bg-neutral-200 px-2.5 text-sm font-medium hover:bg-neutral-300 dark:bg-neutral-800 dark:hover:bg-neutral-700"
>
<EditIcon className="h-3 w-3" />
Edit
</Link>
</div>
</div>
)}
</div>
);
}

View File

@ -1,62 +0,0 @@
import { useArk } from "@lume/ark";
import { LoaderIcon } from "@lume/icons";
import { useStorage } from "@lume/storage";
import { useState } from "react";
import { toast } from "sonner";
export function NWCForm({ setWalletConnectURL }) {
const ark = useArk();
const storage = useStorage();
const [uri, setUri] = useState("");
const [loading, setLoading] = useState(false);
const submit = async () => {
try {
setLoading(true);
if (!uri.startsWith("nostr+walletconnect:")) {
toast.error(
"Connect URI is required and must start with format nostr+walletconnect:, please check again",
);
setLoading(false);
return;
}
const uriObj = new URL(uri);
const params = new URLSearchParams(uriObj.search);
if (params.has("relay") && params.has("secret")) {
await storage.createPrivkey(`${ark.account.pubkey}-nwc`, uri);
setWalletConnectURL(uri);
setLoading(false);
} else {
setLoading(false);
toast.error("Connect URI is not valid, please check again");
return;
}
} catch (e) {
setLoading(false);
toast.error(String(e));
}
};
return (
<div className="flex flex-col gap-3 rounded-xl bg-neutral-100 p-3 dark:bg-neutral-900">
<textarea
name="walletConnectURL"
value={uri}
onChange={(e) => setUri(e.target.value)}
placeholder="nostr+walletconnect://"
className="h-40 w-full resize-none rounded-lg border-transparent bg-neutral-200 px-3 py-3 text-neutral-900 !outline-none placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:focus:ring-blue-800 dark:bg-neutral-800 dark:text-neutral-100 dark:placeholder:text-neutral-400"
/>
<button
type="button"
onClick={submit}
className="inline-flex h-11 w-full items-center justify-center rounded-lg bg-blue-500 font-medium text-white hover:bg-blue-600"
>
{loading ? <LoaderIcon className="h-4 w-4 animate-spin" /> : "Connect"}
</button>
</div>
);
}

View File

@ -1,51 +0,0 @@
import { useArk } from "@lume/ark";
import { LoaderIcon } from "@lume/icons";
import { compactNumber } from "@lume/utils";
import { useQuery } from "@tanstack/react-query";
import { fetch } from "@tauri-apps/plugin-http";
export function ZapCard() {
const ark = useArk();
const { status, data } = useQuery({
queryKey: ["user-stats", ark.account.pubkey],
queryFn: async ({ signal }: { signal: AbortSignal }) => {
const res = await fetch(
`https://api.nostr.band/v0/stats/profile/${ark.account.pubkey}`,
{
signal,
},
);
if (!res.ok) {
throw new Error("Error");
}
return await res.json();
},
refetchOnWindowFocus: false,
refetchOnMount: false,
refetchOnReconnect: false,
staleTime: Infinity,
});
return (
<div className="col-span-1 h-44 rounded-2xl bg-neutral-100 transition-all duration-150 ease-smooth hover:scale-105 dark:bg-neutral-900">
{status === "pending" ? (
<div className="flex h-full w-full items-center justify-center">
<LoaderIcon className="h-4 w-4 animate-spin" />
</div>
) : (
<div className="flex h-full w-full flex-col justify-between p-4">
<h3 className="pt-1 text-5xl font-semibold tabular-nums text-neutral-900 dark:text-neutral-100">
{compactNumber.format(
data?.stats[ark.account.pubkey]?.zaps_received?.msats / 1000 || 0,
)}
</h3>
<div className="mt-auto flex h-6 items-center text-xl font-medium leading-none text-neutral-600 dark:text-neutral-400">
Sats received
</div>
</div>
)}
</div>
);
}

View File

@ -1,34 +0,0 @@
import { useArk } from "@lume/ark";
import { LoaderIcon } from "@lume/icons";
import { User } from "@lume/ui";
import { useQuery } from "@tanstack/react-query";
export function EditContactScreen() {
const ark = useArk();
const { status, data } = useQuery({
queryKey: ["contacts"],
queryFn: async () => {
return await ark.getUserContacts();
},
refetchOnWindowFocus: false,
});
return (
<div className="mx-auto flex w-full max-w-xl flex-col gap-3">
{status === "pending" ? (
<div className="flex h-10 w-full items-center justify-center">
<LoaderIcon className="h-4 w-4 animate-spin" />
</div>
) : (
data.map((item) => (
<div
key={item}
className="flex h-16 w-full items-center justify-between rounded-xl bg-neutral-100 px-2.5 dark:bg-neutral-900"
>
<User pubkey={item} variant="simple" />
</div>
))
)}
</div>
);
}

View File

@ -10,20 +10,17 @@ import {
requestPermission,
} from "@tauri-apps/plugin-notification";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
export function GeneralSettingScreen() {
const storage = useStorage();
const [t] = useTranslation();
const [apiKey, setAPIKey] = useState("");
const [settings, setSettings] = useState({
lowPower: false,
autoupdate: false,
...storage.settings,
notification: false,
autolaunch: false,
outbox: false,
media: true,
hashtag: true,
notification: true,
translation: false,
appearance: "system",
});
@ -100,47 +97,6 @@ export function GeneralSettingScreen() {
const permissionGranted = await isPermissionGranted();
setSettings((prev) => ({ ...prev, notification: permissionGranted }));
const data = await storage.getAllSettings();
if (!data) return;
for (const item of data) {
if (item.key === "autoupdate")
setSettings((prev) => ({
...prev,
autoupdate: !!parseInt(item.value),
}));
if (item.key === "lowPower")
setSettings((prev) => ({
...prev,
lowPower: !!parseInt(item.value),
}));
if (item.key === "outbox")
setSettings((prev) => ({
...prev,
outbox: !!parseInt(item.value),
}));
if (item.key === "media")
setSettings((prev) => ({
...prev,
media: !!parseInt(item.value),
}));
if (item.key === "hashtag")
setSettings((prev) => ({
...prev,
hashtag: !!parseInt(item.value),
}));
if (item.key === "translation")
setSettings((prev) => ({
...prev,
translation: !!parseInt(item.value),
}));
}
}
loadSettings();
@ -152,9 +108,11 @@ export function GeneralSettingScreen() {
<div className="flex w-full items-center justify-between">
<div className="flex items-center gap-8">
<div className="w-36 shrink-0 text-end text-sm font-semibold">
Update
{t("settings.general.update.title")}
</div>
<div className="text-sm">
{t("settings.general.update.subtitle")}
</div>
<div className="text-sm">Automatically download new update</div>
</div>
<Switch.Root
checked={settings.autoupdate}
@ -167,10 +125,10 @@ export function GeneralSettingScreen() {
<div className="flex w-full items-center justify-between">
<div className="flex items-center gap-8">
<div className="w-36 shrink-0 text-end text-sm font-semibold">
Low Power
{t("settings.general.lowPower.title")}
</div>
<div className="text-sm">
Sustainable for low network environment.
{t("settings.general.lowPower.subtitle")}
</div>
</div>
<Switch.Root
@ -184,9 +142,11 @@ export function GeneralSettingScreen() {
<div className="flex w-full items-center justify-between">
<div className="flex items-center gap-8">
<div className="w-36 shrink-0 text-end text-sm font-semibold">
Startup
{t("settings.general.startup.title")}
</div>
<div className="text-sm">
{t("settings.general.startup.subtitle")}
</div>
<div className="text-sm">Launch Lume at Login</div>
</div>
<Switch.Root
checked={settings.autolaunch}
@ -199,9 +159,11 @@ export function GeneralSettingScreen() {
<div className="flex w-full items-center justify-between">
<div className="flex items-center gap-8">
<div className="w-36 shrink-0 text-end text-sm font-semibold">
Media
{t("settings.general.media.title")}
</div>
<div className="text-sm">
{t("settings.general.media.subtitle")}
</div>
<div className="text-sm">Automatically load media</div>
</div>
<Switch.Root
checked={settings.media}
@ -214,9 +176,11 @@ export function GeneralSettingScreen() {
<div className="flex w-full items-center justify-between">
<div className="flex items-center gap-8">
<div className="w-36 shrink-0 text-end text-sm font-semibold">
Hashtag
{t("settings.general.hashtag.title")}
</div>
<div className="text-sm">
{t("settings.general.hashtag.subtitle")}
</div>
<div className="text-sm">Show all hashtags in content</div>
</div>
<Switch.Root
checked={settings.hashtag}
@ -229,9 +193,11 @@ export function GeneralSettingScreen() {
<div className="flex w-full items-center justify-between">
<div className="flex items-center gap-8">
<div className="w-36 shrink-0 text-end text-sm font-semibold">
Notification
{t("settings.general.notification.title")}
</div>
<div className="text-sm">
{t("settings.general.notification.subtitle")}
</div>
<div className="text-sm">Automatically send notification</div>
</div>
<Switch.Root
checked={settings.notification}
@ -245,9 +211,11 @@ export function GeneralSettingScreen() {
<div className="flex w-full items-center justify-between">
<div className="flex items-center gap-8">
<div className="w-36 shrink-0 text-end text-sm font-semibold">
Translation
{t("settings.general.translation.title")}
</div>
<div className="text-sm">
{t("settings.general.translation.subtitle")}
</div>
<div className="text-sm">Translate text to your language</div>
</div>
<Switch.Root
checked={settings.translation}
@ -260,7 +228,7 @@ export function GeneralSettingScreen() {
{settings.translation ? (
<div className="flex w-full items-center gap-8">
<div className="w-36 shrink-0 text-end text-sm font-semibold">
API Key
{t("global.apiKey")}
</div>
<div className="relative w-full">
<input
@ -276,7 +244,7 @@ export function GeneralSettingScreen() {
onClick={saveApi}
className="mr-1 h-7 w-16 text-sm font-medium shrink-0 inline-flex items-center justify-center rounded-md bg-neutral-200 dark:bg-neutral-800 hover:bg-neutral-300 dark:hover:bg-neutral-700"
>
Save
{t("global.save")}
</button>
</div>
</div>
@ -284,7 +252,7 @@ export function GeneralSettingScreen() {
) : null}
<div className="flex w-full items-start gap-8">
<div className="w-36 shrink-0 text-end text-sm font-semibold">
Appearance
{t("settings.general.appearance.title")}
</div>
<div className="flex flex-1 gap-6">
<button
@ -303,7 +271,7 @@ export function GeneralSettingScreen() {
<LightIcon className="h-5 w-5" />
</div>
<p className="text-sm font-medium text-neutral-700 dark:text-neutral-300">
Light
{t("settings.general.appearance.light")}
</p>
</button>
<button
@ -322,7 +290,7 @@ export function GeneralSettingScreen() {
<DarkIcon className="h-5 w-5" />
</div>
<p className="text-sm font-medium text-neutral-700 dark:text-neutral-300">
Dark
{t("settings.general.appearance.dark")}
</p>
</button>
<button
@ -341,7 +309,7 @@ export function GeneralSettingScreen() {
<SystemModeIcon className="h-5 w-5" />
</div>
<p className="text-sm font-medium text-neutral-700 dark:text-neutral-300">
System
{t("settings.general.appearance.system")}
</p>
</button>
</div>

View File

@ -2,12 +2,14 @@ import { useArk } from "@lume/ark";
import { useStorage } from "@lume/storage";
import * as Switch from "@radix-ui/react-switch";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
export function NWCScreen() {
const ark = useArk();
const storage = useStorage();
const [t] = useTranslation();
const [settings, setSettings] = useState({
nwc: false,
instantZap: storage.settings.instantZap,
@ -74,7 +76,7 @@ export function NWCScreen() {
<div className="flex w-full items-center justify-between">
<div className="flex w-full items-start gap-8">
<div className="w-36 shrink-0 text-end text-sm font-semibold">
Connection String
{t("settings.zap.nwc")}
</div>
<div className="flex flex-col items-end gap-2 w-full">
<textarea
@ -89,7 +91,7 @@ export function NWCScreen() {
onClick={saveNWC}
className="h-8 w-16 text-sm font-medium shrink-0 inline-flex items-center justify-center rounded-md bg-neutral-200 dark:bg-neutral-800 hover:bg-neutral-300 dark:hover:bg-neutral-700"
>
Save
{t("global.save")}
</button>
) : (
<button
@ -97,7 +99,7 @@ export function NWCScreen() {
onClick={remove}
className="h-8 w-16 text-sm font-medium shrink-0 inline-flex items-center justify-center rounded-md bg-neutral-200 dark:bg-neutral-800 hover:bg-neutral-300 dark:hover:bg-neutral-700"
>
Remove
{t("global.delete")}
</button>
)}
</div>
@ -108,10 +110,10 @@ export function NWCScreen() {
<div className="flex w-full items-center justify-between">
<div className="flex items-center gap-8">
<div className="w-36 shrink-0 text-end text-sm font-semibold">
Instant Zap
{t("settings.zap.instant.title")}
</div>
<div className="text-sm">
Zap with default amount, no confirmation
{t("settings.zap.instant.subtitle")}
</div>
</div>
<Switch.Root
@ -125,7 +127,7 @@ export function NWCScreen() {
<div className="flex w-full items-center justify-between">
<div className="flex w-full items-center gap-8">
<div className="w-36 shrink-0 text-end text-sm font-semibold">
Default amount
{t("settings.zap.defaultAmount")}
</div>
<div className="relative w-full">
<input
@ -141,7 +143,7 @@ export function NWCScreen() {
onClick={saveAmount}
className="mr-1 h-7 w-16 text-sm font-medium shrink-0 inline-flex items-center justify-center rounded-md bg-neutral-200 dark:bg-neutral-800 hover:bg-neutral-300 dark:hover:bg-neutral-700"
>
Save
{t("global.save")}
</button>
</div>
</div>

View File

@ -1,15 +1,11 @@
import { useArk } from "@lume/ark";
import {
CheckCircleIcon,
LoaderIcon,
PlusIcon,
UnverifiedIcon,
} from "@lume/icons";
import { CheckCircleIcon, LoaderIcon, UnverifiedIcon } from "@lume/icons";
import { useStorage } from "@lume/storage";
import { NDKKind, NDKUserProfile } from "@nostr-dev-kit/ndk";
import { useQueryClient } from "@tanstack/react-query";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { AvatarUpload } from "./components/avatarUpload";
import { CoverUpload } from "./components/coverUpload";
@ -24,6 +20,7 @@ export function ProfileSettingScreen() {
const [banner, setBanner] = useState("");
const [nip05, setNIP05] = useState({ verified: true, text: "" });
const { t } = useTranslation();
const {
register,
handleSubmit,
@ -139,7 +136,7 @@ export function ProfileSettingScreen() {
htmlFor="displayName"
className="text-sm font-semibold uppercase tracking-wider"
>
Display Name
{t("user.displayName")}
</label>
<input
type={"text"}
@ -153,7 +150,7 @@ export function ProfileSettingScreen() {
htmlFor="name"
className="text-sm font-semibold uppercase tracking-wider"
>
Name
{t("user.name")}
</label>
<input
type={"text"}
@ -179,12 +176,12 @@ export function ProfileSettingScreen() {
{nip05.verified ? (
<span className="inline-flex h-6 items-center gap-1 rounded-full bg-teal-500 px-1 pr-1.5 text-xs font-medium text-white">
<CheckCircleIcon className="h-4 w-4" />
Verified
{t("user.verified")}
</span>
) : (
<span className="inline-flex h-6 items-center gap-1 rounded bg-red-500 pl-1 pr-1.5 text-xs font-medium text-white">
<UnverifiedIcon className="h-4 w-4" />
Unverified
{t("user.unverified")}
</span>
)}
</div>
@ -200,7 +197,7 @@ export function ProfileSettingScreen() {
htmlFor="website"
className="text-sm font-semibold uppercase tracking-wider"
>
Website
{t("user.website")}
</label>
<input
type={"text"}
@ -214,7 +211,7 @@ export function ProfileSettingScreen() {
htmlFor="website"
className="text-sm font-semibold uppercase tracking-wider"
>
Lightning address
{t("user.lna")}
</label>
<input
type={"text"}
@ -228,7 +225,7 @@ export function ProfileSettingScreen() {
htmlFor="about"
className="text-sm font-semibold uppercase tracking-wider"
>
Bio
{t("user.bio")}
</label>
<textarea
{...register("about")}
@ -245,7 +242,7 @@ export function ProfileSettingScreen() {
{loading ? (
<LoaderIcon className="size-4 animate-spin" />
) : (
"Update"
t("global.update")
)}
</button>
</div>

View File

@ -7,9 +7,12 @@ import {
ZapIcon,
} from "@lume/icons";
import { cn } from "@lume/utils";
import { useTranslation } from "react-i18next";
import { NavLink, Outlet } from "react-router-dom";
export function SettingsLayout() {
const { t } = useTranslation();
return (
<div className="flex h-full min-h-0 w-full flex-col rounded-xl overflow-y-auto">
<div className="flex h-24 shrink-0 w-full items-center justify-center px-2 bg-white/50 backdrop-blur-xl dark:bg-black/50">
@ -27,7 +30,7 @@ export function SettingsLayout() {
}
>
<SettingsIcon className="size-6" />
<p className="text-sm font-medium">General</p>
<p className="text-sm font-medium">{t("settings.general.title")}</p>
</NavLink>
<NavLink
to="/settings/profile"
@ -42,7 +45,7 @@ export function SettingsLayout() {
}
>
<UserIcon className="size-6" />
<p className="text-sm font-medium">User</p>
<p className="text-sm font-medium">{t("settings.general.user")}</p>
</NavLink>
<NavLink
to="/settings/nwc"
@ -56,7 +59,7 @@ export function SettingsLayout() {
}
>
<ZapIcon className="size-6" />
<p className="text-sm font-medium">Zap</p>
<p className="text-sm font-medium">{t("settings.zap.title")}</p>
</NavLink>
<NavLink
to="/settings/backup"
@ -70,7 +73,7 @@ export function SettingsLayout() {
}
>
<SecureIcon className="size-6" />
<p className="text-sm font-medium">Backup</p>
<p className="text-sm font-medium">{t("settings.backup.title")}</p>
</NavLink>
<NavLink
to="/settings/advanced"
@ -84,7 +87,9 @@ export function SettingsLayout() {
}
>
<AdvancedSettingsIcon className="size-6" />
<p className="text-sm font-medium">Advanced</p>
<p className="text-sm font-medium">
{t("settings.advanced.title")}
</p>
</NavLink>
<NavLink
to="/settings/about"
@ -98,7 +103,7 @@ export function SettingsLayout() {
}
>
<InfoIcon className="size-6" />
<p className="text-sm font-medium">About</p>
<p className="text-sm font-medium">{t("settings.about.title")}</p>
</NavLink>
</div>
</div>

View File

@ -14,9 +14,11 @@
"cancel": "Cancel",
"save": "Save",
"post": "Post",
"update": "Update",
"noResult": "No results found.",
"emptyFeedTitle": "This feed is empty",
"emptyFeedSubtitle": "You can follow more users to build up your timeline"
"emptyFeedSubtitle": "You can follow more users to build up your timeline",
"apiKey": "API Key"
},
"nip89": {
"unsupported": "Lume isn't support this event",
@ -63,6 +65,13 @@
}
},
"user": {
"displayName": "Display Name",
"name": "Name",
"bio": "Bio",
"lna": "Lightning address",
"website": "Website",
"verified": "Verified",
"unverified": "Unverified",
"follow": "Follow",
"unfollow": "Unfollow",
"latestPosts": "Latest posts",
@ -177,5 +186,77 @@
"edit": "Edit Interest",
"followAll": "Follow All",
"unfollowAll": "Unfollow All"
},
"settings": {
"general": {
"title": "General",
"update": {
"title": "Update",
"subtitle": "Automatically download new update"
},
"lowPower": {
"title": "Low Power",
"subtitle": "Sustainable for low network environment"
},
"startup": {
"title": "Startup",
"subtitle": "Launch Lume at Login"
},
"media": {
"title": "Media",
"subtitle": "Automatically load media"
},
"hashtag": {
"title": "Hashtag",
"subtitle": "Show all hashtags in content"
},
"notification": {
"title": "Notification",
"subtitle": "Automatically send notification"
},
"translation": {
"title": "Translation",
"subtitle": "Translate text to your language"
},
"appearance": {
"title": "Appearance",
"light": "Light",
"dark": "Dark",
"system": "System"
}
},
"user": {
"title": "User"
},
"zap": {
"title": "Zap",
"nwc": "Connection String"
},
"backup": {
"title": "Backup",
"privkey": {
"title": "Private key",
"button": "Remove private key"
}
},
"advanced": {
"title": "Advanced",
"cache": {
"title": "Cache",
"subtitle": "Use for boost up nostr connection",
"button": "Clear"
},
"instant": {
"title": "Instant Zap",
"subtitle": "Zap with default amount, no confirmation"
},
"defaultAmount": "Default amount"
},
"about": {
"title": "About",
"version": "Version",
"checkUpdate": "Check for update",
"installUpdate": "Install"
}
}
}