Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add xp metagame to profile #1473

Merged
merged 35 commits into from
Sep 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
b29f846
feat: add progress bar and polygon
dominik-stumpf Sep 9, 2024
31d88dc
feat: implement xp ranking, leveling logic
dominik-stumpf Sep 9, 2024
8cbeefd
chore: remove console.log
dominik-stumpf Sep 9, 2024
326b104
feat: add xp to activity card, refactor xp system
dominik-stumpf Sep 10, 2024
894ecf2
feat: add xp to activity card, refactor xp system
dominik-stumpf Sep 10, 2024
09576e6
fix: cache experiences count on server
dominik-stumpf Sep 10, 2024
e84b62e
feat: add xp to Account and AccountModal
dominik-stumpf Sep 10, 2024
7e2d9db
fix(a11y): add dialog description to modal
dominik-stumpf Sep 10, 2024
2b16189
UI(ActivityCard): move XP to the right in gold
dovalid Sep 10, 2024
8f0f3b2
feat: add activity chart
dominik-stumpf Sep 10, 2024
6de76cf
Merge branch 'add-xp-metagame-to-profile' of github.com:guildxyz/guil…
dominik-stumpf Sep 10, 2024
92df919
feat: add tooltip to chart
dominik-stumpf Sep 10, 2024
6d60d95
UI(ProfileHero): avatar level indicator refinements
dovalid Sep 10, 2024
3818ef3
feat: style tooltip and group xp entries
dominik-stumpf Sep 10, 2024
2259d51
Merge branch 'add-xp-metagame-to-profile' of github.com:guildxyz/guil…
dominik-stumpf Sep 10, 2024
f6e7e0e
chore: adjust barchart size
dominik-stumpf Sep 11, 2024
7f45f03
fix: size experience card with badge
dominik-stumpf Sep 11, 2024
fdc5f6c
refactor: resize RewardBadge
dominik-stumpf Sep 11, 2024
b745f7b
refactor: add progress ui component
dominik-stumpf Sep 11, 2024
5d380a4
Merge branch 'profiles-page' of github.com:guildxyz/guild.xyz into ad…
dominik-stumpf Sep 11, 2024
23c4c8c
fix: assign rank properly on profile
dominik-stumpf Sep 11, 2024
82290e2
chore: floor levels for simplicity
dominik-stumpf Sep 11, 2024
00d3ffd
chore: revalidate experiences every 20 min
dominik-stumpf Sep 11, 2024
36e1b49
chore: merge profiles-page branch
dominik-stumpf Sep 11, 2024
e4a9a7d
fix: Experience & Top contributions title colors in light mode
dovalid Sep 11, 2024
8cd810d
UI: whitespace refinement
dovalid Sep 11, 2024
b74d0de
chore: add fallback to activity chart
dominik-stumpf Sep 11, 2024
d5adb06
UI(experiences): impros/fixes
dovalid Sep 11, 2024
1bf412a
Merge branch 'add-xp-metagame-to-profile' of https://github.com/guild…
dovalid Sep 11, 2024
46661e7
Merge branch 'add-xp-metagame-to-profile' of github.com:guildxyz/guil…
dominik-stumpf Sep 11, 2024
74ef133
fix: reorder useEffect calls, adjust startTime
dominik-stumpf Sep 11, 2024
8dc278d
UI: account modal & progress refinements
dovalid Sep 12, 2024
c8aebdc
cleanup(LevelBadge): duplicated text style
dovalid Sep 12, 2024
8c76f50
copy(ActivityChart): fallback fix
dovalid Sep 12, 2024
caba6ee
ActivityChart batching / fallback rendering impro
dovalid Sep 12, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
20 changes: 20 additions & 0 deletions src/app/(marketing)/profile/[username]/constants.ts
Original file line number Diff line number Diff line change
@@ -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
21 changes: 21 additions & 0 deletions src/app/(marketing)/profile/[username]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<Schemas["Experience"][]>(experiencesRequest, {
next: {
revalidate: 1200,
},
})
const experienceCountRequest = new URL(
`/v2/profiles/${username}/experiences?count=true`,
api
)
const experienceCount = await ssrFetcher<number>(experienceCountRequest, {
next: {
revalidate: 1200,
},
})

return {
profile,
fallback: Object.fromEntries(
Expand All @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions src/app/(marketing)/profile/[username]/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { RANKS } from "./constants"

export type Rank = (typeof RANKS)[number]
166 changes: 166 additions & 0 deletions src/app/(marketing)/profile/_components/ActivityChart.tsx
Original file line number Diff line number Diff line change
@@ -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<TooltipData>()

const { containerRef, TooltipInPortal } = useTooltipInPortal({
scroll: true,
})
const xp = useExperienceProgression()
const groupedData = new Map<number, Schemas["Experience"][]>()
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<Schemas["Experience"][]>((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<string>({
range: [0, Math.min(data.length * 18, xMax)],
round: true,
domain: data.map(getX),
padding: 0.4,
}),
[xMax]
)
const yScale = useMemo(
() =>
scaleLinear<number>({
range: [yMax, 0],
round: true,
domain: [0, Math.max(...data.map(getY))],
}),
[yMax]
)

return width < 10 ? null : (
<div className="relative">
<svg width={width} height={height} ref={containerRef}>
<Group top={verticalMargin / 2}>
{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 (
<Bar
ry={4}
key={currentXp.id}
x={barX}
y={barY}
width={barWidth}
height={barHeight}
fill={xp?.rank.color}
onMouseLeave={() => {
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,
})
}}
/>
)
})}
</Group>
</svg>
{tooltipOpen && tooltipData && (
<TooltipInPortal
top={tooltipTop}
left={tooltipLeft}
unstyled
applyPositionStyle
className="rounded border bg-card px-2 py-1 text-sm"
>
<strong>+{tooltipData.amount} XP</strong>
<div className="text-muted-foreground">
{new Date(tooltipData.createdAt).toLocaleDateString()}
</div>
</TooltipInPortal>
)}
</div>
)
}

export const ActivityChart = () => {
const { data: rawData } = useExperiences({ count: false })

if (!rawData) return <Skeleton className="h-7 w-full" />

if (rawData.length === 0)
return <p className="text-muted-foreground">There's no activity this month</p>

return (
<div className="h-7">
<ParentSize>
{({ width, height }) => (
<ActivityChartChildren height={height} width={width} rawData={rawData} />
)}
</ParentSize>
</div>
)
}
41 changes: 41 additions & 0 deletions src/app/(marketing)/profile/_components/LevelBadge.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof levelBadgeVariants>

export const LevelBadge = ({
rank,
levelIndex,
size,
className,
}: LevelBadgeProps) => {
return (
<div className={levelBadgeVariants({ size, className })}>
<Polygon
sides={rank.polygonCount}
color={rank.color}
className="brightness-75"
/>
<span className="-mt-0.5 absolute font-bold font-display text-white">
{levelIndex}
</span>
</div>
)
}
47 changes: 44 additions & 3 deletions src/app/(marketing)/profile/_components/Profile.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
"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"
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"
Expand All @@ -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 <ProfileMainSkeleton />
if (!profile || !contributions || !referredUsers || !xp)
return <ProfileMainSkeleton />

return (
<>
<div className="mb-3 flex items-center justify-between" data-theme="dark">
<div className="mb-12">
<div data-theme="dark" className="mb-3">
<SectionTitle>Experience</SectionTitle>
</div>
<div className="grid grid-cols-1 gap-3 md:grid-cols-2">
<Card className="flex items-center gap-4 p-6">
<LevelBadge
levelIndex={xp.levelIndex}
rank={xp.rank}
size="lg"
className=""
/>
<div className="-mt-1 flex grow flex-col gap-2">
<div className="flex flex-col justify-between sm:flex-row">
<h3 className="font-bold capitalize">{xp.rank.title}</h3>
<p className="text-muted-foreground">
{`${xp.experienceCount} / ${xp.level} XP`}
</p>
</div>
<ProgressRoot>
<ProgressIndicator
value={xp.progress}
style={{ background: xp.rank.color }}
/>
</ProgressRoot>
</div>
</Card>
<Card className="space-y-3 p-6 pt-5">
<div className="flex flex-col items-start justify-between gap-2 sm:flex-row">
<h3 className="font-bold">Engagement this month</h3>
</div>
<ActivityChart />
</Card>
</div>
</div>
<div className="mb-3 flex items-center justify-between">
<SectionTitle>Top contributions</SectionTitle>
<ProfileOwnerGuard>
<EditContributions />
Expand All @@ -48,7 +89,7 @@ export const Profile = () => {
<ContributionCard contribution={contribution} key={contribution.id} />
))}
</div>
<div className="mt-16">
<div className="mt-14">
<SectionTitle className="mb-3">Recent activity</SectionTitle>
{isWeb3Connected ? <RecentActivity /> : <RecentActivityFallback />}
</div>
Expand Down
Loading
Loading