From 32510d702ff20f76517bd38ef222c80778a48b83 Mon Sep 17 00:00:00 2001 From: Dominik Stumpf <122315398+dominik-stumpf@users.noreply.github.com> Date: Thu, 12 Sep 2024 12:09:01 +0200 Subject: [PATCH] add xp metagame to profile (#1473) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add progress bar and polygon * feat: implement xp ranking, leveling logic * chore: remove console.log * feat: add xp to activity card, refactor xp system * feat: add xp to activity card, refactor xp system * fix: cache experiences count on server * feat: add xp to Account and AccountModal * fix(a11y): add dialog description to modal * UI(ActivityCard): move XP to the right in gold * feat: add activity chart * feat: add tooltip to chart * UI(ProfileHero): avatar level indicator refinements * feat: style tooltip and group xp entries * chore: adjust barchart size * fix: size experience card with badge * refactor: resize RewardBadge * refactor: add progress ui component * fix: assign rank properly on profile * chore: floor levels for simplicity * chore: revalidate experiences every 20 min * fix: Experience & Top contributions title colors in light mode * UI: whitespace refinement * chore: add fallback to activity chart * UI(experiences): impros/fixes * fix: reorder useEffect calls, adjust startTime * UI: account modal & progress refinements * cleanup(LevelBadge): duplicated text style * copy(ActivityChart): fallback fix * ActivityChart batching / fallback rendering impro --------- Co-authored-by: valid --- package-lock.json | 8 +- package.json | 2 +- .../profile/[username]/constants.ts | 20 +++ .../(marketing)/profile/[username]/page.tsx | 21 +++ .../(marketing)/profile/[username]/types.ts | 3 + .../profile/_components/ActivityChart.tsx | 166 ++++++++++++++++++ .../profile/_components/LevelBadge.tsx | 41 +++++ .../profile/_components/Profile.tsx | 47 ++++- .../profile/_components/ProfileHero.tsx | 32 +++- .../_components/ProfileSocialCounters.tsx | 2 +- .../RecentActivity/ActivityCard.tsx | 40 +++-- .../_hooks/useExperienceProgression.ts | 37 ++++ .../profile/_hooks/useExperiences.tsx | 22 +++ .../(marketing)/profile/_hooks/useProfile.tsx | 7 +- src/app/globals.css | 3 +- src/components/[guild]/activity/constants.ts | 1 + .../requirementDisplayComponents.ts | 1 + src/requirements/requirementFormComponents.ts | 1 + src/requirements/types.ts | 4 +- src/v2/components/Account/Account.tsx | 30 +++- .../components/AccountModal/AccountModal.tsx | 41 ++++- src/v2/components/CircularProgressBar.tsx | 60 +++++++ src/v2/components/Polygon.tsx | 33 ++++ src/v2/components/ui/Badge.tsx | 1 + src/v2/components/ui/Progress.tsx | 29 ++- 25 files changed, 592 insertions(+), 60 deletions(-) create mode 100644 src/app/(marketing)/profile/[username]/constants.ts create mode 100644 src/app/(marketing)/profile/[username]/types.ts create mode 100644 src/app/(marketing)/profile/_components/ActivityChart.tsx create mode 100644 src/app/(marketing)/profile/_components/LevelBadge.tsx create mode 100644 src/app/(marketing)/profile/_hooks/useExperienceProgression.ts create mode 100644 src/app/(marketing)/profile/_hooks/useExperiences.tsx create mode 100644 src/v2/components/CircularProgressBar.tsx create mode 100644 src/v2/components/Polygon.tsx diff --git a/package-lock.json b/package-lock.json index 81a74a5162..e72865ce53 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,7 @@ "@emotion/styled": "^11.11.0", "@fuels/connectors": "^0.5.0", "@fuels/react": "^0.20.0", - "@guildxyz/types": "^1.10.8", + "@guildxyz/types": "^1.10.10", "@hcaptcha/react-hcaptcha": "^1.4.4", "@hookform/resolvers": "^3.3.4", "@lexical/code": "^0.12.0", @@ -5393,9 +5393,9 @@ } }, "node_modules/@guildxyz/types": { - "version": "1.10.8", - "resolved": "https://registry.npmjs.org/@guildxyz/types/-/types-1.10.8.tgz", - "integrity": "sha512-oTzLxdFQ3UFXHgsx/xjtIMgb5k9maCsdVlvKZjw/nqY3lYU9UK3DmnPEiiahCVple3zrDE41IgqZqR9McH520A==", + "version": "1.10.10", + "resolved": "https://registry.npmjs.org/@guildxyz/types/-/types-1.10.10.tgz", + "integrity": "sha512-xZjqVSExhp1XHQRsTh4O06GmEf7mGqTejJaNdmFNeNqTKXWIE2jHo0cEeOXavLg6k4BFKx5wC9y4+b6VsAMQBg==", "dependencies": { "zod": "^3.22.4" } diff --git a/package.json b/package.json index 626231b0ef..721238c501 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "@emotion/styled": "^11.11.0", "@fuels/connectors": "^0.5.0", "@fuels/react": "^0.20.0", - "@guildxyz/types": "^1.10.8", + "@guildxyz/types": "^1.10.10", "@hcaptcha/react-hcaptcha": "^1.4.4", "@hookform/resolvers": "^3.3.4", "@lexical/code": "^0.12.0", diff --git a/src/app/(marketing)/profile/[username]/constants.ts b/src/app/(marketing)/profile/[username]/constants.ts new file mode 100644 index 0000000000..d6b4a3b15c --- /dev/null +++ b/src/app/(marketing)/profile/[username]/constants.ts @@ -0,0 +1,20 @@ +export const MAX_LEVEL = 100 +export const MAX_XP = 11e4 +export const RANKS = [ + { color: "#78c93d", title: "novice", polygonCount: 20 }, + { color: "#88d525", title: "learner", polygonCount: 20 }, + { color: "#f6ca45", title: "knight", polygonCount: 4 }, + { color: "#f19b38", title: "veteran", polygonCount: 4 }, + { color: "#ec5a53", title: "champion", polygonCount: 4 }, + { color: "#53adf0", title: "hero", polygonCount: 5 }, + { color: "#c385f8", title: "master", polygonCount: 5 }, + { color: "#3e6fc3", title: "grand master", polygonCount: 5 }, + { color: "#be4681", title: "legend", polygonCount: 6 }, + { color: "#000000", title: "mythic", polygonCount: 6 }, + { + color: "#eeeeee", + title: "???", + requiredXp: 1e19, + polygonCount: 6, + }, +] as const diff --git a/src/app/(marketing)/profile/[username]/page.tsx b/src/app/(marketing)/profile/[username]/page.tsx index 9254fea7d8..fe8768b1ae 100644 --- a/src/app/(marketing)/profile/[username]/page.tsx +++ b/src/app/(marketing)/profile/[username]/page.tsx @@ -158,6 +158,22 @@ const fetchPublicProfileData = async ({ : [] const guildsZipped = guildRequests.map(({ pathname }, i) => [pathname, guilds[i]]) const rolesZipped = roleRequests.map(({ pathname }, i) => [pathname, roles[i]]) + const experiencesRequest = new URL(`/v2/profiles/${username}/experiences`, api) + const experiences = await ssrFetcher(experiencesRequest, { + next: { + revalidate: 1200, + }, + }) + const experienceCountRequest = new URL( + `/v2/profiles/${username}/experiences?count=true`, + api + ) + const experienceCount = await ssrFetcher(experienceCountRequest, { + next: { + revalidate: 1200, + }, + }) + return { profile, fallback: Object.fromEntries( @@ -167,6 +183,11 @@ const fetchPublicProfileData = async ({ [farcasterProfilesRequest.pathname, farcasterProfiles], [neynarRequest?.href, fcFollowers], [referredUsersRequest.pathname, referredUsers], + [experiencesRequest.pathname, experiences], + [ + experienceCountRequest.pathname + experienceCountRequest.search, + experienceCount, + ], ...collectionsZipped, ...guildsZipped, ...rolesZipped, diff --git a/src/app/(marketing)/profile/[username]/types.ts b/src/app/(marketing)/profile/[username]/types.ts new file mode 100644 index 0000000000..f587d90b1f --- /dev/null +++ b/src/app/(marketing)/profile/[username]/types.ts @@ -0,0 +1,3 @@ +import { RANKS } from "./constants" + +export type Rank = (typeof RANKS)[number] diff --git a/src/app/(marketing)/profile/_components/ActivityChart.tsx b/src/app/(marketing)/profile/_components/ActivityChart.tsx new file mode 100644 index 0000000000..b1c2a7bb46 --- /dev/null +++ b/src/app/(marketing)/profile/_components/ActivityChart.tsx @@ -0,0 +1,166 @@ +import { Skeleton } from "@/components/ui/Skeleton" +import { Schemas } from "@guildxyz/types" +import { localPoint } from "@visx/event" +import { Group } from "@visx/group" +import ParentSize from "@visx/responsive/lib/components/ParentSize" +import { scaleBand, scaleLinear } from "@visx/scale" +import { Bar } from "@visx/shape" +import { useTooltip, useTooltipInPortal } from "@visx/tooltip" +import { useMemo } from "react" +import { useExperienceProgression } from "../_hooks/useExperienceProgression" +import { useExperiences } from "../_hooks/useExperiences" + +type TooltipData = Schemas["Experience"] +const verticalMargin = 0 + +const getX = (xp: Schemas["Experience"]) => xp.id.toString() +const getY = (xp: Schemas["Experience"]) => xp.amount + +export type BarsProps = { + width: number + height: number +} + +let tooltipTimeout: number + +const ActivityChartChildren = ({ + width, + height, + rawData, +}: BarsProps & { + rawData: Schemas["Experience"][] +}) => { + const { + tooltipOpen, + tooltipLeft, + tooltipTop, + tooltipData, + hideTooltip, + showTooltip, + } = useTooltip() + + const { containerRef, TooltipInPortal } = useTooltipInPortal({ + scroll: true, + }) + const xp = useExperienceProgression() + const groupedData = new Map() + for (const rawXp of rawData) { + const createdAt = new Date(rawXp.createdAt) + const commonDay = new Date( + createdAt.getFullYear(), + createdAt.getMonth(), + createdAt.getDate() + ).valueOf() + groupedData.set(commonDay, [...(groupedData.get(commonDay) ?? []), rawXp]) + } + const data = [...groupedData.entries()] + .reduce((acc, [_, xpGroup]) => { + return [ + ...acc, + { + ...xpGroup[0], + amount: xpGroup.reduce((sumAcc, xp) => sumAcc + xp.amount, 0), + }, + ] + }, []) + .sort( + (a, b) => new Date(a.createdAt).valueOf() - new Date(b.createdAt).valueOf() + ) + + const xMax = width + const yMax = height - verticalMargin + const xScale = useMemo( + () => + scaleBand({ + range: [0, Math.min(data.length * 18, xMax)], + round: true, + domain: data.map(getX), + padding: 0.4, + }), + [xMax] + ) + const yScale = useMemo( + () => + scaleLinear({ + range: [yMax, 0], + round: true, + domain: [0, Math.max(...data.map(getY))], + }), + [yMax] + ) + + return width < 10 ? null : ( +
+ + + {data.map((currentXp) => { + const x = getX(currentXp) + const barWidth = xScale.bandwidth() + const barHeight = yMax - (yScale(getY(currentXp)) ?? 0) + const barX = xScale(x) + const barY = yMax - barHeight + return ( + { + tooltipTimeout = window.setTimeout(() => { + hideTooltip() + }, 300) + }} + onMouseMove={(event) => { + if (tooltipTimeout) clearTimeout(tooltipTimeout) + const eventSvgCoords = localPoint(event) + const left = (barX || 0) + barWidth / 2 + showTooltip({ + tooltipData: currentXp, + tooltipTop: eventSvgCoords?.y, + tooltipLeft: left, + }) + }} + /> + ) + })} + + + {tooltipOpen && tooltipData && ( + + +{tooltipData.amount} XP +
+ {new Date(tooltipData.createdAt).toLocaleDateString()} +
+
+ )} +
+ ) +} + +export const ActivityChart = () => { + const { data: rawData } = useExperiences({ count: false }) + + if (!rawData) return + + if (rawData.length === 0) + return

There's no activity this month

+ + return ( +
+ + {({ width, height }) => ( + + )} + +
+ ) +} diff --git a/src/app/(marketing)/profile/_components/LevelBadge.tsx b/src/app/(marketing)/profile/_components/LevelBadge.tsx new file mode 100644 index 0000000000..05f733369f --- /dev/null +++ b/src/app/(marketing)/profile/_components/LevelBadge.tsx @@ -0,0 +1,41 @@ +import { Polygon } from "@/components/Polygon" +import { VariantProps, cva } from "class-variance-authority" +import { Rank } from "../[username]/types" + +const levelBadgeVariants = cva("flex items-center justify-center", { + variants: { + size: { + md: "size-7 text-xs", + lg: "text-lg md:text-xl size-10 md:size-12", + }, + }, + defaultVariants: { + size: "md", + }, +}) + +type LevelBadgeProps = { + levelIndex: number + rank: Rank + className?: string +} & VariantProps + +export const LevelBadge = ({ + rank, + levelIndex, + size, + className, +}: LevelBadgeProps) => { + return ( +
+ + + {levelIndex} + +
+ ) +} diff --git a/src/app/(marketing)/profile/_components/Profile.tsx b/src/app/(marketing)/profile/_components/Profile.tsx index 1aa2edd213..69f57b5b48 100644 --- a/src/app/(marketing)/profile/_components/Profile.tsx +++ b/src/app/(marketing)/profile/_components/Profile.tsx @@ -1,6 +1,7 @@ "use client" import { useWeb3ConnectionManager } from "@/components/Web3ConnectionManager/hooks/useWeb3ConnectionManager" import { Card } from "@/components/ui/Card" +import { ProgressIndicator, ProgressRoot } from "@/components/ui/Progress" import { cn } from "@/lib/utils" import { Info } from "@phosphor-icons/react" import { PropsWithChildren } from "react" @@ -8,8 +9,11 @@ import { ContributionCard } from "../_components/ContributionCard" import { EditContributions } from "../_components/EditContributions" import { ProfileOwnerGuard } from "../_components/ProfileOwnerGuard" import { useContributions } from "../_hooks/useContributions" +import { useExperienceProgression } from "../_hooks/useExperienceProgression" import { useProfile } from "../_hooks/useProfile" import { useReferredUsers } from "../_hooks/useReferredUsers" +import { ActivityChart } from "./ActivityChart" +import { LevelBadge } from "./LevelBadge" import { ProfileMainSkeleton } from "./ProfileSkeleton" import { RecentActivity } from "./RecentActivity/RecentActivity" import RecentActivityFallback from "./RecentActivity/RecentActivityFallback" @@ -19,12 +23,49 @@ export const Profile = () => { const { data: contributions } = useContributions() const { data: referredUsers } = useReferredUsers() const { isWeb3Connected } = useWeb3ConnectionManager() + const xp = useExperienceProgression() - if (!profile || !contributions || !referredUsers) return + if (!profile || !contributions || !referredUsers || !xp) + return return ( <> -
+
+
+ Experience +
+
+ + +
+
+

{xp.rank.title}

+

+ {`${xp.experienceCount} / ${xp.level} XP`} +

+
+ + + +
+
+ +
+

Engagement this month

+
+ +
+
+
+
Top contributions @@ -48,7 +89,7 @@ export const Profile = () => { ))}
-
+
Recent activity {isWeb3Connected ? : }
diff --git a/src/app/(marketing)/profile/_components/ProfileHero.tsx b/src/app/(marketing)/profile/_components/ProfileHero.tsx index 1d7e37e751..ac2f012432 100644 --- a/src/app/(marketing)/profile/_components/ProfileHero.tsx +++ b/src/app/(marketing)/profile/_components/ProfileHero.tsx @@ -1,5 +1,7 @@ "use client" + import { CheckMark } from "@/components/CheckMark" +import { CircularProgressBar } from "@/components/CircularProgressBar" import { LayoutContainer } from "@/components/Layout" import { ProfileAvatar } from "@/components/ProfileAvatar" import { Avatar } from "@/components/ui/Avatar" @@ -7,15 +9,19 @@ import { Button } from "@/components/ui/Button" import { Card } from "@/components/ui/Card" import { Pencil } from "@phosphor-icons/react" import { ProfileOwnerGuard } from "../_components/ProfileOwnerGuard" +import { useExperienceProgression } from "../_hooks/useExperienceProgression" import { useProfile } from "../_hooks/useProfile" import { EditProfile } from "./EditProfile/EditProfile" +import { LevelBadge } from "./LevelBadge" import { ProfileHeroSkeleton } from "./ProfileSkeleton" import { ProfileSocialCounters } from "./ProfileSocialCounters" export const ProfileHero = () => { const { data: profile } = useProfile() + const xp = useExperienceProgression() - if (!profile) return + if (!profile || !xp) return + const { rank, levelIndex, progress } = xp return ( @@ -36,12 +42,26 @@ export const ProfileHero = () => { - - + + + + + - +

{profile.name || profile.username} diff --git a/src/app/(marketing)/profile/_components/ProfileSocialCounters.tsx b/src/app/(marketing)/profile/_components/ProfileSocialCounters.tsx index 4c5c08f925..6b8bc23714 100644 --- a/src/app/(marketing)/profile/_components/ProfileSocialCounters.tsx +++ b/src/app/(marketing)/profile/_components/ProfileSocialCounters.tsx @@ -5,6 +5,7 @@ import { Skeleton } from "@/components/ui/Skeleton" import { cn } from "@/lib/utils" import { PropsWithChildren } from "react" import { RequiredFields } from "types" +import pluralize from "utils/pluralize" import { User, useFarcasterProfile, @@ -12,7 +13,6 @@ import { } from "../_hooks/useFarcasterProfile" import { useProfile } from "../_hooks/useProfile" import { useReferredUsers } from "../_hooks/useReferredUsers" -import pluralize from "utils/pluralize" type DisplayableUser = RequiredFields diff --git a/src/app/(marketing)/profile/_components/RecentActivity/ActivityCard.tsx b/src/app/(marketing)/profile/_components/RecentActivity/ActivityCard.tsx index c82e188c9b..7bd074763b 100644 --- a/src/app/(marketing)/profile/_components/RecentActivity/ActivityCard.tsx +++ b/src/app/(marketing)/profile/_components/RecentActivity/ActivityCard.tsx @@ -1,6 +1,7 @@ "use client" import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/Avatar" +import { Badge } from "@/components/ui/Badge" import { Card } from "@/components/ui/Card" import { cn } from "@/lib/utils" import { Guild } from "@guildxyz/types" @@ -60,23 +61,30 @@ export const ActivityCard = ({ activity }: { activity: ActivityLogAction }) => {

)} -
-

- -

-
- -

- {(() => { - const since = Date.now() - parseInt(activity.timestamp) - const sinceMinutes = since / MINUTE_IN_MS - return sinceMinutes === 0 - ? "Just now" - : `${formatRelativeTimeFromNow(since)} ago` - })()} -

-
+
+
+

+ +

+
+ +

+ {(() => { + const since = Date.now() - parseInt(activity.timestamp) + const sinceMinutes = since / MINUTE_IN_MS + return sinceMinutes === 0 + ? "Just now" + : `${formatRelativeTimeFromNow(since)} ago` + })()} +

+
+
+ {activity.xpAmount && ( + + +{activity.xpAmount} XP + + )}
) diff --git a/src/app/(marketing)/profile/_hooks/useExperienceProgression.ts b/src/app/(marketing)/profile/_hooks/useExperienceProgression.ts new file mode 100644 index 0000000000..20aa51f427 --- /dev/null +++ b/src/app/(marketing)/profile/_hooks/useExperienceProgression.ts @@ -0,0 +1,37 @@ +import { MAX_LEVEL, MAX_XP, RANKS } from "../[username]/constants" +import { useExperiences } from "../_hooks/useExperiences" + +const generateExponentialArray = ( + steps: number, + sum: number, + exponent: number +): number[] => { + const baseSum = (Math.pow(exponent, steps) - 1) / (exponent - 1) + const scaleFactor = sum / baseSum + return Array.from({ length: steps }, (_, i) => Math.pow(exponent, i) * scaleFactor) +} + +export const calculateXpProgression = ({ + experienceCount, +}: { experienceCount: number }) => { + const levels = generateExponentialArray(MAX_LEVEL, MAX_XP, 1.03).map((num) => + Math.floor(num) + ) + let levelIndex = levels.findIndex((level) => experienceCount < level) + levelIndex = levelIndex === -1 ? levels.length - 1 : levelIndex + const level = levels.at(levelIndex) + const levelInRank = Math.floor(MAX_LEVEL / RANKS.length) + const rankIndex = Math.max(0, Math.floor((levelIndex - 1) / levelInRank)) + const rank = RANKS.at(rankIndex) + if (!rank || !level) throw new Error("failed to calculate rank") + const nextLevel = levels.at(levelIndex + 1) + const progress = experienceCount / (nextLevel || experienceCount) + return { progress, rank, levelIndex, experienceCount, level } +} + +export const useExperienceProgression = (showOwnProfile?: boolean) => { + const { data: experienceCount } = useExperiences({ showOwnProfile, count: true }) + return typeof experienceCount === "number" + ? calculateXpProgression({ experienceCount }) + : undefined +} diff --git a/src/app/(marketing)/profile/_hooks/useExperiences.tsx b/src/app/(marketing)/profile/_hooks/useExperiences.tsx new file mode 100644 index 0000000000..2a64013e7e --- /dev/null +++ b/src/app/(marketing)/profile/_hooks/useExperiences.tsx @@ -0,0 +1,22 @@ +import { Schemas } from "@guildxyz/types" +import useSWRImmutable from "swr/immutable" +import { useProfile } from "./useProfile" + +export const useExperiences = ({ + count, + showOwnProfile, + startTime, +}: { count: T; showOwnProfile?: boolean; startTime?: number }) => { + const { data: profile } = useProfile(showOwnProfile) + const params = new URLSearchParams( + [ + ["count", count && count.toString()], + ["startTime", startTime && startTime.toString()], + ].filter(([_, value]) => value) as string[][] + ) + return useSWRImmutable( + profile + ? `/v2/profiles/${profile.username}/experiences?${params.toString()}` + : null + ) +} diff --git a/src/app/(marketing)/profile/_hooks/useProfile.tsx b/src/app/(marketing)/profile/_hooks/useProfile.tsx index 5b1634e3fc..a4146c1be6 100644 --- a/src/app/(marketing)/profile/_hooks/useProfile.tsx +++ b/src/app/(marketing)/profile/_hooks/useProfile.tsx @@ -1,10 +1,13 @@ import { Schemas } from "@guildxyz/types" +import useUser from "components/[guild]/hooks/useUser" import { useParams } from "next/navigation" import useSWRImmutable from "swr/immutable" -export const useProfile = () => { +export const useProfile = (showOwnProfile?: boolean) => { const params = useParams<{ username: string }>() + const user = useUser() + const username = showOwnProfile ? user.guildProfile?.username : params?.username return useSWRImmutable( - params?.username ? `/v2/profiles/${params.username}` : null + username ? `/v2/profiles/${username}` : null ) } diff --git a/src/app/globals.css b/src/app/globals.css index de1904e8dd..10896b5ba3 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -92,6 +92,7 @@ /* we plan to add the whole alpha color palettes */ --whiteAlpha-800: rgba(255, 255, 255, 0.80); --blackAlpha-800: rgba(0, 0, 0, 0.80); + --blackAlpha-300: rgba(0, 0, 0, 0.30); --discord: 233 78% 63%; --discord-hover: 243 75% 59%; @@ -129,7 +130,7 @@ --gather_town: 232, 66%, 55%; --cyan: 190, 100%, 42%; --blue: 217, 91%, 60%; - --gold: 35, 39%, 60%; + --gold: 40 67% 51%; --gray: 240, 4%, 46%; --sm: 640px; diff --git a/src/components/[guild]/activity/constants.ts b/src/components/[guild]/activity/constants.ts index 09e6218c5d..e78efde1be 100644 --- a/src/components/[guild]/activity/constants.ts +++ b/src/components/[guild]/activity/constants.ts @@ -126,6 +126,7 @@ export type ActivityLogAction = { rolePlatform?: number } children?: Array + xpAmount?: number } export const activityLogActionIcons: Record< diff --git a/src/requirements/requirementDisplayComponents.ts b/src/requirements/requirementDisplayComponents.ts index 9a6f2a952e..cced28d562 100644 --- a/src/requirements/requirementDisplayComponents.ts +++ b/src/requirements/requirementDisplayComponents.ts @@ -374,4 +374,5 @@ export const REQUIREMENT_DISPLAY_COMPONENTS = { PARALLEL_TRAIT: dynamic( () => import("requirements/Parallel/ParallelRequirement") ), + // @ts-ignore: TODO: migrate to backend types to resolve error } as const satisfies Record> diff --git a/src/requirements/requirementFormComponents.ts b/src/requirements/requirementFormComponents.ts index ca398b81fb..da16f76f53 100644 --- a/src/requirements/requirementFormComponents.ts +++ b/src/requirements/requirementFormComponents.ts @@ -362,6 +362,7 @@ export const REQUIREMENT_FORM_COMPONENTS = { PARALLEL_TRAIT: dynamic( () => import("requirements/Parallel/ParallelForm") ), + // @ts-ignore: TODO: migrate to backend types to resolve error } as const satisfies Record< RequirementType, ReturnType> | null diff --git a/src/requirements/types.ts b/src/requirements/types.ts index 18d1d07bfa..742372869f 100644 --- a/src/requirements/types.ts +++ b/src/requirements/types.ts @@ -3,7 +3,9 @@ import { Icon } from "@phosphor-icons/react" import { UseControllerProps } from "react-hook-form" import { Requirement } from "types" -export type RequirementType = Schemas["Requirement"]["type"] +export type RequirementType = + | Schemas["Requirement"]["type"] + | "WORLD_ID_VERIFICATION" export type RequirementFormProps = { baseFieldPath: string diff --git a/src/v2/components/Account/Account.tsx b/src/v2/components/Account/Account.tsx index e41649c44f..714b4f61db 100644 --- a/src/v2/components/Account/Account.tsx +++ b/src/v2/components/Account/Account.tsx @@ -1,7 +1,9 @@ "use client" +import { CircularProgressBar } from "@/components/CircularProgressBar" import { useDisclosure } from "@/hooks/useDisclosure" import { cn } from "@/lib/utils" +import { useExperienceProgression } from "@app/(marketing)/profile/_hooks/useExperienceProgression" import { Bell } from "@phosphor-icons/react" import { SignIn } from "@phosphor-icons/react/dist/ssr" import useUser from "components/[guild]/hooks/useUser" @@ -30,6 +32,7 @@ export const Account = () => { const { addresses, guildProfile, isLoading } = useUser() const linkedAddressesCount = (addresses?.length ?? 1) - 1 const { captureEvent } = usePostHogContext() + const xp = useExperienceProgression(true) if (isLoading || isWeb3Connected === null) { return ( @@ -80,19 +83,30 @@ export const Account = () => { className="rounded-r-2xl rounded-l-none" > {guildProfile ? ( -
- - - +
+
+ {xp && ( + + )} + + + +
{guildProfile.name || guildProfile.username}
- @{guildProfile.username} + {xp + ? `${xp.experienceCount} / ${xp.level} XP` + : `@${guildProfile.username}`}
diff --git a/src/v2/components/Account/components/AccountModal/AccountModal.tsx b/src/v2/components/Account/components/AccountModal/AccountModal.tsx index 5a3a04fa3c..b98c62c874 100644 --- a/src/v2/components/Account/components/AccountModal/AccountModal.tsx +++ b/src/v2/components/Account/components/AccountModal/AccountModal.tsx @@ -1,4 +1,5 @@ import { CheckMark } from "@/components/CheckMark" +import { CircularProgressBar } from "@/components/CircularProgressBar" import { CopyableAddress } from "@/components/CopyableAddress" import { GuildAvatar } from "@/components/GuildAvatar" import { ProfileAvatar } from "@/components/ProfileAvatar" @@ -13,6 +14,7 @@ import { DialogBody, DialogCloseButton, DialogContent, + DialogDescription, DialogHeader, DialogTitle, } from "@/components/ui/Dialog" @@ -26,6 +28,8 @@ import { import { Separator } from "@/components/ui/Separator" import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/Tooltip" import { useUserPublic } from "@/hooks/useUserPublic" +import { LevelBadge } from "@app/(marketing)/profile/_components/LevelBadge" +import { useExperienceProgression } from "@app/(marketing)/profile/_hooks/useExperienceProgression" import { ArrowRight, DotsThreeVertical } from "@phosphor-icons/react" import { SignOut } from "@phosphor-icons/react/dist/ssr" import useUser from "components/[guild]/hooks/useUser" @@ -48,6 +52,7 @@ const AccountModal = () => { const { address: evmAddress, chainId } = useAccount() const domain = useResolveAddress(evmAddress) + const xp = useExperienceProgression(true) const handleLogout = () => { const keysToRemove = Object.keys({ ...window.localStorage }).filter((key) => @@ -74,6 +79,7 @@ const AccountModal = () => { Account + @@ -81,13 +87,30 @@ const AccountModal = () => { <> {guildProfile ? (
- - - -
+ {xp && ( +
+ + + + + +
+ )} +

{guildProfile.name || guildProfile.username} @@ -95,7 +118,9 @@ const AccountModal = () => {

- @{guildProfile.username} + {xp + ? `${xp.experienceCount} / ${xp.level} XP` + : `@${guildProfile.username}`}
= ({ + strokeWidth = 6, + progress, + color, + bgColor, + className, +}) => { + const size = 128 + const radius = (size - strokeWidth) / 2 + const circumference = 2 * Math.PI * radius + const offset = circumference - progress * circumference + + return ( + + + + + ) +} diff --git a/src/v2/components/Polygon.tsx b/src/v2/components/Polygon.tsx new file mode 100644 index 0000000000..a6ccff948c --- /dev/null +++ b/src/v2/components/Polygon.tsx @@ -0,0 +1,33 @@ +import { cn } from "@/lib/utils" +import { PropsWithChildren } from "react" + +type PolygonProps = PropsWithChildren<{ + sides: number + color?: string + className?: string +}> + +export const Polygon = ({ sides, color, className }: PolygonProps) => { + const angleStep = (2 * Math.PI) / sides + const radius = 38 + const points = Array.from({ length: sides }, (_, i) => { + const x = 50 + radius * Math.cos(i * angleStep) + const y = 50 + radius * Math.sin(i * angleStep) + return [x, y].map((coord) => coord.toFixed(5)).join(",") + }).join(" ") + + return ( + + + + ) +} diff --git a/src/v2/components/ui/Badge.tsx b/src/v2/components/ui/Badge.tsx index 47fee88b80..2818a1391e 100644 --- a/src/v2/components/ui/Badge.tsx +++ b/src/v2/components/ui/Badge.tsx @@ -13,6 +13,7 @@ const badgeVariants = cva( colorScheme: { gray: "[--badge-bg:var(--secondary-subtle)] [--badge-color:var(--secondary-subtle-foreground)]", blue: "[--badge-bg:var(--info-subtle)] [--badge-color:var(--info-subtle-foreground)]", + yellow: "[--badge-bg:var(--gold)] [--badge-color:var(--gold)]", }, size: { sm: "text-xs h-5", diff --git a/src/v2/components/ui/Progress.tsx b/src/v2/components/ui/Progress.tsx index da5f1c8694..1897978e28 100644 --- a/src/v2/components/ui/Progress.tsx +++ b/src/v2/components/ui/Progress.tsx @@ -5,7 +5,7 @@ import * as React from "react" import { cn } from "@/lib/utils" -const Progress = React.forwardRef< +const ProgressRoot = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, value, ...props }, ref) => ( @@ -16,13 +16,24 @@ const Progress = React.forwardRef< className )} {...props} - > - - + /> )) -Progress.displayName = ProgressPrimitive.Root.displayName -export { Progress } +const ProgressIndicator = React.forwardRef< + React.ElementRef< + React.FunctionComponent + >, + ProgressPrimitive.ProgressIndicatorProps & { value: number } +>(({ className, value, style, ...props }, ref) => ( + +)) + +export { ProgressIndicator, ProgressRoot }