feat: add referrals leaderboard

feat/referral
Arik Chakma 2 months ago
parent 9d9d70de76
commit ddf96ff6d6
  1. 4
      src/api/leaderboard.ts
  2. 57
      src/components/AccountStreak/AccountStreak.tsx
  3. 80
      src/components/AccountStreak/InviteFriends.tsx
  4. 25
      src/components/Leaderboard/LeaderboardPage.tsx

@ -17,6 +17,10 @@ export type ListLeaderboardStatsResponse = {
currentMonth: LeadeboardUserDetails[];
lifetime: LeadeboardUserDetails[];
};
referrals: {
currentMonth: LeadeboardUserDetails[];
lifetime: LeadeboardUserDetails[];
};
};
export function leaderboardApi(context: APIContext) {

@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from 'react';
import { isLoggedIn } from '../../lib/jwt';
import { httpGet } from '../../lib/http';
import { useToast } from '../../hooks/use-toast';
import { Flame, UserPlus, X, Zap, ZapOff } from 'lucide-react';
import { Zap, ZapOff } from 'lucide-react';
import { useOutsideClick } from '../../hooks/use-outside-click';
import { StreakDay } from './StreakDay';
import {
@ -12,8 +12,7 @@ import {
import { useStore } from '@nanostores/react';
import { cn } from '../../lib/classname.ts';
import { $accountStreak, type StreakResponse } from '../../stores/streak.ts';
import { useCopyText } from '../../hooks/use-copy-text.ts';
import { useAuth } from '../../hooks/use-auth.ts';
import { InviteFriends } from './InviteFriends.tsx';
type AccountStreakProps = {};
@ -21,8 +20,6 @@ export function AccountStreak(props: AccountStreakProps) {
const toast = useToast();
const dropdownRef = useRef(null);
const user = useAuth();
const { copyText } = useCopyText();
const [isLoading, setIsLoading] = useState(true);
const accountStreak = useStore($accountStreak);
const [showDropdown, setShowDropdown] = useState(false);
@ -73,16 +70,6 @@ export function AccountStreak(props: AccountStreakProps) {
return null;
}
const shareReferralLink = () => {
const referralLink = new URL(
`/signup?rc=${user?.id}`,
import.meta.env.DEV ? 'http://localhost:3000' : 'https://roadmap.sh',
).toString();
copyText(referralLink);
toast.success('Referral link copied to clipboard');
};
let { count: currentCount = 0 } = accountStreak || {};
const previousCount =
accountStreak?.previousCount || accountStreak?.count || 0;
@ -199,43 +186,9 @@ export function AccountStreak(props: AccountStreakProps) {
</a>
</p>
{accountStreak?.refByUserCount ? (
<div className="mt-5">
<div className="flex items-center gap-2 text-sm text-slate-500">
<UserPlus className="size-4" />
<p>
<span className="text-slate-200">
{accountStreak?.refByUserCount || 0}
</span>{' '}
user(s) joined through{' '}
<button
className="text-slate-200 no-underline underline-offset-2 hover:underline"
onClick={shareReferralLink}
>
your referral
</button>
.
</p>
</div>
</div>
) : null}
{!accountStreak?.refByUserCount && (
<div className="mt-5">
<div className="flex items-center gap-2 text-sm text-slate-500">
<UserPlus className="size-4" />
<p>
<button
className="text-slate-200 no-underline underline-offset-2 hover:underline"
onClick={shareReferralLink}
>
Share your referral
</button>{' '}
link with your friends.
</p>
</div>
</div>
)}
<InviteFriends
refByUserCount={accountStreak?.refByUserCount || 0}
/>
</div>
</div>
)}

@ -0,0 +1,80 @@
import { Copy } from 'lucide-react';
import { useAuth } from '../../hooks/use-auth';
import { useCopyText } from '../../hooks/use-copy-text';
import { cn } from '../../lib/classname';
import { CheckIcon } from '../ReactIcons/CheckIcon';
type InviteFriendsProps = {
refByUserCount: number;
};
export function InviteFriends(props: InviteFriendsProps) {
const { refByUserCount } = props;
const user = useAuth();
const { copyText, isCopied } = useCopyText();
const referralLink = new URL(
`/signup?rc=${user?.id}`,
import.meta.env.DEV ? 'http://localhost:3000' : 'https://roadmap.sh',
).toString();
return (
<div className="">
<hr className="-mx-4 my-4 border-slate-700" />
<h4 className="text-sm">Invite Friends</h4>
<p className="mt-1 text-xs text-slate-400">
Share the link below to invite anyone to roadmap.sh
</p>
<div className="mt-4 flex items-center overflow-hidden rounded-md border border-slate-700">
<input
type="text"
className="grow border-none bg-gray-50 bg-transparent p-1 px-2 text-sm text-slate-300 outline-none focus:outline-none"
value={referralLink}
onDoubleClick={(e) => {
e.currentTarget.select();
copyText(referralLink);
}}
readOnly
/>
<button
className={cn(
'flex items-center gap-2 border-l border-slate-700 p-1.5 px-2 text-sm text-white hover:bg-slate-700',
isCopied ? 'text-green-500' : '',
)}
onClick={() => {
copyText(referralLink);
}}
>
{isCopied ? (
<>
<CheckIcon additionalClasses="size-3" />
Copied
</>
) : (
<>
<Copy className="size-3" />
Copy
</>
)}
</button>
</div>
<p className="mt-2.5 text-center text-xs">
🥳 You have invited{' '}
<span className="font-medium underline underline-offset-2">
{refByUserCount} users so far
</span>
</p>
<a
href="/leaderboard"
className="mt-4 flex justify-center text-center text-sm font-medium text-purple-500 underline-offset-2 hover:underline"
>
See how you are compare to others
</a>
</div>
);
}

@ -4,7 +4,7 @@ import type {
ListLeaderboardStatsResponse,
} from '../../api/leaderboard';
import { cn } from '../../lib/classname';
import { FolderKanban, Zap, Trophy } from 'lucide-react';
import { FolderKanban, Zap, Trophy, Users } from 'lucide-react';
import { RankBadgeIcon } from '../ReactIcons/RankBadgeIcon';
import { TrophyEmoji } from '../ReactIcons/TrophyEmoji';
import { SecondPlaceMedalEmoji } from '../ReactIcons/SecondPlaceMedalEmoji';
@ -64,6 +64,23 @@ export function LeaderboardPage(props: LeaderboardPageProps) {
},
]}
/>
<LeaderboardLane
title="Most Referrals"
tabs={[
{
title: 'This Month',
users: stats.referrals.currentMonth,
emptyIcon: <Users className="size-16 text-gray-300" />,
emptyText: 'No referrals this month',
},
{
title: 'Lifetime',
users: stats.referrals.lifetime,
emptyIcon: <Users className="size-16 text-gray-300" />,
emptyText: 'No referrals yet',
},
]}
/>
</div>
</div>
</div>
@ -89,7 +106,7 @@ function LeaderboardLane(props: LeaderboardLaneProps) {
return (
<div className="overflow-hidden rounded-md border bg-white shadow-sm">
<div className="flex items-center justify-between gap-2 bg-gray-100 px-3 py-3 mb-3">
<div className="mb-3 flex items-center justify-between gap-2 bg-gray-100 px-3 py-3">
<h3 className="text-base font-medium">{title}</h3>
{tabs.length > 1 && (
@ -135,12 +152,12 @@ function LeaderboardLane(props: LeaderboardLaneProps) {
return (
<li
key={user.id}
className="flex items-center justify-between gap-1 pl-2 pr-5 py-2.5 hover:bg-gray-50"
className="flex items-center justify-between gap-1 py-2.5 pl-2 pr-5 hover:bg-gray-50"
>
<div className="flex min-w-0 items-center gap-2">
<span
className={cn(
'relative text-xs mr-1 flex size-6 shrink-0 items-center justify-center rounded-full tabular-nums',
'relative mr-1 flex size-6 shrink-0 items-center justify-center rounded-full text-xs tabular-nums',
{
'text-black': rank <= 3,
'text-gray-400': rank > 3,

Loading…
Cancel
Save