Hey is a decentralized and permissionless social media app built with Lens Protocol 馃尶
1import { BeakerIcon, CheckBadgeIcon } from "@heroicons/react/24/solid";
2import getAccount from "@hey/helpers/getAccount";
3import getAvatar from "@hey/helpers/getAvatar";
4import { type AccountStats, useFullAccountLazyQuery } from "@hey/indexer";
5import * as HoverCard from "@radix-ui/react-hover-card";
6import plur from "plur";
7import type { ReactNode } from "react";
8import Markup from "@/components/Shared/Markup";
9import Slug from "@/components/Shared/Slug";
10import { Card, Image } from "@/components/Shared/UI";
11import getMentions from "@/helpers/getMentions";
12import nFormatter from "@/helpers/nFormatter";
13import truncateByWords from "@/helpers/truncateByWords";
14import ENSBadge from "./ENSBadge";
15import FollowUnfollowButton from "./FollowUnfollowButton";
16
17interface AccountPreviewProps {
18 children: ReactNode;
19 username?: string;
20 address?: string;
21 showUserPreview?: boolean;
22}
23
24const AccountPreview = ({
25 children,
26 username,
27 address,
28 showUserPreview = true
29}: AccountPreviewProps) => {
30 const [loadAccount, { data, loading }] = useFullAccountLazyQuery();
31 const account = data?.account;
32 const stats = data?.accountStats as AccountStats;
33
34 const onPreviewStart = async () => {
35 if (account || loading) {
36 return;
37 }
38
39 await loadAccount({
40 variables: {
41 accountRequest: {
42 ...(address
43 ? { address }
44 : { username: { localName: username as string } })
45 },
46 accountStatsRequest: { account: address }
47 }
48 });
49 };
50
51 if (!address && !username) {
52 return null;
53 }
54
55 if (!showUserPreview) {
56 return <span>{children}</span>;
57 }
58
59 const Preview = () => {
60 if (loading) {
61 return (
62 <div className="flex flex-col">
63 <div className="flex p-3">
64 <div>{username || `#${address}`}</div>
65 </div>
66 </div>
67 );
68 }
69
70 if (!account) {
71 return (
72 <div className="flex h-12 items-center px-3">No account found</div>
73 );
74 }
75
76 const UserAvatar = () => (
77 <Image
78 alt={account.address}
79 className="size-12 rounded-full border border-gray-200 bg-gray-200 dark:border-gray-700"
80 height={48}
81 loading="lazy"
82 src={getAvatar(account)}
83 width={48}
84 />
85 );
86
87 const UserName = () => (
88 <div>
89 <div className="flex max-w-sm items-center gap-1 truncate">
90 <div>{getAccount(account).name}</div>
91 {account.hasSubscribed && (
92 <CheckBadgeIcon className="size-4 text-brand-500" />
93 )}
94 {account.isBeta && <BeakerIcon className="size-4 text-green-500" />}
95 <ENSBadge account={account} className="size-4" />
96 </div>
97 <span>
98 <Slug className="text-sm" slug={getAccount(account).username} />
99 {account.operations?.isFollowingMe && (
100 <span className="ml-2 rounded-full bg-gray-200 px-2 py-0.5 text-xs dark:bg-gray-700">
101 Follows you
102 </span>
103 )}
104 </span>
105 </div>
106 );
107
108 return (
109 <div className="space-y-3 p-4">
110 <div className="flex items-center justify-between">
111 <UserAvatar />
112 <FollowUnfollowButton account={account} small />
113 </div>
114 <UserName />
115 {account.metadata?.bio && (
116 <div className="linkify mt-2 break-words text-sm leading-6">
117 <Markup mentions={getMentions(account.metadata.bio)}>
118 {truncateByWords(account.metadata.bio, 20)}
119 </Markup>
120 </div>
121 )}
122 <div className="flex items-center space-x-3">
123 <div className="flex items-center space-x-1">
124 <div className="text-base">
125 {nFormatter(stats.graphFollowStats?.following)}
126 </div>
127 <div className="text-gray-500 text-sm dark:text-gray-200">
128 Following
129 </div>
130 </div>
131 <div className="flex items-center space-x-1">
132 <div className="text-base">
133 {nFormatter(stats.graphFollowStats?.followers)}
134 </div>
135 <div className="text-gray-500 text-sm dark:text-gray-200">
136 {plur("Follower", stats.graphFollowStats?.followers)}
137 </div>
138 </div>
139 </div>
140 </div>
141 );
142 };
143
144 return (
145 <span onFocus={onPreviewStart} onMouseOver={onPreviewStart}>
146 <HoverCard.Root>
147 <HoverCard.Trigger asChild>
148 <span>{children}</span>
149 </HoverCard.Trigger>
150 <HoverCard.Portal>
151 <HoverCard.Content
152 asChild
153 className="z-10 w-72"
154 side="bottom"
155 sideOffset={5}
156 >
157 <div>
158 <Card forceRounded>
159 <Preview />
160 </Card>
161 </div>
162 </HoverCard.Content>
163 </HoverCard.Portal>
164 </HoverCard.Root>
165 </span>
166 );
167};
168
169export default AccountPreview;