feat: add referral user count

feat/referral
Arik Chakma 3 weeks ago
parent f20334b0de
commit 9d9d70de76
  1. 71
      src/components/AccountStreak/AccountStreak.tsx
  2. 19
      src/components/AuthenticationFlow/EmailSignupForm.tsx
  3. 1
      src/stores/streak.ts

@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from 'react';
import { isLoggedIn } from '../../lib/jwt'; import { isLoggedIn } from '../../lib/jwt';
import { httpGet } from '../../lib/http'; import { httpGet } from '../../lib/http';
import { useToast } from '../../hooks/use-toast'; import { useToast } from '../../hooks/use-toast';
import { Flame, X, Zap, ZapOff } from 'lucide-react'; import { Flame, UserPlus, X, Zap, ZapOff } from 'lucide-react';
import { useOutsideClick } from '../../hooks/use-outside-click'; import { useOutsideClick } from '../../hooks/use-outside-click';
import { StreakDay } from './StreakDay'; import { StreakDay } from './StreakDay';
import { import {
@ -11,15 +11,9 @@ import {
} from '../../stores/page.ts'; } from '../../stores/page.ts';
import { useStore } from '@nanostores/react'; import { useStore } from '@nanostores/react';
import { cn } from '../../lib/classname.ts'; import { cn } from '../../lib/classname.ts';
import { $accountStreak } from '../../stores/streak.ts'; import { $accountStreak, type StreakResponse } from '../../stores/streak.ts';
import { useCopyText } from '../../hooks/use-copy-text.ts';
type StreakResponse = { import { useAuth } from '../../hooks/use-auth.ts';
count: number;
longestCount: number;
previousCount?: number | null;
firstVisitAt: Date;
lastVisitAt: Date;
};
type AccountStreakProps = {}; type AccountStreakProps = {};
@ -27,6 +21,8 @@ export function AccountStreak(props: AccountStreakProps) {
const toast = useToast(); const toast = useToast();
const dropdownRef = useRef(null); const dropdownRef = useRef(null);
const user = useAuth();
const { copyText } = useCopyText();
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const accountStreak = useStore($accountStreak); const accountStreak = useStore($accountStreak);
const [showDropdown, setShowDropdown] = useState(false); const [showDropdown, setShowDropdown] = useState(false);
@ -77,6 +73,16 @@ export function AccountStreak(props: AccountStreakProps) {
return null; 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 || {}; let { count: currentCount = 0 } = accountStreak || {};
const previousCount = const previousCount =
accountStreak?.previousCount || accountStreak?.count || 0; accountStreak?.previousCount || accountStreak?.count || 0;
@ -184,11 +190,52 @@ export function AccountStreak(props: AccountStreakProps) {
<p className="-mt-[0px] mb-[1.5px] text-center text-xs tracking-wide text-slate-500"> <p className="-mt-[0px] mb-[1.5px] text-center text-xs tracking-wide text-slate-500">
Visit every day to keep your streak going! Visit every day to keep your streak going!
</p> </p>
<p className='text-xs mt-1.5 text-center'> <p className="mt-1.5 text-center text-xs">
<a href="/leaderboard" className="text-purple-400 hover:underline underline-offset-2"> <a
href="/leaderboard"
className="text-purple-400 underline-offset-2 hover:underline"
>
See how you compare to others See how you compare to others
</a> </a>
</p> </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>
)}
</div> </div>
</div> </div>
)} )}

@ -1,5 +1,7 @@
import { type FormEvent, useState } from 'react'; import { type FormEvent, useEffect, useState } from 'react';
import { httpPost } from '../../lib/http'; import { httpPost } from '../../lib/http';
import { deleteUrlParam, getUrlParams } from '../../lib/browser';
import { isLoggedIn, setAIReferralCode } from '../../lib/jwt';
type EmailSignupFormProps = { type EmailSignupFormProps = {
isDisabled?: boolean; isDisabled?: boolean;
@ -9,6 +11,9 @@ type EmailSignupFormProps = {
export function EmailSignupForm(props: EmailSignupFormProps) { export function EmailSignupForm(props: EmailSignupFormProps) {
const { isDisabled, setIsDisabled } = props; const { isDisabled, setIsDisabled } = props;
const { rc: referralCode } = getUrlParams() as {
rc?: string;
};
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
const [name, setName] = useState(''); const [name, setName] = useState('');
@ -47,6 +52,16 @@ export function EmailSignupForm(props: EmailSignupFormProps) {
)}`; )}`;
}; };
useEffect(() => {
if (!referralCode || isLoggedIn()) {
deleteUrlParam('rc');
return;
}
setAIReferralCode(referralCode);
deleteUrlParam('rc');
}, []);
return ( return (
<form className="flex w-full flex-col gap-2" onSubmit={onSubmit}> <form className="flex w-full flex-col gap-2" onSubmit={onSubmit}>
<label htmlFor="name" className="sr-only"> <label htmlFor="name" className="sr-only">
@ -72,7 +87,7 @@ export function EmailSignupForm(props: EmailSignupFormProps) {
type="email" type="email"
autoComplete="email" autoComplete="email"
required required
className="block w-full rounded-lg border border-gray-300 px-3 py-2 outline-none placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1" className="block w-full rounded-lg border border-gray-300 px-3 py-2 outline-none placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1"
placeholder="Email Address" placeholder="Email Address"
value={email} value={email}
onInput={(e) => setEmail(String((e.target as any).value))} onInput={(e) => setEmail(String((e.target as any).value))}

@ -6,6 +6,7 @@ export type StreakResponse = {
previousCount?: number | null; previousCount?: number | null;
firstVisitAt: Date; firstVisitAt: Date;
lastVisitAt: Date; lastVisitAt: Date;
refByUserCount: number;
}; };
export const $accountStreak = atom<StreakResponse | undefined>(); export const $accountStreak = atom<StreakResponse | undefined>();

Loading…
Cancel
Save