Skip to content

Commit

Permalink
refactor: usercard
Browse files Browse the repository at this point in the history
  • Loading branch information
kawamataryo committed Nov 30, 2024
1 parent 4229a39 commit 4383913
Show file tree
Hide file tree
Showing 6 changed files with 315 additions and 190 deletions.
112 changes: 112 additions & 0 deletions src/lib/components/DetectedUserListItem.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import type { Meta, StoryObj } from "@storybook/react";

import { ACTION_MODE, BSKY_USER_MATCH_TYPE } from "../constants";
import DetectedUserListItem, { type Props } from "./DetectedUserListItem";

const meta: Meta<typeof DetectedUserListItem> = {
title: "Components/DetectedUserListItem",
component: DetectedUserListItem,
};
export default meta;

type Story = StoryObj<{
items: {
user: Props["user"];
action: Props["clickAction"];
}[];
}>;

const demoUser: Props["user"] = {
did: "",
handle: "kawamataryo.bsky.social",
displayName: "KawamataRyo",
description: `
Frontend engineer @lapras-inc/ TypeScript / Vue.js / Firebase / ex-FireFighter 🔥
Developer of Sky Follower Bridge.
Twitter: twitter.com/KawamataRyo
GitHub: github.com/kawamataryo
Zenn: zenn.dev/ryo_kawamata`,
avatar: "https://i.pravatar.cc/150?u=123",
matchType: BSKY_USER_MATCH_TYPE.HANDLE,
isFollowing: false,
followingUri: "",
isBlocking: false,
blockingUri: "",
originalAvatar: "https://i.pravatar.cc/150?u=123",
originalHandle: "kawamataryo",
originalDisplayName: "KawamataRyo",
originalProfileLink: "https://x.com/kawamataryo",
};

const mockAction: Props["clickAction"] = async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
};

const CardTemplate = {
render: (args: Story["args"]["items"][0]) => (
<DetectedUserListItem
user={args.user}
clickAction={args.action}
actionMode={ACTION_MODE.FOLLOW}
/>
),
};

const CardsTemplate: Story = {
render: (args) => (
<div className="divide-y divide-gray-400 border-y border-gray-400">
{args.items.map((arg, i) => (
<DetectedUserListItem
// biome-ignore lint/suspicious/noArrayIndexKey: <explanation>
key={i}
user={arg.user}
clickAction={arg.action}
actionMode={ACTION_MODE.FOLLOW}
/>
))}
</div>
),
};

export const Default = {
...CardTemplate,
args: {
action: mockAction,
user: {
...demoUser,
matchType: BSKY_USER_MATCH_TYPE.HANDLE,
},
},
};

export const Cards = {
...CardsTemplate,
args: {
items: [
{
action: mockAction,
user: {
...demoUser,
matchType: BSKY_USER_MATCH_TYPE.HANDLE,
isFollowing: true,
},
},
{
action: mockAction,
user: {
...demoUser,
matchType: BSKY_USER_MATCH_TYPE.DESCRIPTION,
},
},
{
action: mockAction,
user: {
...demoUser,
matchType: BSKY_USER_MATCH_TYPE.DISPLAY_NAME,
inFollowing: true,
},
},
],
},
};
99 changes: 99 additions & 0 deletions src/lib/components/DetectedUserListItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import React from "react";
import { match } from "ts-pattern";
import type { BskyUser } from "~types";
import { ACTION_MODE } from "../constants";
import DetectedUserSource from "./DetectedUserSource";
import UserCard from "./UserCard";
export type Props = {
user: BskyUser;
actionMode: (typeof ACTION_MODE)[keyof typeof ACTION_MODE];
clickAction: (user: BskyUser) => Promise<void>;
};

const DetectedUserListItem = ({ user, actionMode, clickAction }: Props) => {
const [isBtnHovered, setIsBtnHovered] = React.useState(false);
const [isJustClicked, setIsJustClicked] = React.useState(false);
const actionBtnLabelAndClass = React.useMemo(
() =>
match(actionMode)
.with(ACTION_MODE.FOLLOW, ACTION_MODE.IMPORT_LIST, () => {
const follow = {
label: "Follow on Bluesky",
class: "btn-primary",
};
const following = {
label: "Following on Bluesky",
class:
"btn-outline hover:bg-transparent hover:border hover:bg-transparent hover:text-base-content",
};
const unfollow = {
label: "Unfollow on Bluesky",
class:
"text-red-500 hover:bg-transparent hover:border hover:border-red-500",
};
if (!isBtnHovered) {
return user.isFollowing ? following : follow;
}
if (user.isFollowing) {
return isJustClicked ? following : unfollow;
}
return follow;
})
.with(ACTION_MODE.BLOCK, () => {
const block = {
label: "Block on Bluesky",
class: "btn-primary",
};
const blocking = {
label: "Blocking on Bluesky",
class:
"btn-outline hover:bg-transparent hover:border hover:bg-transparent hover:text-base-content",
};
const unblock = {
label: "Unblock on Bluesky",
class:
"text-red-500 hover:bg-transparent hover:border hover:border-red-500",
};
if (!isBtnHovered) {
return user.isBlocking ? blocking : block;
}
if (user.isBlocking) {
return isJustClicked ? blocking : unblock;
}
return block;
})
.run(),
[
user.isFollowing,
user.isBlocking,
actionMode,
isBtnHovered,
isJustClicked,
],
);

const [loading, setLoading] = React.useState(false);

const handleActionButtonClick = async () => {
setLoading(true);
await clickAction(user);
setLoading(false);
setIsJustClicked(true);
};

return (
<div className="bg-base-100 w-full relative grid grid-cols-[22%_1fr] gap-5">
<DetectedUserSource user={user} />
<UserCard
user={user}
loading={loading}
actionBtnLabelAndClass={actionBtnLabelAndClass}
handleActionButtonClick={handleActionButtonClick}
setIsBtnHovered={setIsBtnHovered}
setIsJustClicked={setIsJustClicked}
/>
</div>
);
};

export default DetectedUserListItem;
50 changes: 50 additions & 0 deletions src/lib/components/DetectedUserSource.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import React from "react";
import type { BskyUser } from "~types";
import { MATCH_TYPE_LABEL_AND_COLOR } from "../constants";
import { UserInfo, UserProfile } from "./UserCard";

type DetectedUserSourceProps = {
user: BskyUser;
};

const DetectedUserSource = ({ user }: DetectedUserSourceProps) => (
<div className="flex flex-row gap-2 bg-slate-100 dark:bg-slate-800 justify-between pr-2">
<div
className={`border-l-8 border-${
MATCH_TYPE_LABEL_AND_COLOR[user.matchType].color
} relative py-3 pl-4 pr-1 grid grid-cols-[50px_1fr]`}
>
<UserProfile
avatar={user.originalAvatar}
url={user.originalProfileLink}
/>
<div className="flex flex-col gap-2">
<div className="flex justify-between items-center gap-2">
<UserInfo
handle={user.originalHandle}
displayName={user.originalDisplayName}
url={user.originalProfileLink}
/>
</div>
</div>
</div>
<div className="flex items-center justify-center">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="h-7 w-7"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="m8.25 4.5 7.5 7.5-7.5 7.5"
/>
</svg>
</div>
</div>
);

export default DetectedUserSource;
60 changes: 14 additions & 46 deletions src/lib/components/UserCard.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Meta, StoryObj } from "@storybook/react";

import { ACTION_MODE, BSKY_USER_MATCH_TYPE } from "../constants";
import UserCard, { type Props } from "./UserCard";
import { BSKY_USER_MATCH_TYPE } from "../constants";
import UserCard, { type UserCardProps } from "./UserCard";

const meta: Meta<typeof UserCard> = {
title: "Components/UserCard",
Expand All @@ -11,12 +11,11 @@ export default meta;

type Story = StoryObj<{
items: {
user: Props["user"];
action: Props["clickAction"];
user: UserCardProps["user"];
}[];
}>;

const demoUser: Props["user"] = {
const demoUser: UserCardProps["user"] = {
did: "",
handle: "kawamataryo.bsky.social",
displayName: "KawamataRyo",
Expand All @@ -38,17 +37,15 @@ const demoUser: Props["user"] = {
originalDisplayName: "KawamataRyo",
originalProfileLink: "https://x.com/kawamataryo",
};

const mockAction: Props["clickAction"] = async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
};

const CardTemplate = {
render: (args: Story["args"]["items"][0]) => (
<UserCard
user={args.user}
clickAction={args.action}
actionMode={ACTION_MODE.FOLLOW}
loading={false}
actionBtnLabelAndClass={{ label: "Follow", class: "btn-primary" }}
handleActionButtonClick={() => {}}
setIsBtnHovered={() => {}}
setIsJustClicked={() => {}}
/>
),
};
Expand All @@ -61,8 +58,11 @@ const CardsTemplate: Story = {
// biome-ignore lint/suspicious/noArrayIndexKey: <explanation>
key={i}
user={arg.user}
clickAction={arg.action}
actionMode={ACTION_MODE.FOLLOW}
loading={false}
actionBtnLabelAndClass={{ label: "Follow", class: "btn-primary" }}
handleActionButtonClick={() => {}}
setIsBtnHovered={() => {}}
setIsJustClicked={() => {}}
/>
))}
</div>
Expand All @@ -72,41 +72,9 @@ const CardsTemplate: Story = {
export const Default = {
...CardTemplate,
args: {
action: mockAction,
user: {
...demoUser,
matchType: BSKY_USER_MATCH_TYPE.HANDLE,
},
},
};

export const Cards = {
...CardsTemplate,
args: {
items: [
{
action: mockAction,
user: {
...demoUser,
matchType: BSKY_USER_MATCH_TYPE.HANDLE,
isFollowing: true,
},
},
{
action: mockAction,
user: {
...demoUser,
matchType: BSKY_USER_MATCH_TYPE.DESCRIPTION,
},
},
{
action: mockAction,
user: {
...demoUser,
matchType: BSKY_USER_MATCH_TYPE.DISPLAY_NAME,
inFollowing: true,
},
},
],
},
};
Loading

0 comments on commit 4383913

Please sign in to comment.