add user page
This commit is contained in:
@@ -13,7 +13,7 @@ export function ChatsListItem({ data }: { data: any }) {
|
||||
<div className="inline-flex h-9 items-center gap-2.5 rounded-md px-2.5">
|
||||
<div className="relative h-6 w-6 shrink-0 animate-pulse rounded bg-zinc-800" />
|
||||
<div>
|
||||
<div className="h-2.5 w-full animate-pulse truncate rounded bg-zinc-800 text-base font-medium" />
|
||||
<div className="h-2.5 w-2/3 animate-pulse truncate rounded bg-zinc-800 text-base font-medium" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -41,7 +41,10 @@ export function ChatsListItem({ data }: { data: any }) {
|
||||
<div className="w-full inline-flex items-center justify-between">
|
||||
<div className="inline-flex items-baseline gap-1">
|
||||
<h5 className="max-w-[9rem] truncate font-medium text-zinc-200">
|
||||
{user?.nip05 || user?.displayName || shortenKey(data.sender_pubkey)}
|
||||
{user?.nip05 ||
|
||||
user?.displayName ||
|
||||
user?.name ||
|
||||
shortenKey(data.sender_pubkey)}
|
||||
</h5>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
|
||||
@@ -112,7 +112,7 @@ export function NewMessageModal() {
|
||||
/>
|
||||
<div className="inline-flex flex-col gap-1">
|
||||
<h3 className="leading-none max-w-[15rem] line-clamp-1 font-medium text-zinc-100">
|
||||
{pleb.display_name || pleb.name}
|
||||
{pleb.displayName || pleb.name}
|
||||
</h3>
|
||||
<span className="leading-none max-w-[10rem] line-clamp-1 text-sm text-zinc-400">
|
||||
{pleb.nip05 ||
|
||||
|
||||
@@ -83,7 +83,7 @@ export function FollowingBlock({ block }: { block: number }) {
|
||||
|
||||
return (
|
||||
<div className="shrink-0 relative w-[400px] border-r border-zinc-900">
|
||||
<TitleBar title="Circle" />
|
||||
<TitleBar title="Your Circle" />
|
||||
{hasNewNote && (
|
||||
<div className="z-50 absolute top-12 left-1/2 transform -translate-x-1/2">
|
||||
<button
|
||||
|
||||
20
src/app/user/components/feed.tsx
Normal file
20
src/app/user/components/feed.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { getNotesByPubkey } from "@libs/storage";
|
||||
import { Note } from "@shared/notes/note";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { LumeEvent } from "@utils/types";
|
||||
|
||||
export function UserFeed({ pubkey }: { pubkey: string }) {
|
||||
const { status, data } = useQuery(["user-feed", pubkey], async () => {
|
||||
return await getNotesByPubkey(pubkey);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-[400px] px-2">
|
||||
{status === "loading" ? (
|
||||
<p>Loading...</p>
|
||||
) : (
|
||||
data.map((note: LumeEvent) => <Note key={note.event_id} event={note} />)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
55
src/app/user/components/metadata.tsx
Normal file
55
src/app/user/components/metadata.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { compactNumber } from "@utils/number";
|
||||
|
||||
export function UserMetadata({ pubkey }: { pubkey: string }) {
|
||||
const { status, data } = useQuery(["user-metadata", pubkey], async () => {
|
||||
const res = await fetch(
|
||||
`https://api.nostr.band/v0/stats/profile/${pubkey}`,
|
||||
);
|
||||
if (!res.ok) {
|
||||
throw new Error("Error");
|
||||
}
|
||||
return await res.json();
|
||||
});
|
||||
|
||||
if (status === "loading") {
|
||||
return <p>Loading...</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full flex items-center gap-10">
|
||||
<div className="inline-flex flex-col gap-1">
|
||||
<span className="leading-none font-semibold text-zinc-100">
|
||||
{data.stats[pubkey].followers_pubkey_count ?? 0}
|
||||
</span>
|
||||
<span className="leading-none text-sm text-zinc-400">Followers</span>
|
||||
</div>
|
||||
<div className="inline-flex flex-col gap-1">
|
||||
<span className="leading-none font-semibold text-zinc-100">
|
||||
{data.stats[pubkey].pub_following_pubkey_count ?? 0}
|
||||
</span>
|
||||
<span className="leading-none text-sm text-zinc-400">Following</span>
|
||||
</div>
|
||||
<div className="inline-flex flex-col gap-1">
|
||||
<span className="leading-none font-semibold text-zinc-100">
|
||||
{data.stats[pubkey].zaps_received
|
||||
? compactNumber.format(
|
||||
data.stats[pubkey].zaps_received.msats / 1000,
|
||||
)
|
||||
: 0}
|
||||
</span>
|
||||
<span className="leading-none text-sm text-zinc-400">
|
||||
Zaps received
|
||||
</span>
|
||||
</div>
|
||||
<div className="inline-flex flex-col gap-1">
|
||||
<span className="leading-none font-semibold text-zinc-100">
|
||||
{data.stats[pubkey].zaps_sent
|
||||
? compactNumber.format(data.stats[pubkey].zaps_sent.msats / 1000)
|
||||
: 0}
|
||||
</span>
|
||||
<span className="leading-none text-sm text-zinc-400">Zaps sent</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,47 +1,25 @@
|
||||
import { usePublish } from "@libs/ndk";
|
||||
import { UserFeed } from "@app/user/components/feed";
|
||||
import { UserMetadata } from "@app/user/components/metadata";
|
||||
import { Tab } from "@headlessui/react";
|
||||
import { ThreadsIcon, ZapIcon } from "@shared/icons";
|
||||
import { Image } from "@shared/image";
|
||||
import { DEFAULT_AVATAR } from "@stores/constants";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useFollows } from "@utils/hooks/useFollows";
|
||||
import { useProfile } from "@utils/hooks/useProfile";
|
||||
import { compactNumber } from "@utils/number";
|
||||
import { useSocial } from "@utils/hooks/useSocial";
|
||||
import { shortenKey } from "@utils/shortenKey";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Fragment, useEffect, useState } from "react";
|
||||
import { Link, useParams } from "react-router-dom";
|
||||
|
||||
export function UserScreen() {
|
||||
const publish = usePublish();
|
||||
const [followed, setFollowed] = useState(false);
|
||||
|
||||
const { pubkey } = useParams();
|
||||
const { user } = useProfile(pubkey);
|
||||
const { status: followsStatus, follows } = useFollows();
|
||||
const {
|
||||
status: userStatsStatus,
|
||||
data: userStats,
|
||||
error,
|
||||
} = useQuery(["user", pubkey], async () => {
|
||||
const res = await fetch(
|
||||
`https://api.nostr.band/v0/stats/profile/${pubkey}`,
|
||||
);
|
||||
if (!res.ok) {
|
||||
throw new Error("Error");
|
||||
}
|
||||
return await res.json();
|
||||
});
|
||||
const { status, userFollows, follow, unfollow } = useSocial();
|
||||
|
||||
const follow = (pubkey: string) => {
|
||||
const [followed, setFollowed] = useState(false);
|
||||
|
||||
const followUser = (pubkey: string) => {
|
||||
try {
|
||||
const followsAsSet = new Set(follows);
|
||||
followsAsSet.add(pubkey);
|
||||
|
||||
const tags = [];
|
||||
followsAsSet.forEach((item) => {
|
||||
tags.push(["p", item]);
|
||||
});
|
||||
|
||||
// publish event
|
||||
publish({ content: "", kind: 3, tags: tags });
|
||||
follow(pubkey);
|
||||
|
||||
// update state
|
||||
setFollowed(true);
|
||||
@@ -50,18 +28,9 @@ export function UserScreen() {
|
||||
}
|
||||
};
|
||||
|
||||
const unfollow = (pubkey: string) => {
|
||||
const unfollowUser = (pubkey: string) => {
|
||||
try {
|
||||
const followsAsSet = new Set(follows);
|
||||
followsAsSet.delete(pubkey);
|
||||
|
||||
const tags = [];
|
||||
followsAsSet.forEach((item) => {
|
||||
tags.push(["p", item]);
|
||||
});
|
||||
|
||||
// publish event
|
||||
publish({ content: "", kind: 3, tags: tags });
|
||||
unfollow(pubkey);
|
||||
|
||||
// update state
|
||||
setFollowed(false);
|
||||
@@ -71,15 +40,15 @@ export function UserScreen() {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (followsStatus === "success" && follows) {
|
||||
if (follows.includes(pubkey)) {
|
||||
if (status === "success" && userFollows) {
|
||||
if (userFollows.includes(pubkey)) {
|
||||
setFollowed(true);
|
||||
}
|
||||
}
|
||||
}, [followsStatus]);
|
||||
}, [status]);
|
||||
|
||||
return (
|
||||
<div className="h-full w-full">
|
||||
<div className="h-full w-full overflow-y-auto">
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
className="h-11 w-full flex items-center px-3 border-b border-zinc-900"
|
||||
@@ -92,107 +61,97 @@ export function UserScreen() {
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full px-5 -mt-7">
|
||||
<div>
|
||||
<div className="w-full -mt-7">
|
||||
<div className="px-5">
|
||||
<Image
|
||||
src={user?.image}
|
||||
fallback={DEFAULT_AVATAR}
|
||||
alt={pubkey}
|
||||
className="w-14 h-14 rounded-md ring-2 ring-black"
|
||||
/>
|
||||
<div className="flex-1 flex flex-col gap-2 mt-4">
|
||||
<h5 className="font-semibold leading-none">
|
||||
{user?.displayName || user?.name || "No name"}
|
||||
</h5>
|
||||
<span className="max-w-[15rem] text-sm truncate leading-none text-zinc-500">
|
||||
{user?.nip05 || shortenKey(pubkey)}
|
||||
</span>
|
||||
<p className="mt-1 line-clamp-3 break-words leading-tight text-zinc-100">
|
||||
{user?.about}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-8">
|
||||
{error && <p>Failed to fetch user stats</p>}
|
||||
{userStatsStatus === "loading" ? (
|
||||
<p>Loading...</p>
|
||||
) : (
|
||||
<div className="w-full flex items-center gap-10">
|
||||
<div className="inline-flex flex-col gap-1">
|
||||
<span className="leading-none font-semibold text-zinc-100">
|
||||
{userStats.stats[pubkey].followers_pubkey_count ?? 0}
|
||||
</span>
|
||||
<span className="leading-none text-sm text-zinc-400">
|
||||
Followers
|
||||
<div className="flex-1 flex flex-col gap-4 mt-2">
|
||||
<div className="flex items-center gap-16">
|
||||
<div className="inline-flex flex-col gap-1.5">
|
||||
<h5 className="font-semibold text-lg leading-none">
|
||||
{user?.displayName || user?.name || "No name"}
|
||||
</h5>
|
||||
<span className="max-w-[15rem] text-sm truncate leading-none text-zinc-500">
|
||||
{user?.nip05 || shortenKey(pubkey)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="inline-flex flex-col gap-1">
|
||||
<span className="leading-none font-semibold text-zinc-100">
|
||||
{userStats.stats[pubkey].pub_following_pubkey_count ?? 0}
|
||||
</span>
|
||||
<span className="leading-none text-sm text-zinc-400">
|
||||
Following
|
||||
</span>
|
||||
</div>
|
||||
<div className="inline-flex flex-col gap-1">
|
||||
<span className="leading-none font-semibold text-zinc-100">
|
||||
{userStats.stats[pubkey].zaps_received
|
||||
? compactNumber.format(
|
||||
userStats.stats[pubkey].zaps_received.msats / 1000,
|
||||
)
|
||||
: 0}
|
||||
</span>
|
||||
<span className="leading-none text-sm text-zinc-400">
|
||||
Zaps received
|
||||
</span>
|
||||
</div>
|
||||
<div className="inline-flex flex-col gap-1">
|
||||
<span className="leading-none font-semibold text-zinc-100">
|
||||
{userStats.stats[pubkey].zaps_sent
|
||||
? compactNumber.format(
|
||||
userStats.stats[pubkey].zaps_sent.msats / 1000,
|
||||
)
|
||||
: 0}
|
||||
</span>
|
||||
<span className="leading-none text-sm text-zinc-400">
|
||||
Zaps sent
|
||||
</span>
|
||||
<div className="inline-flex items-center gap-2">
|
||||
{status === "loading" ? (
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex w-36 h-10 items-center justify-center rounded-md bg-zinc-900 hover:bg-fuchsia-500 text-sm font-medium"
|
||||
>
|
||||
Loading...
|
||||
</button>
|
||||
) : followed ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => unfollowUser(pubkey)}
|
||||
className="inline-flex w-36 h-10 items-center justify-center rounded-md bg-zinc-900 hover:bg-fuchsia-500 text-sm font-medium"
|
||||
>
|
||||
Unfollow
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => followUser(pubkey)}
|
||||
className="inline-flex w-36 h-10 items-center justify-center rounded-md bg-zinc-900 hover:bg-fuchsia-500 text-sm font-medium"
|
||||
>
|
||||
Follow
|
||||
</button>
|
||||
)}
|
||||
<Link
|
||||
to={`/app/chat/${pubkey}`}
|
||||
className="inline-flex w-36 h-10 items-center justify-center rounded-md bg-zinc-900 hover:bg-fuchsia-500 text-sm font-medium"
|
||||
>
|
||||
Message
|
||||
</Link>
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex w-10 h-10 items-center justify-center rounded-md bg-zinc-900 group hover:bg-orange-500 text-sm font-medium"
|
||||
>
|
||||
<ZapIcon className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-6 flex items-center gap-2">
|
||||
{followsStatus === "loading" ? (
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex w-44 h-10 items-center justify-center rounded-md bg-zinc-900 hover:bg-fuchsia-500 text-sm font-medium"
|
||||
>
|
||||
Loading...
|
||||
</button>
|
||||
) : followed ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => unfollow(pubkey)}
|
||||
className="inline-flex w-44 h-10 items-center justify-center rounded-md bg-zinc-900 hover:bg-fuchsia-500 text-sm font-medium"
|
||||
>
|
||||
Unfollow
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => follow(pubkey)}
|
||||
className="inline-flex w-44 h-10 items-center justify-center rounded-md bg-zinc-900 hover:bg-fuchsia-500 text-sm font-medium"
|
||||
>
|
||||
Follow
|
||||
</button>
|
||||
)}
|
||||
<Link
|
||||
to={`/app/chat/${pubkey}`}
|
||||
className="inline-flex w-44 h-10 items-center justify-center rounded-md bg-zinc-900 hover:bg-fuchsia-500 text-sm font-medium"
|
||||
>
|
||||
Message
|
||||
</Link>
|
||||
<div className="flex flex-col gap-8">
|
||||
<p className="mt-2 max-w-[500px] break-words select-text text-zinc-100">
|
||||
{user?.about}
|
||||
</p>
|
||||
<UserMetadata pubkey={pubkey} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-8 w-full border-t border-zinc-900">
|
||||
<Tab.Group>
|
||||
<Tab.List className="px-5 mb-2">
|
||||
<Tab as={Fragment}>
|
||||
{({ selected }) => (
|
||||
<button
|
||||
type="button"
|
||||
className={`${
|
||||
selected
|
||||
? "text-fuchsia-500 border-fuchsia-500"
|
||||
: "text-zinc-200 border-transparent"
|
||||
} font-medium inline-flex items-center gap-2 h-10 border-t`}
|
||||
>
|
||||
<ThreadsIcon className="w-4 h-4" />
|
||||
Posts
|
||||
</button>
|
||||
)}
|
||||
</Tab>
|
||||
</Tab.List>
|
||||
<Tab.Panels>
|
||||
<Tab.Panel>
|
||||
<UserFeed pubkey={pubkey} />
|
||||
</Tab.Panel>
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user