-
Notifications
You must be signed in to change notification settings - Fork 24
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
4229a39
commit 4383913
Showing
6 changed files
with
315 additions
and
190 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}, | ||
}, | ||
], | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.