This repository has been archived by the owner on Dec 9, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 17
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Profile Card
- Loading branch information
Showing
22 changed files
with
518 additions
and
49 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 |
---|---|---|
@@ -1,3 +1,2 @@ | ||
CLIENT_ID="YOUR_CLIENT_ID" | ||
NEYNAR_API_URL="https://sdk-api.neynar.com" | ||
NEYNAR_LOGIN_URL="https://app.neynar.com/login" |
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
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
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
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
171 changes: 171 additions & 0 deletions
171
src/components/NeynarProfileCard/components/ProfileCard.tsx
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,171 @@ | ||
import { useMemo, memo } from "react"; | ||
import { styled } from "@pigment-css/react"; | ||
import { Box, HBox, VBox } from "../../shared/Box"; | ||
import { formatToReadableNumber } from "../../../utils/formatUtils"; | ||
import { useLinkifyBio } from "../hooks/useLinkifyBio"; | ||
import { WarpcastPowerBadge } from "../icons/WarpcastPowerBadge"; | ||
import ButtonOutline from "../../shared/ButtonOutline"; | ||
import ButtonPrimary from "../../shared/ButtonPrimary"; | ||
import Avatar from "../../shared/Avatar"; | ||
|
||
const StyledProfileCard = styled.div(({ theme }) => ({ | ||
display: "flex", | ||
flexDirection: "column", | ||
width: "100%", | ||
maxWidth: "608px", | ||
borderWidth: "1px", | ||
borderStyle: "solid", | ||
borderColor: "var(--palette-border)", | ||
borderRadius: "15px", | ||
padding: "30px", | ||
color: "var(--palette-text)", | ||
fontFamily: theme.typography.fonts.base, | ||
fontSize: theme.typography.fontSizes.medium, | ||
backgroundColor: "var(--palette-background)", | ||
})); | ||
|
||
const Main = styled.div(() => ({ | ||
display: "flex", | ||
flexDirection: "column", | ||
justifyContent: "space-between", | ||
flex: 1, | ||
})); | ||
|
||
const Username = styled.div(() => ({ | ||
color: "var(--palette-textMuted)", | ||
})); | ||
|
||
const UsernameTitle = styled.div(({ theme }) => ({ | ||
fontSize: theme.typography.fontSizes.large, | ||
fontWeight: theme.typography.fontWeights.bold, | ||
})); | ||
|
||
const ProfileMetaCell = styled.div(() => ({ | ||
color: "var(--palette-textMuted)", | ||
"> strong": { | ||
color: "var(--palette-text)", | ||
}, | ||
"& + &": { | ||
marginLeft: "15px", | ||
}, | ||
})); | ||
|
||
const Tag = styled.div(({ theme }) => ({ | ||
borderWidth: "1px", | ||
borderStyle: "solid", | ||
borderColor: "var(--palette-border)", | ||
borderRadius: "5px", | ||
padding: "3px 6px", | ||
marginTop: "3px", | ||
marginLeft: "5px", | ||
backgroundColor: "transparent", | ||
fontSize: theme.typography.fontSizes.small, | ||
color: "var(--palette-textMuted)", | ||
lineHeight: 1, | ||
})); | ||
|
||
export type ProfileCardProps = { | ||
username: string; | ||
displayName: string; | ||
avatarImgUrl: string; | ||
bio: string; | ||
followers: number; | ||
following: number; | ||
hasPowerBadge: boolean; | ||
isFollowing?: boolean; | ||
isOwnProfile?: boolean; | ||
onCast?: () => void; | ||
}; | ||
|
||
export const ProfileCard = memo( | ||
({ | ||
username, | ||
displayName, | ||
avatarImgUrl, | ||
bio, | ||
followers, | ||
following, | ||
hasPowerBadge, | ||
isFollowing, | ||
isOwnProfile, | ||
onCast, | ||
}: ProfileCardProps) => { | ||
const linkifiedBio = useLinkifyBio(bio); | ||
|
||
const formattedFollowingCount = useMemo( | ||
() => formatToReadableNumber(following), | ||
[following] | ||
); | ||
|
||
const formattedFollowersCount = useMemo( | ||
() => formatToReadableNumber(followers), | ||
[followers] | ||
); | ||
|
||
const handleEditProfile = () => { | ||
window.open("https://warpcast.com/~/settings", "_blank"); | ||
}; | ||
|
||
return ( | ||
<StyledProfileCard> | ||
{isOwnProfile && onCast && ( | ||
<HBox | ||
alignItems="center" | ||
justifyContent="space-between" | ||
spacingBottom="20px" | ||
> | ||
<UsernameTitle>@{username}</UsernameTitle> | ||
<ButtonPrimary onClick={onCast}>Cast</ButtonPrimary> | ||
</HBox> | ||
)} | ||
<HBox> | ||
<Box spacingRight="10px"> | ||
<Avatar | ||
src={avatarImgUrl} | ||
loading="lazy" | ||
alt={`${displayName} Avatar`} | ||
/> | ||
</Box> | ||
<Main> | ||
<HBox justifyContent="space-between" flexGrow={1}> | ||
<VBox> | ||
<HBox> | ||
<strong>{displayName}</strong> | ||
{hasPowerBadge && ( | ||
<Box spacingLeft="5px"> | ||
<WarpcastPowerBadge /> | ||
</Box> | ||
)} | ||
</HBox> | ||
<HBox alignItems="center"> | ||
<Username>@{username}</Username> | ||
{isFollowing && <Tag>Follows you</Tag>} | ||
</HBox> | ||
</VBox> | ||
<HBox> | ||
{isOwnProfile && ( | ||
<ButtonOutline onClick={handleEditProfile}> | ||
Edit Profile | ||
</ButtonOutline> | ||
)} | ||
</HBox> | ||
</HBox> | ||
|
||
<Box spacingVertical="15px"> | ||
<div>{linkifiedBio}</div> | ||
</Box> | ||
|
||
<HBox> | ||
<ProfileMetaCell> | ||
<strong>{formattedFollowingCount}</strong> Following | ||
</ProfileMetaCell> | ||
<ProfileMetaCell> | ||
<strong>{formattedFollowersCount}</strong> Followers | ||
</ProfileMetaCell> | ||
</HBox> | ||
</Main> | ||
</HBox> | ||
</StyledProfileCard> | ||
); | ||
} | ||
); |
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,56 @@ | ||
import React from 'react'; | ||
import { styled } from "@pigment-css/react"; | ||
|
||
const WARPCAST_DOMAIN = 'https://warpcast.com'; | ||
|
||
const channelRegex = /\/\w+/g; | ||
const mentionRegex = /@\w+/g; | ||
const urlRegex = /((https?:\/\/)?([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})(\/[^\s]*)?)/g; | ||
const combinedRegex = new RegExp( | ||
`(${channelRegex.source})|(${mentionRegex.source})|(${urlRegex.source})`, | ||
'g' | ||
); | ||
|
||
const generateUrl = (match: string): string => { | ||
if (channelRegex.test(match)) { | ||
return `${WARPCAST_DOMAIN}/~/channel${match}`; | ||
} else if (mentionRegex.test(match)) { | ||
return `${WARPCAST_DOMAIN}/${match.substring(1)}`; | ||
} else if (urlRegex.test(match)) { | ||
return match.startsWith('http') ? match : `http://${match}`; | ||
} | ||
return ''; | ||
}; | ||
|
||
const StyledLink = styled.a(({ theme }) => ({ | ||
textDecoration: "underline", | ||
color: "var(--colors-primary)", | ||
})); | ||
|
||
export const useLinkifyBio = (text: string): React.ReactNode[] => { | ||
const elements: React.ReactNode[] = []; | ||
let lastIndex = 0; | ||
|
||
let match; | ||
while ((match = combinedRegex.exec(text)) !== null) { | ||
const matchIndex = match.index; | ||
if (lastIndex < matchIndex) { | ||
elements.push(text.slice(lastIndex, matchIndex)); | ||
} | ||
|
||
const url = generateUrl(match[0]); | ||
elements.push( | ||
<StyledLink key={matchIndex} href={url} target='_blank'> | ||
{match[0]} | ||
</StyledLink> | ||
); | ||
|
||
lastIndex = combinedRegex.lastIndex; | ||
} | ||
|
||
if (lastIndex < text.length) { | ||
elements.push(text.slice(lastIndex)); | ||
} | ||
|
||
return elements; | ||
}; |
8 changes: 8 additions & 0 deletions
8
src/components/NeynarProfileCard/icons/WarpcastPowerBadge.tsx
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,8 @@ | ||
export const WarpcastPowerBadge = () => { | ||
return ( | ||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"> | ||
<rect width="18" height="18" rx="9" fill="#8A63D2" /> | ||
<path d="M13.375 7.19002H10.25V3.58541C10.25 3.40518 10.125 3.22495 10 3.10479C9.75 2.92456 9.3125 2.98464 9.125 3.22495L4.125 9.83339C4.0625 9.95355 4 10.0737 4 10.1939C4 10.5543 4.25 10.7946 4.625 10.7946H7.75V14.3992C7.75 14.7597 8 15 8.375 15C8.5625 15 8.75 14.8798 8.875 14.7597L13.875 8.15125C13.9375 8.03109 14 7.91094 14 7.79078C14 7.43032 13.75 7.19002 13.375 7.19002Z" fill="white" /> | ||
</svg> | ||
); | ||
}; |
Oops, something went wrong.