feat: display the source of detection

This commit is contained in:
kawamataryo 2024-11-24 22:10:33 +09:00
parent 9364cc3ce0
commit be53d28b14
6 changed files with 153 additions and 68 deletions

View File

@ -27,12 +27,16 @@ const demoUser: Props["user"] = {
Twitter: twitter.com/KawamataRyo Twitter: twitter.com/KawamataRyo
GitHub: github.com/kawamataryo GitHub: github.com/kawamataryo
Zenn: zenn.dev/ryo_kawamata`, Zenn: zenn.dev/ryo_kawamata`,
avatar: "https://avatar.iran.liara.run/public", avatar: "https://i.pravatar.cc/150?u=123",
matchType: BSKY_USER_MATCH_TYPE.HANDLE, matchType: BSKY_USER_MATCH_TYPE.HANDLE,
isFollowing: false, isFollowing: false,
followingUri: "", followingUri: "",
isBlocking: false, isBlocking: false,
blockingUri: "", blockingUri: "",
originalAvatar: "https://i.pravatar.cc/150?u=123",
originalHandle: "kawamataryo",
originalDisplayName: "KawamataRyo",
originalProfileLink: "https://x.com/kawamataryo",
}; };
const mockAction: Props["clickAction"] = async () => { const mockAction: Props["clickAction"] = async () => {

View File

@ -4,6 +4,74 @@ import type { BskyUser } from "~types";
import { ACTION_MODE, MATCH_TYPE_LABEL_AND_COLOR } from "../constants"; import { ACTION_MODE, MATCH_TYPE_LABEL_AND_COLOR } from "../constants";
import AvatarFallbackSvg from "./Icons/AvatarFallbackSvg"; import AvatarFallbackSvg from "./Icons/AvatarFallbackSvg";
type UserProfileProps = {
avatar: string;
url: string;
};
const UserProfile = ({ avatar, url }: UserProfileProps) => (
<div className="avatar">
<div className="w-10 h-10 rounded-full border border-white">
<a href={url} target="_blank" rel="noreferrer">
{avatar ? <img src={avatar} alt="" /> : <AvatarFallbackSvg />}
</a>
</div>
</div>
);
type UserInfoProps = {
handle: string;
displayName: string;
url: string;
};
const UserInfo = ({ handle, displayName, url }: UserInfoProps) => (
<div>
<h2 className="card-title break-all text-[1.1rem] font-bold">
<a href={url} target="_blank" rel="noreferrer">
{displayName}
</a>
</h2>
<p className="w-fit break-all text-gray-500 dark:text-gray-400 text-sm">
<a href={url} target="_blank" rel="noreferrer" className="break-all">
@{handle}
</a>
</p>
</div>
);
type ActionButtonProps = {
loading: boolean;
actionBtnLabelAndClass: { label: string; class: string };
handleActionButtonClick: () => void;
setIsBtnHovered: (value: boolean) => void;
setIsJustClicked: (value: boolean) => void;
};
const ActionButton = ({
loading,
actionBtnLabelAndClass,
handleActionButtonClick,
setIsBtnHovered,
setIsJustClicked,
}: ActionButtonProps) => (
<button
type="button"
className={`btn btn-sm rounded-3xl ${
loading ? "" : actionBtnLabelAndClass.class
}`}
onClick={handleActionButtonClick}
onMouseEnter={() => setIsBtnHovered(true)}
onMouseLeave={() => {
setIsBtnHovered(false);
setIsJustClicked(false);
}}
disabled={loading}
>
{loading ? "Processing..." : actionBtnLabelAndClass.label}
</button>
);
export type Props = { export type Props = {
user: BskyUser; user: BskyUser;
actionMode: (typeof ACTION_MODE)[keyof typeof ACTION_MODE]; actionMode: (typeof ACTION_MODE)[keyof typeof ACTION_MODE];
@ -82,67 +150,62 @@ const UserCard = ({ user, actionMode, clickAction }: Props) => {
}; };
return ( return (
<div className="bg-base-100 w-full relative"> <div className="bg-base-100 w-full relative grid grid-cols-[20%_5%_75%]">
<div <div
className={`border-l-8 border-${ className={`border-l-8 border-${
MATCH_TYPE_LABEL_AND_COLOR[user.matchType].color MATCH_TYPE_LABEL_AND_COLOR[user.matchType].color
} card-body relative py-3 px-4 rounded-sm grid grid-cols-[70px_1fr]`} } card-body relative py-3 pl-4 pr-1 rounded-sm grid grid-cols-[50px_1fr]`}
> >
<div> <UserProfile
<div className="avatar"> avatar={user.originalAvatar}
<div className="w-14 rounded-full border border-white "> url={user.originalProfileLink}
<a />
href={`https://bsky.app/profile/${user.handle}`}
target="_blank"
rel="noreferrer"
>
{user.avatar ? (
<img src={user.avatar} alt="" />
) : (
<AvatarFallbackSvg />
)}
</a>
</div>
</div>
</div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<div className="flex justify-between items-center gap-2"> <div className="flex justify-between items-center gap-2">
<div> <UserInfo
<h2 className="card-title break-all"> handle={user.originalHandle}
<a displayName={user.originalDisplayName}
href={`https://bsky.app/profile/${user.handle}`} url={user.originalProfileLink}
target="_blank" />
rel="noreferrer" </div>
> </div>
{user.displayName} </div>
</a> <div className="flex items-center justify-center">
</h2> <svg
<p className="whitespace-nowrap w-fit break-all text-gray-500 dark:text-gray-400 text-sm"> xmlns="http://www.w3.org/2000/svg"
<a fill="none"
href={`https://bsky.app/profile/${user.handle}`} viewBox="0 0 24 24"
target="_blank" strokeWidth={1.5}
rel="noreferrer" stroke="currentColor"
> className="h-7 w-7"
@{user.handle} >
</a> <path
</p> strokeLinecap="round"
</div> strokeLinejoin="round"
d="m5.25 4.5 7.5 7.5-7.5 7.5m6-15 7.5 7.5-7.5 7.5"
/>
</svg>
</div>
<div className="card-body relative py-3 pl-0 pr-2 rounded-sm grid grid-cols-[50px_1fr]">
<UserProfile
avatar={user.avatar}
url={`https://bsky.app/profile/${user.handle}`}
/>
<div className="flex flex-col gap-2">
<div className="flex justify-between items-center gap-2">
<UserInfo
handle={user.handle}
displayName={user.displayName}
url={`https://bsky.app/profile/${user.handle}`}
/>
<div className="card-actions"> <div className="card-actions">
<button <ActionButton
type="button" loading={loading}
className={`btn btn-sm rounded-3xl ${ actionBtnLabelAndClass={actionBtnLabelAndClass}
loading ? "" : actionBtnLabelAndClass.class handleActionButtonClick={handleActionButtonClick}
}`} setIsBtnHovered={setIsBtnHovered}
onClick={handleActionButtonClick} setIsJustClicked={setIsJustClicked}
onMouseEnter={() => setIsBtnHovered(true)} />
onMouseLeave={() => {
setIsBtnHovered(false);
setIsJustClicked(false);
}}
disabled={loading}
>
{loading ? "Processing..." : actionBtnLabelAndClass.label}
</button>
</div> </div>
</div> </div>
<p className="text-sm break-all">{user.description}</p> <p className="text-sm break-all">{user.description}</p>

View File

@ -70,6 +70,10 @@ export const useRetrieveBskyUsers = () => {
followingUri: searchResult.bskyProfile.viewer?.following, followingUri: searchResult.bskyProfile.viewer?.following,
isBlocking: !!searchResult.bskyProfile.viewer?.blocking, isBlocking: !!searchResult.bskyProfile.viewer?.blocking,
blockingUri: searchResult.bskyProfile.viewer?.blocking, blockingUri: searchResult.bskyProfile.viewer?.blocking,
originalAvatar: userData.originalAvatar,
originalHandle: userData.accountName,
originalDisplayName: userData.displayName,
originalProfileLink: userData.originalProfileLink,
}, },
]; ];
}); });

View File

@ -20,6 +20,10 @@ export class XService extends AbstractService {
?.match(/bsky\.app\/profile\/([^/\s]+)…?/)?.[1] ?.match(/bsky\.app\/profile\/([^/\s]+)…?/)?.[1]
?.replace("…", "") ?? ?.replace("…", "") ??
""; "";
const originalAvatar = userCell
.querySelector('[data-testid^="UserAvatar-Container"]')
?.querySelector("img")
?.getAttribute("src");
return { return {
accountName, accountName,
@ -27,6 +31,8 @@ export class XService extends AbstractService {
accountNameRemoveUnderscore, accountNameRemoveUnderscore,
accountNameReplaceUnderscore, accountNameReplaceUnderscore,
bskyHandle, bskyHandle,
originalAvatar,
originalProfileLink: `https://x.com/${accountName}`,
}; };
} }

View File

@ -48,19 +48,21 @@ const Option = () => {
matchTypeStats={matchTypeStats} matchTypeStats={matchTypeStats}
/> />
</div> </div>
<div className="flex-1 ml-80 p-6 overflow-y-auto"> <div className="flex-1 ml-80 p-6 pt-0 overflow-y-auto">
<div className="flex flex-col gap-6"> <div className="grid grid-cols-[25%_75%] sticky top-0 z-10 bg-base-100 border-b-[1px] border-gray-500">
<div className="flex flex-col gap-4"> <h2 className="text-lg font-bold text-center py-2">Source</h2>
<div className="divide-y divide-gray-500"> <h2 className="text-lg font-bold text-center py-2">Detected</h2>
{filteredUsers.map((user) => ( </div>
<UserCard <div className="flex flex-col gap-4">
key={user.handle} <div className="divide-y divide-gray-500">
user={user} {filteredUsers.map((user) => (
clickAction={handleClickAction} <UserCard
actionMode={actionMode} key={user.handle}
/> user={user}
))} clickAction={handleClickAction}
</div> actionMode={actionMode}
/>
))}
</div> </div>
</div> </div>
</div> </div>

View File

@ -16,6 +16,10 @@ export type BskyUser = {
followingUri: string | null; followingUri: string | null;
isBlocking: boolean; isBlocking: boolean;
blockingUri: string | null; blockingUri: string | null;
originalAvatar: string;
originalHandle: string;
originalDisplayName: string;
originalProfileLink: string;
}; };
export type MatchTypeFilterValue = { export type MatchTypeFilterValue = {
@ -31,4 +35,6 @@ export type CrawledUserInfo = {
accountNameRemoveUnderscore: string; accountNameRemoveUnderscore: string;
accountNameReplaceUnderscore: string; accountNameReplaceUnderscore: string;
bskyHandle: string; bskyHandle: string;
originalAvatar: string;
originalProfileLink: string;
}; };