Skip to content
This repository has been archived by the owner on Dec 9, 2024. It is now read-only.

Commit

Permalink
feat: Profile Card
Browse files Browse the repository at this point in the history
feat: Profile Card
  • Loading branch information
Shreyaschorge authored Jun 10, 2024
2 parents ea2f0c3 + 5b21aa1 commit d3c624c
Show file tree
Hide file tree
Showing 22 changed files with 518 additions and 49 deletions.
1 change: 0 additions & 1 deletion .env.example
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"
1 change: 1 addition & 0 deletions .storybook/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const config: StorybookConfig = {
"@storybook/addon-essentials",
"@chromatic-com/storybook",
"@storybook/addon-interactions",
'@storybook/addon-themes',
],
framework: {
name: "@storybook/react-vite",
Expand Down
51 changes: 32 additions & 19 deletions .storybook/preview.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,44 @@
import React from "react";
import type { Preview } from "@storybook/react";
import type { Preview, Decorator } from "@storybook/react";
import { withThemeByClassName } from "@storybook/addon-themes";
import { NeynarContextProvider } from "../src/contexts/NeynarContextProvider";
import { Theme } from "../src/enums";

import "../dist/style.css";

const withNeynarProvider = (Story) => (
<NeynarContextProvider
settings={{
clientId: process.env.CLIENT_ID || "",
defaultTheme: Theme.Light,
eventsCallbacks: {
onAuthSuccess(params) {
console.log(`User ${params.user.username} authenticated`);
},
onSignout(user) {
console.log(`User ${user?.username} signed out`);
const themeDecorator = withThemeByClassName({
defaultTheme: Theme.Light,
themes: {
light: "theme-light",
dark: "theme-dark",
},
});

const withNeynarProvider: Decorator = (Story, context) => {
const theme = context.globals.theme || Theme.Light;

return (
<NeynarContextProvider
settings={{
clientId: process.env.CLIENT_ID || "",
defaultTheme: theme,
eventsCallbacks: {
onAuthSuccess(params) {
console.log("onAuthSuccess", params);
},
onSignout(user) {
console.log("onSignout", user);
},
},
},
}}
>
<Story />
</NeynarContextProvider>
);
}}
>
<Story />
</NeynarContextProvider>
);
};

const preview: Preview = {
decorators: [withNeynarProvider],
decorators: [themeDecorator, withNeynarProvider],
parameters: {
controls: {
matchers: {
Expand Down
11 changes: 6 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@neynar/react",
"version": "0.3.1",
"version": "0.4.0",
"description": "Farcaster frontend component library powered by Neynar",
"main": "dist/bundle.cjs.js",
"module": "dist/bundle.es.js",
Expand All @@ -19,20 +19,22 @@
]
},
"peerDependencies": {
"@pigment-css/react": "^0.0.9",
"react": "^18.3.0",
"react-dom": "^18.3.0",
"@pigment-css/react": "^0.0.9"
"react-dom": "^18.3.0"
},
"devDependencies": {
"@babel/core": "^7.24.4",
"@babel/preset-env": "^7.24.4",
"@babel/preset-react": "^7.24.1",
"@chromatic-com/storybook": "^1.3.3",
"@pigment-css/react": "^0.0.9",
"@pigment-css/vite-plugin": "^0.0.9",
"@storybook/addon-essentials": "^8.0.9",
"@storybook/addon-interactions": "^8.0.9",
"@storybook/addon-links": "^8.0.9",
"@storybook/addon-onboarding": "^8.0.9",
"@storybook/addon-themes": "^8.1.2",
"@storybook/blocks": "^8.0.9",
"@storybook/react": "^8.0.9",
"@storybook/react-vite": "^8.0.9",
Expand All @@ -47,7 +49,6 @@
"typescript": "^5.4.5",
"vite": "^5.2.10",
"vite-plugin-dts": "^3.9.0",
"vite-tsconfig-paths": "^4.3.2",
"@pigment-css/react": "^0.0.9"
"vite-tsconfig-paths": "^4.3.2"
}
}
3 changes: 2 additions & 1 deletion src/components/NeynarAuthButton/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,8 @@ export const NeynarAuthButton: React.FC<ButtonProps> = ({
removeNeynarAuthenticatedUser();
setIsAuthenticated(false);
closeModal();
onSignout(_user);
const { signer_uuid, ...rest } = _user;
onSignout(rest);
}
};

Expand Down
171 changes: 171 additions & 0 deletions src/components/NeynarProfileCard/components/ProfileCard.tsx
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>
);
}
);
56 changes: 56 additions & 0 deletions src/components/NeynarProfileCard/hooks/useLinkifyBio.tsx
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 src/components/NeynarProfileCard/icons/WarpcastPowerBadge.tsx
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>
);
};
Loading

0 comments on commit d3c624c

Please sign in to comment.