diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d34a278bc..e5748c8025 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,27 @@ > All notable changes to this project will be documented in this file +## [2.54.0-beta.1](https://github.com/open-sauced/app/compare/v2.53.1-beta.2...v2.54.0-beta.1) (2024-08-12) + + +### πŸ• Features + +* update DevCard design ([#3879](https://github.com/open-sauced/app/issues/3879)) ([979576d](https://github.com/open-sauced/app/commit/979576d482ec1f0d6fe5cc6b63468ac27e1628a8)) + +## [2.53.1-beta.2](https://github.com/open-sauced/app/compare/v2.53.1-beta.1...v2.53.1-beta.2) (2024-08-12) + + +### πŸ› Bug Fixes + +* contributor insight card orange dot no longer squished ([#3912](https://github.com/open-sauced/app/issues/3912)) ([01cb265](https://github.com/open-sauced/app/commit/01cb265d531e7b9392d89b397436afab12442c78)) + +## [2.53.1-beta.1](https://github.com/open-sauced/app/compare/v2.53.0...v2.53.1-beta.1) (2024-08-12) + + +### πŸ› Bug Fixes + +* add margin left to prevent overflow of nav item ([#3904](https://github.com/open-sauced/app/issues/3904)) ([c8b8ace](https://github.com/open-sauced/app/commit/c8b8acec3595bf4a243f15ade411f4fe4d4e7693)) + ## [2.53.0](https://github.com/open-sauced/app/compare/v2.52.0...v2.53.0) (2024-08-12) diff --git a/components/atoms/Pill/pill.tsx b/components/atoms/Pill/pill.tsx index 33d73fc0b3..b5a06c8ae5 100644 --- a/components/atoms/Pill/pill.tsx +++ b/components/atoms/Pill/pill.tsx @@ -30,7 +30,21 @@ const Pill: React.FC = ({ className, text, color = "slate", size = "b ${size === "small" ? "py-1 px-1.5" : "py-1.5 px-2 "} inline-flex items-center rounded-full w-fit gap-1 ${className}`} > - {icon} + + {icon} +

= (args) => void; -} +}; export default function DevCard(props: DevCardProps) { const [isFlipped, setIsFlipped] = useState(props.isFlipped ?? false); const isInteractive = props.isInteractive ?? true; - const activity = getActivity(props.prs ?? 0); - useEffect(() => { setIsFlipped(props.isFlipped ?? false); }, [props.isFlipped]); @@ -50,38 +34,18 @@ export default function DevCard(props: DevCardProps) { } }, [props.isInteractive, props.isFlipped]); - const profileHref = `/u/${props.username}`; - function handleCardClick(event: React.MouseEvent) { if (!isInteractive) { return; } + // flip the card if the click is not on the button if (!(event.target instanceof HTMLAnchorElement || event.target instanceof HTMLButtonElement)) { - if (props.isFlipped === undefined) { - setIsFlipped(!isFlipped); - } else { - props.onFlip?.(); - } + setIsFlipped(!isFlipped); + props.onFlip?.(); } } - const faceClasses = clsx( - "flex", - "flex-col", - "items-stretch", - "justify-items-stretch", - "overflow-hidden", - "rounded-3xl", - "border-white", - "cursor-pointer", - "w-full", - "h-full", - "absolute", - "left-0", - "top-0" - ); - const faceStyle: React.CSSProperties = { backfaceVisibility: "hidden", background: "#11181c linear-gradient(152.13deg, rgba(217, 217, 217, 0.1) 0%, rgba(255, 255, 255, 0.1) 100%)", @@ -89,6 +53,12 @@ export default function DevCard(props: DevCardProps) { border: "2px", }; + const getValueBasedOnCount = ({ low, med, high }: { low: any; med: any; high: any }) => { + const recent_pull_requests_count = props.user?.recent_pull_requests_count || 0; + + return recent_pull_requests_count < 7 ? low : recent_pull_requests_count < 28 ? med : high; + }; + return (

+ {/** Front View **/}
-
-
-
- OpenSauced Logo -

- OpenSauced -

-
+ devcard-gradient + devcard-border + +
+ {/** Avatar + @Username **/} +
+ {`${props.user?.login} +

@{props.user?.login}

-
- {!props.isLoading && ( -
- -
- )} -
@{props.username}
-
-
-
{props.isLoading ? "-" : props.oscr}
-
OSCR
-
-
+ + {/** OSCR Score **/} +
+

{!props.user?.oscr ? "-" : Math.ceil(props.user?.oscr)}

+

+ OSCR Score +

-
-
- avatar + + {/** 'Ranking' Badge **/} + {props.user?.oscr && props.user?.oscr > 200 && }
+ + {/** Back View **/}
-
-
+
+ {/** Avatar + username + OSCR **/} +
avatar -
-
{props.name ?? props.username}
-
- {props.prs !== undefined && ( -
- -
{props.prs} PR
-
- )} - {props.age !== undefined && ( -
- -
{getRelativeDays(props.age)}
-
- )} -
-
+

{props.user?.login}

+

+ {Math.ceil(props.user?.oscr || 0)} + /300 +

-
-
-
Activity
- {!props.isLoading ? ( - - ) : ( -
{DATA_FALLBACK_VALUE}
- )} + + {/** Bio **/} +

{props.user?.bio}

+ + {/** Last 30 Days **/} +
+

Last 30 days

+ +
+

Activity

+
- -
-
PRs Velocity
-
- {!props.prVelocity || props.prMergePercentage == undefined ? ( -
{DATA_FALLBACK_VALUE}
- ) : ( - <> -
{getRelativeDays(props.prVelocity)}
- - - )} -
+ +
+

Opened PRs

+

{props.user?.recent_pull_requests_count}

- -
{props.bio}
+ +
+

PR Velocity

+

{getRelativeDays(props.user?.recent_pull_request_velocity_count || 0)}

+
+
- {/* bottom */} -
+ + {/** Footer **/} +
{!props.hideProfileButton && ( - - - + )} -
+
OpenSauced Logo

OpenSauced @@ -255,40 +198,71 @@ export default function DevCard(props: DevCardProps) { ); } -type Activity = "high" | "mid"; - -function getActivity(prs: number): Activity { - if (prs > 4) { - return "high"; - } else { - return "mid"; - } +function Separator() { + return ( +

+ ); } -function VelocityPill({ velocity, ...props }: { velocity: number } & Omit) { - const icon = - velocity > 0 ? : ; - return ; -} +function ActivityBadge({ + getValueBasedOnCount, +}: { + getValueBasedOnCount: ({ low, med, high }: { low: any; med: any; high: any }) => any; +}) { + const status = getValueBasedOnCount({ + low: "Low", + med: "Mid", + high: "High", + }); + + const icon = getValueBasedOnCount({ + low: , + med: , + high: , + }); -function ActivityPill({ activity, ...props }: { activity: Activity } & Omit) { - const color = activity === "high" ? "green" : "yellow"; - const activityText = activity === "high" ? "High" : "Mid"; - const icon = - activity === "high" ? ( - - ) : ( - - ); + const color = getValueBasedOnCount({ + low: "red", + med: "yellow", + high: "green", + }); - return ; + return ; } -function Seperator() { +function RankingBadge({ oscr }: { oscr: number }) { + const getValueBasedOnOscr = ({ + five, + four, + three, + two, + one, + }: { + five: any; + four: any; + three: any; + two: any; + one: any; + }) => { + return oscr > 250 ? one : oscr > 235 ? two : oscr > 225 ? three : oscr > 215 ? four : oscr > 200 ? five : null; + }; + + const percentage = getValueBasedOnOscr({ five: 5, four: 4, three: 3, two: 2, one: 1 }); + const icon = getValueBasedOnOscr({ + five: , + four: , + three: , + two: , + one: , + }); + return ( -
+
+ {icon} +

In the top {percentage}%

+
); } diff --git a/components/molecules/ListCard/list-card.tsx b/components/molecules/ListCard/list-card.tsx index 843d15b973..c08da9ccf1 100644 --- a/components/molecules/ListCard/list-card.tsx +++ b/components/molecules/ListCard/list-card.tsx @@ -29,7 +29,7 @@ const ListCard = ({ list, handleOnDeleteClick, workspaceId, user }: ListCardProp
-
+
{ it("should trigger the onSelect", async () => { const onSelect = vi.fn(); render(); - const thirdDevCard = screen.getByTitle(`Select @${STUB_DEV_CARDS[2].username}`); + const thirdDevCard = screen.getByTitle(`Select @${STUB_DEV_CARDS[2].login}`); await userEvent.click(thirdDevCard); - expect(onSelect).toHaveBeenCalledWith(STUB_DEV_CARDS[2].username); + expect(onSelect).toHaveBeenCalledWith(STUB_DEV_CARDS[2].login); }); }); @@ -25,7 +25,7 @@ describe("DevCardCarousel", () => { const onSelect = vi.fn(); render(); await userEvent.keyboard("{arrowright}"); - expect(onSelect).toHaveBeenCalledWith(STUB_DEV_CARDS.slice(-1)[0].username); + expect(onSelect).toHaveBeenCalledWith(STUB_DEV_CARDS.slice(-1)[0].login); }); }); describe("when the user presses the left arrow key", () => { @@ -33,7 +33,7 @@ describe("DevCardCarousel", () => { const onSelect = vi.fn(); render(); await userEvent.keyboard("{arrowleft}"); - expect(onSelect).toHaveBeenCalledWith(STUB_DEV_CARDS[1].username); + expect(onSelect).toHaveBeenCalledWith(STUB_DEV_CARDS[1].login); }); }); }); diff --git a/components/organisms/DevCardCarousel/dev-card-carousel.tsx b/components/organisms/DevCardCarousel/dev-card-carousel.tsx index d0bcb68151..33a04cc455 100644 --- a/components/organisms/DevCardCarousel/dev-card-carousel.tsx +++ b/components/organisms/DevCardCarousel/dev-card-carousel.tsx @@ -3,10 +3,11 @@ import { animated, to, useSprings } from "@react-spring/web"; import { useGesture } from "@use-gesture/react"; import { useCallback, useEffect, useState } from "react"; import { useKey } from "react-use"; -import DevCard, { DevCardProps } from "components/molecules/DevCard/dev-card"; +import DevCard from "components/molecules/DevCard/dev-card"; +import { UserDevStats } from "pages/u/[username]/card"; export interface DevCardCarouselProps { - cards: DevCardProps[]; + cards: UserDevStats[]; onSelect?: (username: string) => void; } @@ -43,7 +44,7 @@ export default function DevCardCarousel(props: DevCardCarouselProps) { const handleSelect = useCallback( (cardOrderIndex: number) => { const cardIndex = cardOrder[cardOrderIndex]; - props.onSelect?.(props.cards[cardIndex].username); + props.onSelect?.(props.cards[cardIndex].login); // take all cards above the clicked card and move them down setCardOrder((cards) => { const cardsAfterIndex = cards.slice(cardOrderIndex); @@ -102,10 +103,10 @@ export default function DevCardCarousel(props: DevCardCarouselProps) { return ( - + { handleClick(cardOrderIndex); diff --git a/components/organisms/DevCardCarousel/stubData.ts b/components/organisms/DevCardCarousel/stubData.ts index 2270c6d3fe..3efdf1c9cd 100644 --- a/components/organisms/DevCardCarousel/stubData.ts +++ b/components/organisms/DevCardCarousel/stubData.ts @@ -1,56 +1,216 @@ export const STUB_DEV_CARDS = [ { - username: "foxyblocks", - name: "Chris Schlensker", - avatarURL: "https://avatars.githubusercontent.com/u/555044?v=4", - prs: 2, - repos: 57, - bio: "This is the story all about how my life got flipped turned upside down, and I'd like to take a minute just sit right there, I'll tell you how I became the prince of a town called Bel-Air.", - prVelocity: 102, + email: "", + notification_count: 0, + insights_count: 0, + personal_workspace_id: "", + id: 20603494, + open_issues: 31, + created_at: "2016-07-22T19:49:17.000Z", + updated_at: "2024-08-08T19:49:09.940Z", + first_opened_pr_at: "2022-11-09T20:50:08.000Z", + first_pushed_commit_at: "2019-04-11T20:59:57.000Z", + connected_at: "2024-01-31T17:16:08.609Z", + campaign_start_date: "2024-01-31T17:16:08.609Z", + node_id: "", + avatar_url: "https://avatars.githubusercontent.com/u/20603494?u=dab23ed63c98dc94be294eb1a826e8d204b72235&v=4", + gravatar_id: "", + url: "https://github.com/zeucapua", + login: "zeucapua", + is_private: false, + is_open_sauced_member: true, + is_onboarded: true, + is_waitlisted: true, + role: 10, + bio: "Fullstack Web Developer and Coding Educator", + blog: "zeu.dev", + name: "zeudev", + twitter_username: "zeu_dev", + linkedin_url: "", + github_sponsors_url: "", + discord_url: "", + company: "@open-sauced", + location: "Los Angeles, CA", + display_local_time: false, + interests: "javascript,typescript,svelte,react", + hireable: false, + public_repos: 58, + public_gists: 0, + type: "User", + display_email: false, + receive_collaboration: false, + receive_product_updates: true, + timezone: "Pacific Daylight Time", + coupon_code: "", + languages: { + CSS: 228, + HTML: 346, + Svelte: 10076, + JavaScript: 1015, + TypeScript: 5284, + }, + highlights_count: 0, + following_count: 1, + followers_count: 1, + oscr: 165.20000000000002, + devstats_updated_at: "2024-08-08T19:49:08.000Z", + accepted_usage_terms: false, + recent_pull_request_velocity_count: 4, + recent_pull_requests_count: 23, + is_maintainer: true, + commits: 0, + prs_created: 19, + prs_reviewed: 14, + issues_created: 15, + commit_comments: 0, + issue_comments: 14, + pr_review_comments: 3, + comments: 17, + total_contributions: 48, }, { - username: "codebytere", - name: "Shelley Vohr", - avatarURL: "https://avatars.githubusercontent.com/u/2036040?v=4", - prs: 31, - repos: 1, - bio: "This is the story all about how my life got flipped turned upside down, and I'd like to take a minute just sit right there, I'll tell you how I became the prince of a town called Bel-Air.", - prVelocity: 67, + email: "", + notification_count: 0, + insights_count: 0, + personal_workspace_id: "3432aaf7-55db-4c2b-9fde-f61604471e21", + id: 833231, + open_issues: 150, + created_at: "2011-06-06T17:32:57.000Z", + updated_at: "2024-08-10T02:49:07.497Z", + first_opened_pr_at: "2014-05-01T01:58:22.000Z", + first_pushed_commit_at: "2020-03-30T12:04:16.000Z", + connected_at: "2023-07-30T03:31:02.219Z", + campaign_start_date: null, + node_id: "MDQ6VXNlcjgzMzIzMQ==", + avatar_url: "https://avatars.githubusercontent.com/u/833231?u=c462621b379f11fb265d8f85b8c75016ad33d243&v=4", + gravatar_id: "", + url: "https://github.com/nickytonline", + login: "nickytonline", + is_private: false, + is_open_sauced_member: true, + is_onboarded: true, + is_waitlisted: true, + role: 50, + bio: "Senior Software Engineer at OpenSauced", + blog: "https://oss.fyi/nickytonline", + name: "Nick Taylor", + twitter_username: "nickytonline", + linkedin_url: "https://www.linkedin.com/in/nickytonline/", + github_sponsors_url: "https://github.com/sponsors/nickytonline", + discord_url: "", + company: "@open-sauced", + location: "Montreal, Canada", + display_local_time: true, + interests: "javascript,typescript,csharp,ai,ml,react,rust", + hireable: false, + public_repos: 197, + public_gists: 32, + type: "User", + display_email: true, + receive_collaboration: true, + receive_product_updates: true, + timezone: "Egypt Standard Time", + coupon_code: "OPENSAUCEDFOREVERTEST", + languages: { + CSS: 2139, + SCSS: 92601, + Astro: 23912, + Shell: 2987, + Liquid: 3994, + Nunjucks: 30270, + JavaScript: 97519, + TypeScript: 65126, + }, + highlights_count: 13, + following_count: 38, + followers_count: 9, + oscr: 258.00000000000006, + devstats_updated_at: "2024-08-09T15:19:50.000Z", + accepted_usage_terms: false, + recent_pull_request_velocity_count: 1, + recent_pull_requests_count: 41, + is_maintainer: true, + commits: 98, + prs_created: 39, + prs_reviewed: 151, + issues_created: 36, + commit_comments: 0, + issue_comments: 55, + pr_review_comments: 55, + comments: 110, + total_contributions: 324, }, { - username: "miniak", - name: "Milan Burda", - avatarURL: "https://avatars.githubusercontent.com/u/1281234?v=4", - prs: 31, - repos: 1, - bio: "This is the story all about how my life got flipped turned upside down, and I'd like to take a minute just sit right there, I'll tell you how I became the prince of a town called Bel-Air.", - prVelocity: 67, - }, - { - username: "ckerr", - name: "Charles Kerr", - avatarURL: "https://avatars.githubusercontent.com/u/70381?v=4", - prs: 31, - repos: 1, - bio: "This is the story all about how my life got flipped turned upside down, and I'd like to take a minute just sit right there, I'll tell you how I became the prince of a town called Bel-Air.", - prVelocity: 67, - }, - { - username: "JeanMeche", - name: "Matthieu Riegler", - avatarURL: "https://avatars.githubusercontent.com/u/1300985?v=4", - prs: 31, - repos: 1, - bio: "This is the story all about how my life got flipped turned upside down, and I'd like to take a minute just sit right there, I'll tell you how I became the prince of a town called Bel-Air.", - prVelocity: 67, - }, - { - username: "annacmc", - name: "Anna McPhee", - avatarURL: "https://avatars.githubusercontent.com/u/30754158?v=4", - prs: 31, - repos: 1, - bio: "This is the story all about how my life got flipped turned upside down, and I'd like to take a minute just sit right there, I'll tell you how I became the prince of a town called Bel-Air.", - prVelocity: 67, + email: "", + notification_count: 0, + insights_count: 0, + personal_workspace_id: "85be2079-5293-4482-9cd2-d420de812691", + id: 42211, + open_issues: 83, + created_at: "2008-12-23T02:43:29.000Z", + updated_at: "2024-08-09T17:24:50.173Z", + first_opened_pr_at: "2012-10-06T02:35:52.000Z", + first_pushed_commit_at: "2013-01-22T06:22:13.000Z", + connected_at: "2023-05-10T16:50:57.665Z", + campaign_start_date: "2023-05-10T16:50:57.665Z", + node_id: "MDQ6VXNlcjQyMjEx", + avatar_url: "https://avatars.githubusercontent.com/u/42211?u=af4c194c5ba48b817aab3f0ca39724e92d468e82&v=4", + gravatar_id: "", + url: "https://github.com/brandonroberts", + login: "brandonroberts", + is_private: false, + is_open_sauced_member: true, + is_onboarded: true, + is_waitlisted: false, + role: 50, + bio: "Web developer, @ngrx maintainer, creator of @analogjs, GDE, sports ranter, and gif slinger. Engineering Lead @open-sauced.", + blog: "https://brandonroberts.dev", + name: "Brandon Roberts", + twitter_username: "brandontroberts", + linkedin_url: "https://www.linkedin.com/in/brandontroberts", + github_sponsors_url: "https://github.com/sponsors/brandonroberts", + discord_url: "", + company: "@open-sauced", + location: "Alabama", + display_local_time: true, + interests: "typescript", + hireable: false, + public_repos: 307, + public_gists: 69, + type: "User", + display_email: false, + receive_collaboration: true, + receive_product_updates: true, + timezone: "Central Standard Time", + coupon_code: "", + languages: { + jq: 1282, + CSS: 11344, + EJS: 12611, + MDX: 22588, + HTML: 98065, + Shell: 8132, + Starlark: 107250, + JavaScript: 52839, + TypeScript: 4365822, + }, + highlights_count: 21, + following_count: 10, + followers_count: 15, + oscr: 269.64285714285717, + devstats_updated_at: "2024-08-09T16:00:46.000Z", + accepted_usage_terms: false, + recent_pull_request_velocity_count: 1, + recent_pull_requests_count: 51, + is_maintainer: true, + commits: 464, + prs_created: 49, + prs_reviewed: 87, + issues_created: 7, + commit_comments: 0, + issue_comments: 54, + pr_review_comments: 17, + comments: 71, + total_contributions: 607, }, ] as const; diff --git a/components/organisms/DevCardWall/dev-card-wall.stories.tsx b/components/organisms/DevCardWall/dev-card-wall.stories.tsx index 0adf5c5980..59623a9d6d 100644 --- a/components/organisms/DevCardWall/dev-card-wall.stories.tsx +++ b/components/organisms/DevCardWall/dev-card-wall.stories.tsx @@ -1,12 +1,7 @@ import type { Meta, StoryObj } from "@storybook/react"; +import { STUB_DEV_CARDS as cards } from "../DevCardCarousel/stubData"; import DevCardWall from "./dev-card-wall"; -const cards = Array.from({ length: 10 }, (_, i) => ({ - username: `test${i}`, - name: "test", - avatarURL: "https://avatars.githubusercontent.com/u/54212428?v=4", -})); - export default { title: "Design System/Organisms/DevCard Wall", component: DevCardWall, diff --git a/components/organisms/DevCardWall/dev-card-wall.tsx b/components/organisms/DevCardWall/dev-card-wall.tsx index 82108a86ce..57559aa878 100644 --- a/components/organisms/DevCardWall/dev-card-wall.tsx +++ b/components/organisms/DevCardWall/dev-card-wall.tsx @@ -3,7 +3,8 @@ import { useCallback, useEffect, useState } from "react"; import { animated, useSpring, useSprings } from "@react-spring/web"; import { useGesture } from "@use-gesture/react"; import { useOutsideClickRef } from "rooks"; -import DevCard, { DevCardProps } from "components/molecules/DevCard/dev-card"; +import DevCard from "components/molecules/DevCard/dev-card"; +import { UserDevStats } from "pages/u/[username]/card"; import Button from "components/shared/Button/button"; import ChevronLeft from "../../../img/icons/chevron-left.svg"; @@ -28,7 +29,7 @@ const coordinatesForIndex = (height: number) => (index: number) => { }; interface DevCardWallProps { - cards: DevCardProps[]; + cards: UserDevStats[]; isLoading?: boolean; initialCardIndex?: number; } @@ -235,9 +236,9 @@ export default function DevCardWall({ isLoading = false, cards, initialCardIndex zIndex, }} > - + - diff --git a/components/organisms/ToolList/nav.tsx b/components/organisms/ToolList/nav.tsx index da6fc17ce7..0c109f5821 100644 --- a/components/organisms/ToolList/nav.tsx +++ b/components/organisms/ToolList/nav.tsx @@ -70,7 +70,7 @@ const Nav: React.FC = ({ data-state={selectedTool === tool.name.toLowerCase() ? "active" : "inactive"} tabIndex={-1} key={index} - className={`tool-list-item border-b-2 transition-all ease-in-out ${ + className={`tool-list-item border-b-2 ml-2 transition-all ease-in-out ${ (selectedTool as string).toLowerCase() === tool.name.toLowerCase() ? "border-orange-500" : "border-transparent hover:border-light-slate-8" diff --git a/next-types.d.ts b/next-types.d.ts index eb8fdfa2d0..5171aeca60 100644 --- a/next-types.d.ts +++ b/next-types.d.ts @@ -530,12 +530,15 @@ interface DbListContributorStat { login: string; commits: number; prs_created: number; + prs_reviewed: number; issues_created: number; commit_comments: number; issue_comments: number; pr_review_comments: number; comments: number; total_contributions: number; + recent_pull_requests_count?: number; + recent_pull_request_velocity_count?: number; } interface DbProjectContributions { diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index f6840612e3..d4ea729b93 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1,12 +1,12 @@ { "name": "@open-sauced/app", - "version": "2.53.0", + "version": "2.54.0-beta.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@open-sauced/app", - "version": "2.53.0", + "version": "2.54.0-beta.1", "hasInstallScript": true, "license": "Apache 2.0", "dependencies": { diff --git a/package.json b/package.json index 54a33deaf2..1cb027bb0e 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@open-sauced/app", "description": "πŸ•The dashboard for open source discovery.", "keywords": [], - "version": "2.53.0", + "version": "2.54.0-beta.1", "author": "Brian Douglas ", "private": true, "license": "Apache 2.0", diff --git a/pages/404.tsx b/pages/404.tsx index 782cd05a38..f6aa437b1a 100644 --- a/pages/404.tsx +++ b/pages/404.tsx @@ -1,27 +1,30 @@ import Link from "next/link"; import { useEffect, useState } from "react"; -import { differenceInDays } from "date-fns"; import FullHeightContainer from "components/atoms/FullHeightContainer/full-height-container"; import HeaderLogo from "components/molecules/HeaderLogo/header-logo"; import { useFetchTopContributors } from "lib/hooks/useFetchTopContributors"; import DevCardWall from "components/organisms/DevCardWall/dev-card-wall"; -import { DevCardProps } from "components/molecules/DevCard/dev-card"; -import { fetchContributorPRs } from "lib/hooks/api/useContributorPullRequests"; -import { getRepoList } from "lib/hooks/useRepoList"; -import getContributorPullRequestVelocity from "lib/utils/get-contributor-pr-velocity"; -import getPercent from "lib/utils/get-percent"; -import { getAvatarByUsername } from "lib/utils/github"; import BubbleBG from "../img/bubble-bg.svg"; +import { UserDevStats } from "./u/[username]/card"; + +async function fetchUserData(username: string) { + const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/users/${username}/devstats`, { + headers: { + "Content-Type": "application/json" + } + }); + return await response.json() as UserDevStats; +}; export default function Custom404() { const { data } = useFetchTopContributors({ limit: 20 }); - const [cards, setCards] = useState([]); + const [cards, setCards] = useState([]); const [isLoading, setIsLoading] = useState(true); const [initialCardIndex, setInitialCardIndex] = useState(); useEffect(() => { async function loadCards() { - const cardData = await Promise.all(data.map((user) => getAllCardData(user.login))); + const cardData = await Promise.all(data.map((user) => fetchUserData(user.login))); // randomize cards cardData.sort(() => Math.random() - 0.5); setCards(cardData); @@ -76,58 +79,3 @@ export default function Custom404() { ); } -async function getAllCardData(username: string): Promise { - const [basicData, contributorData] = await Promise.all([ - fetchBasicCardData(username), - fetchContributorCardData(username), - ]); - - return { - ...basicData, - ...contributorData, - isLoading: false, - }; -} - -async function fetchUserData(username: string) { - const req = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/users/${username}`, { - headers: { - accept: "application/json", - }, - }); - - return (await req.json()) as DbUser; -} - -async function fetchBasicCardData(username: string): Promise { - const user = await fetchUserData(username); - const githubAvatar = getAvatarByUsername(username, 300); - - const ageInDays = user.first_opened_pr_at ? differenceInDays(new Date(), new Date(user.first_opened_pr_at)) : 0; - - return { - username, - avatarURL: githubAvatar, - name: user.name || username, - bio: user.bio, - age: ageInDays, - }; -} - -async function fetchContributorCardData( - username: string -): Promise> { - const { data, meta } = await fetchContributorPRs(username, undefined, "*", [], 100); - const prs = data.length; - const prVelocity = getContributorPullRequestVelocity(data); - const prTotal = meta.itemCount; - const mergedPrs = data.filter((prData) => prData.pr_is_merged); - const prMergePercentage = getPercent(prTotal, mergedPrs.length || 0); - const repos = getRepoList(Array.from(new Set(data.map((prData) => prData.repo_name))).join(",")).length; - return { - prs, - prVelocity, - prMergePercentage, - repos, - }; -} diff --git a/pages/u/[username]/card.tsx b/pages/u/[username]/card.tsx index c16ede84a7..bc3c13f273 100644 --- a/pages/u/[username]/card.tsx +++ b/pages/u/[username]/card.tsx @@ -1,19 +1,12 @@ -import { ParsedUrlQuery } from "querystring"; -import { GetServerSideProps, NextPage } from "next"; -import { useEffect, useState } from "react"; +import { GetServerSidePropsContext } from "next"; +import { useState } from "react"; import { useTransition, animated } from "@react-spring/web"; import Image from "next/image"; -import cntl from "cntl"; import { usePostHog } from "posthog-js/react"; +import { captureException } from "@sentry/nextjs"; import Button from "components/shared/Button/button"; import HeaderLogo from "components/molecules/HeaderLogo/header-logo"; import DevCardCarousel from "components/organisms/DevCardCarousel/dev-card-carousel"; -import { getAvatarByUsername } from "lib/utils/github"; -import { fetchContributorPRs } from "lib/hooks/api/useContributorPullRequests"; -import getContributorPullRequestVelocity from "lib/utils/get-contributor-pr-velocity"; -import getPercent from "lib/utils/get-percent"; -import { getRepoList } from "lib/hooks/useRepoList"; -import { DevCardProps } from "components/molecules/DevCard/dev-card"; import SEO from "layouts/SEO/SEO"; import useSupabaseAuth from "lib/hooks/useSupabaseAuth"; import { linkedinCardShareUrl, siteUrl, twitterCardShareUrl } from "lib/utils/urls"; @@ -35,67 +28,26 @@ const ADDITIONAL_PROFILES_TO_LOAD = [ "CBID2", ]; -interface CardProps { - username: string; - cards: DevCardProps[]; -} - -interface Params extends ParsedUrlQuery { - username: string; -} +export type UserDevStats = DbUser & DbListContributorStat; async function fetchUserData(username: string) { - if (!isValidUrlSlug(username)) { - throw new Error("Invalid input"); + try { + if (!isValidUrlSlug(username)) { + throw Error("Invalid username"); + } + + const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/users/${username}/devstats`, { + headers: { + "Content-Type": "application/json", + }, + }); + return (await response.json()) as UserDevStats; + } catch (e) { + captureException(e); } - - const req = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/users/${username}`, { - headers: { - accept: "application/json", - }, - }); - - return (await req.json()) as DbUser; } -async function fetchInitialCardData(username: string): Promise { - const user = await fetchUserData(username); - const githubAvatar = getAvatarByUsername(username, 300); - - const ageInDays = user.first_opened_pr_at - ? Math.floor((Date.now() - Date.parse(user.first_opened_pr_at)) / 86400000) - : 0; - - return { - username, - avatarURL: githubAvatar, - name: user.name || username, - bio: user.bio, - age: ageInDays, - oscr: Math.ceil(user.oscr), - isLoading: true, - }; -} - -async function fetchRemainingCardData( - username: string -): Promise> { - const { data, meta } = await fetchContributorPRs(username, undefined, "*", [], 100); - const prs = data.length; - const prVelocity = getContributorPullRequestVelocity(data); - const prTotal = meta.itemCount; - const mergedPrs = data.filter((prData) => prData.pr_is_merged); - const prMergePercentage = getPercent(prTotal, mergedPrs.length || 0); - const repos = getRepoList(Array.from(new Set(data.map((prData) => prData.repo_name))).join(",")).length; - return { - prs, - prVelocity, - prMergePercentage, - repos, - }; -} - -export const getServerSideProps: GetServerSideProps = async (context) => { +export async function getServerSideProps(context: GetServerSidePropsContext) { const username = context?.params?.username as string | undefined; if (!username) { return { @@ -104,7 +56,7 @@ export const getServerSideProps: GetServerSideProps = async ( } const uniqueUsernames = [...new Set([username, ...ADDITIONAL_PROFILES_TO_LOAD])]; - const cards = await Promise.all(uniqueUsernames.map(fetchInitialCardData)); + const cards = await Promise.all(uniqueUsernames.map(fetchUserData)); return { props: { @@ -112,9 +64,9 @@ export const getServerSideProps: GetServerSideProps = async ( cards, }, }; -}; +} -const Card: NextPage = ({ username, cards }) => { +export default function CardPage({ username, cards }: { username: string; cards: UserDevStats[] }) { const { user: loggedInUser } = useSupabaseAuth(); const [selectedUserName, setSelectedUserName] = useState(username); const iframeTransition = useTransition(selectedUserName, { @@ -123,35 +75,11 @@ const Card: NextPage = ({ username, cards }) => { leave: { opacity: 0, transform: "translate3d(100%, 0, 0)" }, }); - const [fullCardsData, setFullCardsData] = useState(cards); - const firstCard = fullCardsData.find((card) => card.username === username); + const firstCard = cards!.find((card) => card.login === username); const isViewingOwnProfile = loggedInUser?.user_metadata?.user_name === username; const socialSummary = `${firstCard?.bio || `${username} has connected their GitHub but has not added a bio.`}`; - /** - * for each of the cards we need to load additional data async because it's slow to block page load - * to fetch all of them - */ - useEffect(() => { - cards.forEach(async (card) => { - const cardData = await fetchRemainingCardData(card.username); - setFullCardsData((prev) => - prev.map((c) => { - if (c.username === card.username) { - return { - ...c, - ...cardData, - isLoading: false, - }; - } - - return c; - }) - ); - }); - }, [cards]); - return ( = ({ username, cards }) => { >
- setSelectedUserName(name)} /> + setSelectedUserName(name)} />
{isViewingOwnProfile ? ( @@ -249,9 +177,7 @@ const Card: NextPage = ({ username, cards }) => { ); -}; - -export default Card; +} function SocialButtons({ username, summary }: { username: string; summary: string }) { const posthog = usePostHog(); @@ -270,18 +196,6 @@ function SocialButtons({ username, summary }: { username: string; summary: strin }, ]; - const linkStyle = cntl` - rounded-full - w-10 - h-10 - p-2.5 - grid - place-content-center - border - hover:opacity-80 - transition-all - `; - return (
Share your DevCard
@@ -290,7 +204,7 @@ function SocialButtons({ username, summary }: { username: string; summary: strin posthog.capture("DevCard share link clicked", { platform: icon.name, username })} diff --git a/public/devcard-border.svg b/public/devcard-border.svg new file mode 100644 index 0000000000..32457d2513 --- /dev/null +++ b/public/devcard-border.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/public/devcard-gradient.png b/public/devcard-gradient.png new file mode 100644 index 0000000000..7984095500 Binary files /dev/null and b/public/devcard-gradient.png differ diff --git a/vitest.config.ts b/vitest.config.ts index 5510bafad8..fc23e2b6fa 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -17,5 +17,10 @@ export default defineConfig({ include: ["./**/*.test.ts", "./**/*.test.tsx"], globals: true, environment: "jsdom", + env: { + NEXT_PUBLIC_SUPABASE_URL: "https://fcqqkxwlntnrtjfbcioz.supabase.co", + NEXT_PUBLIC_SUPABASE_ANON_KEY: + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImZjcXFreHdsbnRucnRqZmJjaW96Iiwicm9sZSI6ImFub24iLCJpYXQiOjE2OTg0MTkyNzQsImV4cCI6MjAxMzk5NTI3NH0.ymWWYdnJC2gsnrJx4lZX2cfSOp-1xVuWFGt1Wr6zwtg", + }, }, });