mirror of
https://github.com/snachodog/tok-to-insta-follower-bridge.git
synced 2025-04-23 20:12:22 -06:00
feat: display the source of detection
This commit is contained in:
parent
9364cc3ce0
commit
be53d28b14
@ -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 () => {
|
||||||
|
@ -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>
|
||||||
|
@ -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,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
@ -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}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user