mirror of
https://github.com/lumehq/lume.git
synced 2025-03-26 01:31:48 +01:00
feat: migrate ui components to i18n
This commit is contained in:
parent
698bd78684
commit
cfda9ba899
@ -3,12 +3,11 @@ import {
|
||||
MoveLeftIcon,
|
||||
MoveRightIcon,
|
||||
RefreshIcon,
|
||||
ThreadIcon,
|
||||
TrashIcon,
|
||||
} from "@lume/icons";
|
||||
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { ReactNode } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { InterestModal } from "./interestModal";
|
||||
import { useColumnContext } from "./provider";
|
||||
|
||||
@ -16,14 +15,14 @@ export function ColumnHeader({
|
||||
id,
|
||||
title,
|
||||
queryKey,
|
||||
icon,
|
||||
}: {
|
||||
id: number;
|
||||
title: string;
|
||||
queryKey?: string[];
|
||||
icon?: ReactNode;
|
||||
}) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { moveColumn, removeColumn } = useColumnContext();
|
||||
|
||||
const refresh = async () => {
|
||||
@ -63,7 +62,7 @@ export function ColumnHeader({
|
||||
className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
|
||||
>
|
||||
<RefreshIcon className="size-4" />
|
||||
Refresh
|
||||
{t("global.refresh")}
|
||||
</button>
|
||||
</DropdownMenu.Item>
|
||||
{queryKey?.[0] === "foryou-9998" ? (
|
||||
@ -81,7 +80,7 @@ export function ColumnHeader({
|
||||
className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
|
||||
>
|
||||
<MoveLeftIcon className="size-4" />
|
||||
Move left
|
||||
{t("global.moveLeft")}
|
||||
</button>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item asChild>
|
||||
@ -91,7 +90,7 @@ export function ColumnHeader({
|
||||
className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
|
||||
>
|
||||
<MoveRightIcon className="size-4" />
|
||||
Move right
|
||||
{t("global.moveRight")}
|
||||
</button>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Separator className="h-px my-1 bg-black/10 dark:bg-white/10" />
|
||||
@ -102,7 +101,7 @@ export function ColumnHeader({
|
||||
className="inline-flex items-center gap-3 px-3 text-sm font-medium text-red-500 rounded-lg h-9 hover:bg-red-500 hover:text-red-50 focus:outline-none"
|
||||
>
|
||||
<TrashIcon className="size-4" />
|
||||
Delete
|
||||
{t("global.Delete")}
|
||||
</button>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
|
@ -4,6 +4,7 @@ import { TOPICS, cn } from "@lume/utils";
|
||||
import * as Dialog from "@radix-ui/react-dialog";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { ReactNode, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export function InterestModal({
|
||||
@ -14,6 +15,7 @@ export function InterestModal({
|
||||
const storage = useStorage();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [t] = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [hashtags, setHashtags] = useState(storage.interests?.hashtags || []);
|
||||
@ -65,7 +67,7 @@ export function InterestModal({
|
||||
) : (
|
||||
<>
|
||||
<EditInterestIcon className="size-4" />
|
||||
Edit interest
|
||||
{t("interests.edit")}
|
||||
</>
|
||||
)}
|
||||
</Dialog.Trigger>
|
||||
@ -80,7 +82,7 @@ export function InterestModal({
|
||||
<div className="w-full h-full flex flex-col">
|
||||
<div className="h-16 shrink-0 px-8 border-b border-neutral-100 dark:border-neutral-900 flex w-full items-center justify-between">
|
||||
<div className="flex flex-col">
|
||||
<h3 className="font-semibold">Edit Interest</h3>
|
||||
<h3 className="font-semibold">{t("interests.edit")}</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full flex-1 min-h-0 flex flex-col justify-between">
|
||||
@ -104,7 +106,7 @@ export function InterestModal({
|
||||
onClick={() => toggleAll(topic.content)}
|
||||
className="text-sm font-medium text-blue-500"
|
||||
>
|
||||
Follow All
|
||||
{t("interests.followAll")}
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
@ -131,7 +133,7 @@ export function InterestModal({
|
||||
<div className="h-16 shrink-0 w-full flex items-center px-8 justify-center gap-2 border-t border-neutral-100 dark:border-neutral-900 bg-neutral-50 dark:bg-neutral-950">
|
||||
<Dialog.Close className="inline-flex h-9 flex-1 gap-2 shrink-0 items-center justify-center rounded-lg bg-neutral-100 font-medium dark:bg-neutral-900 dark:hover:bg-neutral-800 hover:bg-blue-200">
|
||||
<ArrowLeftIcon className="size-4" />
|
||||
Cancel
|
||||
{t("global.cancel")}
|
||||
</Dialog.Close>
|
||||
<button
|
||||
type="button"
|
||||
@ -141,7 +143,7 @@ export function InterestModal({
|
||||
{loading ? (
|
||||
<LoaderIcon className="size-4 animate-spin" />
|
||||
) : (
|
||||
"Save"
|
||||
t("global.save")
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { LoaderIcon } from "@lume/icons";
|
||||
import { cn } from "@lume/utils";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useArk } from "../../hooks/useArk";
|
||||
|
||||
export function UserFollowButton({
|
||||
@ -9,6 +10,7 @@ export function UserFollowButton({
|
||||
}: { target: string; className?: string }) {
|
||||
const ark = useArk();
|
||||
|
||||
const [t] = useTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [followed, setFollowed] = useState(false);
|
||||
|
||||
@ -43,14 +45,14 @@ export function UserFollowButton({
|
||||
type="button"
|
||||
disabled={loading}
|
||||
onClick={toggleFollow}
|
||||
className={cn("", className)}
|
||||
className={cn("w-max", className)}
|
||||
>
|
||||
{loading ? (
|
||||
<LoaderIcon className="size-4 animate-spin" />
|
||||
) : followed ? (
|
||||
"Unfollow"
|
||||
t("user.unfollow")
|
||||
) : (
|
||||
"Follow"
|
||||
t("user.follow")
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
|
@ -5,6 +5,7 @@ import * as Avatar from "@radix-ui/react-avatar";
|
||||
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
|
||||
import { minidenticon } from "minidenticons";
|
||||
import { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Logout } from "./logout";
|
||||
|
||||
@ -19,6 +20,7 @@ export function ActiveAccount() {
|
||||
[],
|
||||
);
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { user } = useProfile(ark.account.pubkey);
|
||||
|
||||
return (
|
||||
@ -62,7 +64,7 @@ export function ActiveAccount() {
|
||||
className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
|
||||
>
|
||||
<UserIcon className="size-4" />
|
||||
Edit profile
|
||||
{t("user.editProfile")}
|
||||
</Link>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item asChild>
|
||||
@ -71,7 +73,7 @@ export function ActiveAccount() {
|
||||
className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
|
||||
>
|
||||
<SettingsIcon className="size-4" />
|
||||
Settings
|
||||
{t("user.settings")}
|
||||
</Link>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Separator className="h-px my-1 bg-black/10 dark:bg-white/10" />
|
||||
|
@ -3,6 +3,7 @@ import { LogoutIcon } from "@lume/icons";
|
||||
import { useStorage } from "@lume/storage";
|
||||
import * as AlertDialog from "@radix-ui/react-alert-dialog";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { toast } from "sonner";
|
||||
|
||||
@ -12,6 +13,8 @@ export function Logout() {
|
||||
const queryClient = useQueryClient();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const logout = async () => {
|
||||
try {
|
||||
// logout
|
||||
@ -38,7 +41,7 @@ export function Logout() {
|
||||
className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
|
||||
>
|
||||
<LogoutIcon className="size-4" />
|
||||
Logout
|
||||
{t("user.logout")}
|
||||
</button>
|
||||
</AlertDialog.Trigger>
|
||||
<AlertDialog.Portal>
|
||||
@ -47,11 +50,10 @@ export function Logout() {
|
||||
<div className="relative h-min w-full max-w-md rounded-xl bg-neutral-100 dark:bg-neutral-900">
|
||||
<div className="flex flex-col gap-1 border-b border-white/5 px-5 py-4">
|
||||
<AlertDialog.Title className="text-lg font-semibold text-neutral-900 dark:text-neutral-100">
|
||||
Are you sure!
|
||||
{t("user.logoutConfirmTitle")}
|
||||
</AlertDialog.Title>
|
||||
<AlertDialog.Description className="text-sm leading-tight text-neutral-600 dark:text-neutral-400">
|
||||
You can always log back in at any time. If you just want to
|
||||
switch accounts, you can do that by adding an existing account.
|
||||
{t("user.logoutConfirmSubtitle")}
|
||||
</AlertDialog.Description>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2 px-5 py-3">
|
||||
@ -60,7 +62,7 @@ export function Logout() {
|
||||
type="button"
|
||||
className="inline-flex h-9 items-center justify-center rounded-lg px-4 text-sm font-medium text-neutral-900 outline-none hover:bg-neutral-200 dark:text-neutral-100 dark:hover:bg-neutral-800"
|
||||
>
|
||||
Cancel
|
||||
{t("global.cancel")}
|
||||
</button>
|
||||
</AlertDialog.Cancel>
|
||||
<AlertDialog.Action asChild>
|
||||
@ -69,7 +71,7 @@ export function Logout() {
|
||||
onClick={() => logout()}
|
||||
className="inline-flex h-9 items-center justify-center rounded-lg bg-red-500 px-4 text-sm font-medium text-white outline-none hover:bg-red-600"
|
||||
>
|
||||
Logout
|
||||
{t("user.logout")}
|
||||
</button>
|
||||
</AlertDialog.Action>
|
||||
</div>
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { useArk } from "@lume/ark";
|
||||
import { LoaderIcon } from "@lume/icons";
|
||||
import { Dispatch, SetStateAction, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export function AvatarUploadButton({
|
||||
@ -9,6 +10,8 @@ export function AvatarUploadButton({
|
||||
setPicture: Dispatch<SetStateAction<string>>;
|
||||
}) {
|
||||
const ark = useArk();
|
||||
|
||||
const [t] = useTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const uploadAvatar = async () => {
|
||||
@ -36,7 +39,7 @@ export function AvatarUploadButton({
|
||||
{loading ? (
|
||||
<LoaderIcon className="size-4 animate-spin" />
|
||||
) : (
|
||||
"Change avatar"
|
||||
t("user.avatarButton")
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
|
@ -6,6 +6,7 @@ import { COL_TYPES, cn, editorValueAtom } from "@lume/utils";
|
||||
import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk";
|
||||
import { useAtom } from "jotai";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Descendant,
|
||||
Editor,
|
||||
@ -200,6 +201,7 @@ export function EditorForm() {
|
||||
withMentions(withNostrEvent(withImages(withReact(createEditor())))),
|
||||
);
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { addColumn } = useColumnContext();
|
||||
|
||||
const filters = contacts
|
||||
@ -247,9 +249,7 @@ export function EditorForm() {
|
||||
const publish = await event.publish();
|
||||
|
||||
if (publish) {
|
||||
toast.success(
|
||||
`Event has been published successfully to ${publish.size} relays.`,
|
||||
);
|
||||
toast.success(t("editor.successMessage"));
|
||||
|
||||
// add current post as column thread
|
||||
addColumn({
|
||||
@ -321,7 +321,7 @@ export function EditorForm() {
|
||||
>
|
||||
<div className="flex items-center justify-between h-16 pl-7 pr-3 border-b shrink-0 border-neutral-100 dark:border-neutral-900 bg-neutral-50 dark:bg-neutral-950">
|
||||
<div>
|
||||
<h3 className="font-medium">New Post</h3>
|
||||
<h3 className="font-medium">{t("editor.title")}</h3>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<div className="inline-flex items-center gap-2">
|
||||
@ -336,7 +336,7 @@ export function EditorForm() {
|
||||
{loading ? (
|
||||
<LoaderIcon className="size-4 animate-spin" />
|
||||
) : (
|
||||
"Post"
|
||||
t("global.post")
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
@ -349,7 +349,7 @@ export function EditorForm() {
|
||||
autoCorrect="none"
|
||||
spellCheck={false}
|
||||
renderElement={(props) => <Element {...props} />}
|
||||
placeholder="What are you up to?"
|
||||
placeholder={t("editor.placeholder")}
|
||||
className="focus:outline-none"
|
||||
/>
|
||||
{target && filters.length > 0 && (
|
||||
|
@ -6,6 +6,7 @@ import { cn } from "@lume/utils";
|
||||
import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk";
|
||||
import { Portal } from "@radix-ui/react-dropdown-menu";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Descendant,
|
||||
Editor,
|
||||
@ -207,6 +208,8 @@ export function ReplyForm({
|
||||
withMentions(withNostrEvent(withImages(withReact(createEditor())))),
|
||||
);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const filters = contacts
|
||||
?.filter((c) => c?.name?.toLowerCase().startsWith(search.toLowerCase()))
|
||||
?.slice(0, 10);
|
||||
@ -334,7 +337,7 @@ export function ReplyForm({
|
||||
autoCorrect="none"
|
||||
spellCheck={false}
|
||||
renderElement={(props) => <Element {...props} />}
|
||||
placeholder="Post your reply"
|
||||
placeholder={t("editor.replyPlaceholder")}
|
||||
className="focus:outline-none h-28"
|
||||
/>
|
||||
{target && filters.length > 0 && (
|
||||
@ -383,7 +386,7 @@ export function ReplyForm({
|
||||
{loading ? (
|
||||
<LoaderIcon className="size-4 animate-spin" />
|
||||
) : (
|
||||
"Post"
|
||||
t("global.post")
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
@ -1,11 +1,14 @@
|
||||
import { InfoIcon } from "@lume/icons";
|
||||
import { cn } from "@lume/utils";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export function EmptyFeed({
|
||||
text,
|
||||
subtext,
|
||||
className,
|
||||
}: { text?: string; subtext?: string; className?: string }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
@ -16,12 +19,10 @@ export function EmptyFeed({
|
||||
<InfoIcon className="size-8 text-blue-500" />
|
||||
<div className="text-center">
|
||||
<p className="font-semibold text-lg">
|
||||
{text ? text : "This feed is empty"}
|
||||
{text ? text : t("global.emptyFeedTitle")}
|
||||
</p>
|
||||
<p className="leading-tight text-sm">
|
||||
{subtext
|
||||
? subtext
|
||||
: "You can follow more users to build up your timeline"}
|
||||
{subtext ? subtext : t("global.emptyFeedSubtitle")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -10,6 +10,7 @@ import {
|
||||
|
||||
import { NDKCacheUserProfile } from "@lume/types";
|
||||
import { cn } from "@lume/utils";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type MentionListRef = {
|
||||
onKeyDown: (props: { event: Event }) => boolean;
|
||||
@ -22,6 +23,7 @@ const List = (
|
||||
},
|
||||
ref: Ref<unknown>,
|
||||
) => {
|
||||
const [t] = useTranslation();
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
|
||||
const selectItem = (index) => {
|
||||
@ -107,7 +109,9 @@ const List = (
|
||||
</button>
|
||||
))
|
||||
) : (
|
||||
<div className="text-center text-sm font-medium">No result</div>
|
||||
<div className="text-center text-sm font-medium">
|
||||
{t("global.noResult")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
@ -1,74 +0,0 @@
|
||||
import { UnverifiedIcon, VerifiedIcon } from "@lume/icons";
|
||||
import { cn } from "@lume/utils";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { fetch } from "@tauri-apps/plugin-http";
|
||||
import { memo } from "react";
|
||||
|
||||
interface NIP05 {
|
||||
names: {
|
||||
[key: string]: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const NIP05 = memo(function NIP05({
|
||||
pubkey,
|
||||
nip05,
|
||||
className,
|
||||
}: {
|
||||
pubkey: string;
|
||||
nip05: string;
|
||||
className?: string;
|
||||
}) {
|
||||
const { status, data } = useQuery({
|
||||
queryKey: ["nip05", nip05],
|
||||
queryFn: async ({ signal }: { signal: AbortSignal }) => {
|
||||
try {
|
||||
const localPath = nip05.split("@")[0];
|
||||
const service = nip05.split("@")[1];
|
||||
const verifyURL = `https://${service}/.well-known/nostr.json?name=${localPath}`;
|
||||
|
||||
const res = await fetch(verifyURL, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
signal,
|
||||
});
|
||||
|
||||
if (!res.ok)
|
||||
throw new Error(`Failed to fetch NIP-05 service: ${nip05}`);
|
||||
|
||||
const data: NIP05 = await res.json();
|
||||
if (data.names) {
|
||||
if (data.names[localPath.toLowerCase()] === pubkey) return true;
|
||||
if (data.names[localPath] === pubkey) return true;
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
} catch (e) {
|
||||
throw new Error(`Failed to verify NIP-05, error: ${e}`);
|
||||
}
|
||||
},
|
||||
refetchOnMount: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchOnWindowFocus: false,
|
||||
staleTime: Infinity,
|
||||
});
|
||||
|
||||
if (status === "pending") {
|
||||
<div className="h-4 w-4 animate-pulse rounded-full bg-neutral-100 dark:bg-neutral-900" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="inline-flex items-center gap-1">
|
||||
<p className={cn("text-sm font-medium", className)}>
|
||||
{nip05.startsWith("_@") ? nip05.replace("_@", "") : nip05}
|
||||
</p>
|
||||
{data === true ? (
|
||||
<VerifiedIcon className="h-4 w-4 text-teal-500" />
|
||||
) : (
|
||||
<UnverifiedIcon className="h-4 w-4 text-red-500" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
@ -4,6 +4,7 @@ import { NDKEventWithReplies } from "@lume/types";
|
||||
import { cn } from "@lume/utils";
|
||||
import { NDKKind, type NDKSubscription } from "@nostr-dev-kit/ndk";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ReplyForm } from "./editor/replyForm";
|
||||
|
||||
export function ReplyList({
|
||||
@ -11,6 +12,8 @@ export function ReplyList({
|
||||
className,
|
||||
}: { eventId: string; className?: string }) {
|
||||
const ark = useArk();
|
||||
|
||||
const [t] = useTranslation();
|
||||
const [data, setData] = useState<null | NDKEventWithReplies[]>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@ -68,7 +71,7 @@ export function ReplyList({
|
||||
<div className="flex flex-col items-center justify-center gap-2 py-6">
|
||||
<h3 className="text-3xl">👋</h3>
|
||||
<p className="leading-none text-neutral-600 dark:text-neutral-400">
|
||||
Be the first to Reply!
|
||||
{t("note.reply.empty")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { User } from "@lume/ark";
|
||||
import { ArrowLeftIcon, ArrowRightIcon, LoaderIcon } from "@lume/icons";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { toast } from "sonner";
|
||||
import { WindowVirtualizer } from "virtua";
|
||||
@ -28,6 +29,7 @@ export function SuggestRoute({ queryKey }: { queryKey: string[] }) {
|
||||
const queryClient = useQueryClient();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { isLoading, isError, data } = useQuery({
|
||||
queryKey: ["trending-users"],
|
||||
queryFn: async ({ signal }: { signal: AbortSignal }) => {
|
||||
@ -71,7 +73,7 @@ export function SuggestRoute({ queryKey }: { queryKey: string[] }) {
|
||||
</div>
|
||||
<div className="relative px-3">
|
||||
<div className="flex items-center h-16">
|
||||
<h3 className="font-semibold text-xl">Suggested Follows</h3>
|
||||
<h3 className="font-semibold text-xl">{t("suggestion.title")}</h3>
|
||||
</div>
|
||||
<div className="flex flex-col divide-y divide-neutral-100 dark:divide-neutral-900">
|
||||
{isLoading ? (
|
||||
@ -80,7 +82,7 @@ export function SuggestRoute({ queryKey }: { queryKey: string[] }) {
|
||||
</div>
|
||||
) : isError ? (
|
||||
<div className="flex h-44 w-full items-center justify-center">
|
||||
Error. Cannot get trending users
|
||||
{t("suggestion.error")}
|
||||
</div>
|
||||
) : (
|
||||
data?.profiles.map((item: { pubkey: string }) => (
|
||||
@ -115,7 +117,7 @@ export function SuggestRoute({ queryKey }: { queryKey: string[] }) {
|
||||
onClick={submit}
|
||||
className="inline-flex items-center justify-center gap-2 px-6 font-medium shadow-xl dark:shadow-none shadow-neutral-500/50 text-white transform bg-blue-500 rounded-full active:translate-y-1 w-44 h-11 hover:bg-blue-600 focus:outline-none disabled:cursor-not-allowed"
|
||||
>
|
||||
Save & Go back
|
||||
{t("suggestion.button")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -9,6 +9,7 @@ import { FETCH_LIMIT } from "@lume/utils";
|
||||
import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk";
|
||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||
import { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { WindowVirtualizer } from "virtua";
|
||||
|
||||
@ -17,6 +18,7 @@ export function UserRoute() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { id } = useParams();
|
||||
const { t } = useTranslation();
|
||||
const { data, hasNextPage, isLoading, isFetchingNextPage, fetchNextPage } =
|
||||
useInfiniteQuery({
|
||||
queryKey: ["user-posts", id],
|
||||
@ -107,7 +109,7 @@ export function UserRoute() {
|
||||
</User.Provider>
|
||||
<div className="pt-2 mt-2 border-t border-neutral-100 dark:border-neutral-900">
|
||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100">
|
||||
Latest posts
|
||||
{t("user.latestPosts")}
|
||||
</h3>
|
||||
<div className="flex h-full w-full flex-col justify-between gap-1.5 pb-10">
|
||||
{isLoading ? (
|
||||
@ -130,7 +132,7 @@ export function UserRoute() {
|
||||
) : (
|
||||
<>
|
||||
<ArrowRightCircleIcon className="size-5" />
|
||||
Load more
|
||||
{t("global.loadMore")}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
@ -4,17 +4,20 @@ import { COL_TYPES, searchAtom } from "@lume/utils";
|
||||
import { type NDKEvent, NDKKind } from "@nostr-dev-kit/ndk";
|
||||
import { useAtom } from "jotai";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useDebounce } from "use-debounce";
|
||||
import { Command } from "../cmdk";
|
||||
|
||||
export function SearchDialog() {
|
||||
const ark = useArk();
|
||||
|
||||
const [open, setOpen] = useAtom(searchAtom);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [events, setEvents] = useState<NDKEvent[]>([]);
|
||||
const [search, setSearch] = useState("");
|
||||
const [value] = useDebounce(search, 1200);
|
||||
|
||||
const ark = useArk();
|
||||
const { t } = useTranslation();
|
||||
const { vlistRef, columns, addColumn } = useColumnContext();
|
||||
|
||||
const searchEvents = async () => {
|
||||
@ -90,7 +93,7 @@ export function SearchDialog() {
|
||||
<Command.Input
|
||||
value={search}
|
||||
onValueChange={setSearch}
|
||||
placeholder="Type something to search..."
|
||||
placeholder={t("search.placeholder")}
|
||||
className="w-full h-12 bg-neutral-100 dark:bg-neutral-900 rounded-xl border-none focus:outline-none focus:ring-0 placeholder:text-neutral-500 dark:placeholder:text-neutral-600"
|
||||
/>
|
||||
</div>
|
||||
@ -101,7 +104,7 @@ export function SearchDialog() {
|
||||
</Command.Loading>
|
||||
) : !events.length ? (
|
||||
<Command.Empty className="flex items-center justify-center h-full text-sm">
|
||||
No results found.
|
||||
{t("global.noResult")}
|
||||
</Command.Empty>
|
||||
) : (
|
||||
<>
|
||||
@ -161,7 +164,7 @@ export function SearchDialog() {
|
||||
<div className="size-16 bg-blue-100 dark:bg-blue-900 rounded-full inline-flex items-center justify-center text-blue-500">
|
||||
<SearchIcon className="size-6" />
|
||||
</div>
|
||||
Try searching for people, notes, or keywords
|
||||
{t("search.empty")}
|
||||
</div>
|
||||
) : null}
|
||||
</Command.List>
|
||||
|
@ -7,7 +7,16 @@
|
||||
"moveLeft": "Move Left",
|
||||
"moveRight": "Move Right",
|
||||
"newColumn": "New Column",
|
||||
"inspect": "Inspect"
|
||||
"inspect": "Inspect",
|
||||
"loadMore": "Load more",
|
||||
"delete": "Delete",
|
||||
"refresh": "Refresh",
|
||||
"cancel": "Cancel",
|
||||
"save": "Save",
|
||||
"post": "Post",
|
||||
"noResult": "No results found.",
|
||||
"emptyFeedTitle": "This feed is empty",
|
||||
"emptyFeedSubtitle": "You can follow more users to build up your timeline"
|
||||
},
|
||||
"nip89": {
|
||||
"unsupported": "Lume isn't support this event",
|
||||
@ -49,9 +58,32 @@
|
||||
},
|
||||
"reply": {
|
||||
"single": "reply",
|
||||
"plural": "replies"
|
||||
"plural": "replies",
|
||||
"empty": "Be the first to Reply!"
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"follow": "Follow",
|
||||
"unfollow": "Unfollow",
|
||||
"latestPosts": "Latest posts",
|
||||
"avatarButton": "Change avatar",
|
||||
"coverButton": "Change cover",
|
||||
"editProfile": "Edit profile",
|
||||
"settings": "Settings",
|
||||
"logout": "Log out",
|
||||
"logoutConfirmTitle": "Are you sure!",
|
||||
"logoutConfirmSubtitle": "You can always log back in at any time. If you just want to switch accounts, you can do that by adding an existing account."
|
||||
},
|
||||
"editor": {
|
||||
"title": "New Post",
|
||||
"placeholder": "What are you up to?",
|
||||
"successMessage": "Your note has been published successfully.",
|
||||
"replyPlaceholder": "Post your reply"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Type something to search...",
|
||||
"empty": "Try searching for people, notes, or keywords"
|
||||
},
|
||||
"welcome": {
|
||||
"title": "Lume is a magnificent client for Nostr to meet, explore\nand freely share your thoughts with everyone.",
|
||||
"signup": "Join Nostr",
|
||||
@ -133,5 +165,17 @@
|
||||
"payment": "Open payment website",
|
||||
"paymentNote": "You need to make a payment to connect this relay"
|
||||
}
|
||||
},
|
||||
"suggestion": {
|
||||
"title": "Suggested Follows",
|
||||
"error": "Error. Cannot get trending users",
|
||||
"button": "Save & Go back"
|
||||
},
|
||||
"interests": {
|
||||
"title": "Interests",
|
||||
"subtitle": "Pick things you'd like to see in your home feed.",
|
||||
"edit": "Edit Interest",
|
||||
"followAll": "Follow All",
|
||||
"unfollowAll": "Unfollow All"
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user