Roadmap to becoming a developer in 2022
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

189 lines
6.4 KiB

import { useEffect, useRef, useState } from 'react';
import { isLoggedIn } from '../../lib/jwt';
import { httpGet } from '../../lib/http';
import { useToast } from '../../hooks/use-toast';
import { Zap, ZapOff } from 'lucide-react';
import { useOutsideClick } from '../../hooks/use-outside-click';
import { StreakDay } from './StreakDay';
import {
navigationDropdownOpen,
roadmapsDropdownOpen,
} from '../../stores/page.ts';
import { useStore } from '@nanostores/react';
import { cn } from '../../lib/classname.ts';
import { $accountStreak, type StreakResponse } from '../../stores/streak.ts';
import { InviteFriends } from './InviteFriends.tsx';
type AccountStreakProps = {};
export function AccountStreak(props: AccountStreakProps) {
const toast = useToast();
const dropdownRef = useRef(null);
const [isLoading, setIsLoading] = useState(true);
const accountStreak = useStore($accountStreak);
const [showDropdown, setShowDropdown] = useState(false);
const $roadmapsDropdownOpen = useStore(roadmapsDropdownOpen);
const $navigationDropdownOpen = useStore(navigationDropdownOpen);
useEffect(() => {
if ($roadmapsDropdownOpen || $navigationDropdownOpen) {
setShowDropdown(false);
}
}, [$roadmapsDropdownOpen, $navigationDropdownOpen]);
const loadAccountStreak = async () => {
if (!isLoggedIn()) {
return;
}
if (accountStreak) {
setIsLoading(false);
return;
}
setIsLoading(true);
const { response, error } = await httpGet<StreakResponse>(
`${import.meta.env.PUBLIC_API_URL}/v1-streak`,
);
if (error || !response) {
toast.error(error?.message || 'Failed to load account streak');
setIsLoading(false);
return;
}
$accountStreak.set(response);
setIsLoading(false);
};
useOutsideClick(dropdownRef, () => {
setShowDropdown(false);
});
useEffect(() => {
loadAccountStreak().finally(() => {});
}, []);
if (!isLoggedIn() || isLoading) {
return null;
}
let { count: currentCount = 0 } = accountStreak || {};
const previousCount =
accountStreak?.previousCount || accountStreak?.count || 0;
// Adding one to show the current day
const currentCircleCount = Math.min(currentCount, 5) + 1;
// Adding one day to show the streak they broke
const leftCircleCount = Math.min(5 - currentCircleCount, previousCount) + 1;
// In the maximum case, we will show 10 circles
const remainingCount = Math.max(0, 10 - leftCircleCount - currentCircleCount);
const totalCircles = leftCircleCount + currentCircleCount + remainingCount;
return (
<div className="relative z-90 animate-fade-in">
<button
className={cn(
'flex items-center justify-center rounded-lg p-1.5 px-2 text-purple-400 hover:bg-purple-100/10 focus:outline-hidden',
{
'bg-purple-100/10': showDropdown,
},
)}
onClick={() => setShowDropdown(true)}
>
<Zap strokeWidth={1} className="size-5 fill-current" />
<span className="ml-1.5 text-sm font-semibold">
{accountStreak?.count}
</span>
</button>
{showDropdown && (
<div
ref={dropdownRef}
className="absolute right-0 top-full z-50 w-[335px] translate-y-1 rounded-lg bg-slate-800 shadow-xl"
>
<div className="py-5 pl-4 pr-5">
<div className="flex items-center justify-between gap-2 text-sm text-slate-500">
<p>
Current Streak
<span className="ml-2 font-medium text-white">
{accountStreak?.count || 0}
</span>
</p>
<p>
Longest Streak
<span className="ml-2 font-medium text-white">
{accountStreak?.longestCount || 0}
</span>
</p>
</div>
<div className="mb-6 mt-9">
<div className="grid grid-cols-10 gap-1">
{Array.from({ length: totalCircles }).map((_, index) => {
let dayCount,
icon,
isPreviousStreakDay,
isBrokenStreakDay,
isCurrentStreakDay,
isRemainingStreakDay,
isToday;
if (index < leftCircleCount) {
// Previous streak days
dayCount = previousCount - leftCircleCount + index + 1 + 1;
isPreviousStreakDay = true;
isBrokenStreakDay = index === leftCircleCount - 1;
icon = isBrokenStreakDay ? (
<ZapOff className="size-5 fill-current" />
) : (
<Zap className="size-5 fill-current" />
);
} else if (index < leftCircleCount + currentCircleCount) {
// Current streak days
const currentIndex = index - leftCircleCount;
dayCount =
currentCount - currentCircleCount + currentIndex + 1 + 1;
isCurrentStreakDay = true;
isToday = currentIndex === currentCircleCount - 1;
icon = <Zap className="size-5 fill-current" />;
} else {
// Remaining streak days
const remainingIndex =
index - leftCircleCount - currentCircleCount;
dayCount = currentCount + remainingIndex + 1 + 1;
isRemainingStreakDay = true;
}
return (
<StreakDay
key={`streak-${index}`}
dayCount={dayCount}
icon={icon}
isBrokenStreakDay={isBrokenStreakDay}
isPreviousStreakDay={isPreviousStreakDay}
isCurrentStreakDay={isCurrentStreakDay}
isRemainingStreakDay={isRemainingStreakDay}
isToday={isToday}
/>
);
})}
</div>
</div>
<p className="-mt-[0px] mb-[1.5px] text-center text-xs tracking-wide text-slate-500">
Visit every day to keep your streak going!
</p>
<InviteFriends
refByUserCount={accountStreak?.refByUserCount || 0}
/>
</div>
</div>
)}
</div>
);
}