Add activity dashboard

pull/3985/head
Kamran Ahmed 2 years ago
parent f7625a8250
commit 476557db80
  1. 56
      src/components/Activity/ActivityCounters.tsx
  2. 141
      src/components/Activity/ActivityPage.tsx
  3. 27
      src/components/Activity/EmptyActivity.tsx
  4. 113
      src/components/Activity/ResourceProgress.tsx
  5. 2
      src/components/CommandMenu/CommandMenu.tsx
  6. 36
      src/pages/account/index.astro

@ -0,0 +1,56 @@
type ActivityCountersType = {
done: {
today: number;
total: number;
};
learning: {
today: number;
total: number;
};
streak: {
count: number;
};
};
type ActivityCounterType = {
text: string;
count: string;
};
function ActivityCounter(props: ActivityCounterType) {
const { text, count } = props;
return (
<div class="relative flex flex-1 flex-row-reverse sm:flex-col px-0 sm:px-4 py-2 sm:py-4 text-center sm:pt-10 items-center gap-2 sm:gap-0 justify-end">
<h2 class="text-base sm:text-5xl font-bold">
{count}
</h2>
<p class="mt-0 sm:mt-2 text-sm text-gray-400">{text}</p>
</div>
);
}
export function ActivityCounters(props: ActivityCountersType) {
const { done, learning, streak } = props;
return (
<div class="mx-0 -mt-5 sm:-mx-10 md:-mt-10">
<div class="flex flex-col sm:flex-row gap-0 sm:gap-2 divide-y sm:divide-y-0 divide-x-0 sm:divide-x border-b">
<ActivityCounter
text={'Topics Completed'}
count={`${done?.total || 0}`}
/>
<ActivityCounter
text={'Currently Learning'}
count={`${learning?.total || 0}`}
/>
<ActivityCounter
text={'Learning Streak'}
count={`${streak?.count || 0}d`}
/>
</div>
</div>
);
}

@ -0,0 +1,141 @@
import { useEffect, useState } from 'preact/hooks';
import { httpGet } from '../../lib/http';
import { ActivityCounters } from './ActivityCounters';
import { ResourceProgress } from './ResourceProgress';
import { pageLoadingMessage } from '../../stores/page';
import { EmptyActivity } from './EmptyActivity';
type ActivityResponse = {
done: {
today: number;
total: number;
};
learning: {
today: number;
total: number;
roadmaps: {
title: string;
id: string;
learning: number;
done: number;
total: number;
skipped: number;
}[];
bestPractices: {
title: string;
id: string;
learning: number;
done: number;
skipped: number;
total: number;
}[];
};
streak: {
count: number;
};
activity: {
type: 'done' | 'learning' | 'pending' | 'skipped';
createdAt: Date;
metadata: {
resourceId?: string;
resourceType?: 'roadmap' | 'best-practice';
topicId?: string;
topicLabel?: string;
resourceTitle?: string;
};
}[];
};
export function ActivityPage() {
const [activity, setActivity] = useState<ActivityResponse>();
const [isLoading, setIsLoading] = useState(true);
async function loadActivity() {
const { error, response } = await httpGet<ActivityResponse>(
`${import.meta.env.PUBLIC_API_URL}/v1-get-user-activity`
);
if (!response || error) {
console.error('Error loading activity');
console.error(error);
return;
}
setActivity(response);
}
useEffect(() => {
loadActivity().finally(() => {
pageLoadingMessage.set('');
setIsLoading(false);
});
}, []);
const learningRoadmaps = activity?.learning.roadmaps || [];
const learningBestPractices = activity?.learning.bestPractices || [];
if (isLoading) {
return null;
}
return (
<>
<ActivityCounters
done={activity?.done || { today: 0, total: 0 }}
learning={activity?.learning || { today: 0, total: 0 }}
streak={activity?.streak || { count: 0 }}
/>
<div class="mx-0 px-0 py-5 md:-mx-10 md:px-8 md:py-8">
{learningRoadmaps.length === 0 &&
learningBestPractices.length === 0 && <EmptyActivity />}
{(learningRoadmaps.length > 0 || learningBestPractices.length > 0) && (
<>
<h2 class="mb-3 text-xs uppercase text-gray-400">
Continue Following
</h2>
<div class="flex flex-col gap-3">
{learningRoadmaps.map((roadmap) => (
<ResourceProgress
doneCount={roadmap.done || 0}
learningCount={roadmap.learning || 0}
totalCount={roadmap.total || 0}
skippedCount={roadmap.skipped || 0}
resourceId={roadmap.id}
resourceType={'roadmap'}
title={roadmap.title}
onCleared={() => {
pageLoadingMessage.set('Updating activity');
loadActivity().finally(() => {
pageLoadingMessage.set('');
});
}}
/>
))}
{learningBestPractices.map((bestPractice) => (
<ResourceProgress
doneCount={bestPractice.done || 0}
totalCount={bestPractice.total || 0}
learningCount={bestPractice.learning || 0}
resourceId={bestPractice.id}
skippedCount={bestPractice.skipped || 0}
resourceType={'best-practice'}
title={bestPractice.title}
onCleared={() => {
pageLoadingMessage.set('Updating activity');
loadActivity().finally(() => {
pageLoadingMessage.set('');
});
}}
/>
))}
</div>
</>
)}
</div>
</>
);
}

@ -0,0 +1,27 @@
import CheckIcon from '../../icons/roadmap.svg';
export function EmptyActivity() {
return (
<div class="rounded-md">
<div class="flex flex-col items-center p-7 text-center">
<img
alt="no roadmaps"
src={CheckIcon}
class="mb-2 h-[120px] w-[120px] opacity-10"
/>
<h2 class="text-xl font-bold">No Progress</h2>
<p className="my-2 max-w-[400px] text-gray-500">
Progress will appear here as you start tracking your{' '}
<a href="/roadmaps" class="mt-4 text-blue-500 hover:underline">
Roadmaps
</a>{' '}
or{' '}
<a href="/best-practices" class="mt-4 text-blue-500 hover:underline">
Best Practices
</a>{' '}
progress.
</p>
</div>
</div>
);
}

@ -0,0 +1,113 @@
import { useEffect, useState } from 'preact/hooks';
import { httpPost } from '../../lib/http';
type ResourceProgressType = {
resourceType: 'roadmap' | 'best-practice';
resourceId: string;
title: string;
totalCount: number;
doneCount: number;
learningCount: number;
skippedCount: number;
onCleared: () => void;
};
export function ResourceProgress(props: ResourceProgressType) {
const [isClearing, setIsClearing] = useState(false);
const [isConfirming, setIsConfirming] = useState(false);
const {
resourceType,
resourceId,
title,
totalCount,
learningCount,
doneCount,
skippedCount,
onCleared,
} = props;
async function clearProgress() {
setIsClearing(true);
const { error, response } = await httpPost(
`${import.meta.env.PUBLIC_API_URL}/v1-clear-resource-progress`,
{
resourceId,
resourceType,
}
);
if (error || !response) {
alert('Error clearing progress. Please try again.');
console.error(error);
setIsClearing(false);
return;
}
localStorage.removeItem(`${resourceType}-${resourceId}-progress`);
console.log(`${resourceType}-${resourceId}-progress`);
setIsClearing(false);
setIsConfirming(false);
onCleared();
}
const url =
resourceType === 'roadmap'
? `/${resourceId}`
: `/best-practices/${resourceId}`;
const totalMarked = doneCount + skippedCount;
const progressPercentage = Math.round((totalMarked / totalCount) * 100);
return (
<div>
<a
href={url}
className="group relative flex cursor-pointer items-center rounded-t-md border p-3 text-gray-600 hover:border-gray-300 hover:text-black"
>
<span
className={`absolute left-0 top-0 block h-full cursor-pointer rounded-tl-md bg-black/5 group-hover:bg-black/10`}
style={{
width: `${progressPercentage}%`,
}}
></span>
<span className="relative flex-1 cursor-pointer">{title}</span>
<span className="cursor-pointer text-sm text-gray-400">
5 hours ago
</span>
</a>
<p className="items-start sm:space-between flex flex-row rounded-b-md border border-t-0 px-2 py-2 text-xs text-gray-500">
<span className="flex-1 gap-1 flex">
<span>{doneCount} done</span> &bull;
{ learningCount > 0 && <><span>{learningCount} in progress</span> &bull;</> }
{ skippedCount > 0 && <><span>{skippedCount} skipped</span> &bull;</> }
<span>{totalCount} total</span>
</span>
{!isConfirming && (
<button
className="text-red-500 hover:text-red-800"
onClick={() => setIsConfirming(true)}
disabled={isClearing}
>
{!isClearing && (
<>
Clear Progress <span>&times;</span>
</>
)}
{isClearing && 'Processing...'}
</button>
)}
{isConfirming && (
<span>
<span className='hidden sm:inline'>Are you sure?{' '}</span>
<span className='inline sm:hidden'>Sure?{' '}</span>
<button onClick={clearProgress} className="ml-1 mr-1 underline text-red-500 hover:text-red-800">Yes</button>{' '}
<button onClick={() => setIsConfirming(false)} className="underline text-red-500 hover:text-red-800">No</button>
</span>
)}
</p>
</div>
);
}

@ -21,7 +21,7 @@ type PageType = {
const defaultPages: PageType[] = [
{ url: '/', title: 'Home', group: 'Pages', icon: HomeIcon },
{
url: '/account/update-profile',
url: '/account',
title: 'Account',
group: 'Pages',
icon: UserIcon,

@ -1,41 +1,11 @@
---
import AccountSidebar from '../../components/AccountSidebar.astro';
import { ActivityPage } from '../../components/Activity/ActivityPage';
import AccountLayout from '../../layouts/AccountLayout.astro';
---
<AccountLayout title='Update Profile' noIndex={true}>
<AccountLayout title='Update Profile' noIndex={true} initialLoadingMessage={'Loading activity'}>
<AccountSidebar activePageId='activity' activePageTitle='Activity'>
<div class='mx-0 -mt-5 sm:-mx-10 md:-mt-10'>
<div class='flex gap-2 divide-x border-b'>
<div class='flex flex-1 flex-col px-4 pb-4 pt-5 text-center sm:pt-10'>
<h2 class='text-5xl font-bold'>30</h2>
<p class='mt-2 text-sm text-gray-400'>Topics Completed</p>
</div>
<div class='flex flex-1 flex-col px-4 pb-4 pt-5 text-center sm:pt-10'>
<h2 class='mb-1 text-5xl font-bold'>20</h2>
<p class='mt-2 text-sm text-gray-400'>Currently Learning</p>
</div>
<div class='flex flex-1 flex-col px-4 pb-4 pt-5 text-center sm:pt-10'>
<h2 class='mb-1 text-5xl font-bold'>2d</h2>
<p class='mt-2 text-sm text-gray-400'>Learning Streak</p>
</div>
</div>
</div>
<div class='-mx-10 border-b px-8 py-8'>
<h2 class='mb-3 text-xs uppercase text-gray-400'>Continue Learning</h2>
<div class='flex flex-col gap-2'>
<a
href='#'
class='relative flex items-center rounded-md border p-3 text-gray-600 group hover:border-gray-300 hover:text-black'
>
<span
class='absolute left-0 top-0 block h-full w-[50%] rounded-l-md bg-black/5 group-hover:bg-black/10'
></span>
<span class='relative flex-1'>Frontend Roadmap</span>
<span class='text-sm text-gray-400'>5 / 142</span>
</a>
</div>
</div>
<ActivityPage client:load />
</AccountSidebar>
</AccountLayout>

Loading…
Cancel
Save