mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2025-08-19 07:31:24 +02:00
Add activity page
This commit is contained in:
@@ -47,7 +47,7 @@ export function ActivityCounters(props: ActivityCountersType) {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<ActivityCounter
|
<ActivityCounter
|
||||||
text={'Learning Streak'}
|
text={'Visit Streak'}
|
||||||
count={`${streak?.count || 0}d`}
|
count={`${streak?.count || 0}d`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -20,6 +20,7 @@ type ActivityResponse = {
|
|||||||
done: number;
|
done: number;
|
||||||
total: number;
|
total: number;
|
||||||
skipped: number;
|
skipped: number;
|
||||||
|
updatedAt: string;
|
||||||
}[];
|
}[];
|
||||||
bestPractices: {
|
bestPractices: {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -28,10 +29,13 @@ type ActivityResponse = {
|
|||||||
done: number;
|
done: number;
|
||||||
skipped: number;
|
skipped: number;
|
||||||
total: number;
|
total: number;
|
||||||
|
updatedAt: string;
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
streak: {
|
streak: {
|
||||||
count: number;
|
count: number;
|
||||||
|
firstVisitAt: Date | null;
|
||||||
|
lastVisitAt: Date | null;
|
||||||
};
|
};
|
||||||
activity: {
|
activity: {
|
||||||
type: 'done' | 'learning' | 'pending' | 'skipped';
|
type: 'done' | 'learning' | 'pending' | 'skipped';
|
||||||
@@ -52,7 +56,7 @@ export function ActivityPage() {
|
|||||||
|
|
||||||
async function loadActivity() {
|
async function loadActivity() {
|
||||||
const { error, response } = await httpGet<ActivityResponse>(
|
const { error, response } = await httpGet<ActivityResponse>(
|
||||||
`${import.meta.env.PUBLIC_API_URL}/v1-get-user-activity`
|
`${import.meta.env.PUBLIC_API_URL}/v1-get-user-stats`
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!response || error) {
|
if (!response || error) {
|
||||||
@@ -105,6 +109,7 @@ export function ActivityPage() {
|
|||||||
skippedCount={roadmap.skipped || 0}
|
skippedCount={roadmap.skipped || 0}
|
||||||
resourceId={roadmap.id}
|
resourceId={roadmap.id}
|
||||||
resourceType={'roadmap'}
|
resourceType={'roadmap'}
|
||||||
|
updatedAt={roadmap.updatedAt}
|
||||||
title={roadmap.title}
|
title={roadmap.title}
|
||||||
onCleared={() => {
|
onCleared={() => {
|
||||||
pageLoadingMessage.set('Updating activity');
|
pageLoadingMessage.set('Updating activity');
|
||||||
@@ -124,6 +129,7 @@ export function ActivityPage() {
|
|||||||
skippedCount={bestPractice.skipped || 0}
|
skippedCount={bestPractice.skipped || 0}
|
||||||
resourceType={'best-practice'}
|
resourceType={'best-practice'}
|
||||||
title={bestPractice.title}
|
title={bestPractice.title}
|
||||||
|
updatedAt={bestPractice.updatedAt}
|
||||||
onCleared={() => {
|
onCleared={() => {
|
||||||
pageLoadingMessage.set('Updating activity');
|
pageLoadingMessage.set('Updating activity');
|
||||||
loadActivity().finally(() => {
|
loadActivity().finally(() => {
|
||||||
|
@@ -7,10 +7,10 @@ export function EmptyActivity() {
|
|||||||
<img
|
<img
|
||||||
alt="no roadmaps"
|
alt="no roadmaps"
|
||||||
src={CheckIcon}
|
src={CheckIcon}
|
||||||
class="mb-2 h-[120px] w-[120px] opacity-10"
|
class="mb-2 w-[60px] h-[60px] sm:h-[120px] sm:w-[120px] opacity-10"
|
||||||
/>
|
/>
|
||||||
<h2 class="text-xl font-bold">No Progress</h2>
|
<h2 class="text-lg sm:text-xl font-bold">No Progress</h2>
|
||||||
<p className="my-2 max-w-[400px] text-gray-500">
|
<p className="my-1 sm:my-2 max-w-[400px] text-gray-500 text-sm sm:text-base">
|
||||||
Progress will appear here as you start tracking your{' '}
|
Progress will appear here as you start tracking your{' '}
|
||||||
<a href="/roadmaps" class="mt-4 text-blue-500 hover:underline">
|
<a href="/roadmaps" class="mt-4 text-blue-500 hover:underline">
|
||||||
Roadmaps
|
Roadmaps
|
||||||
|
@@ -1,10 +1,12 @@
|
|||||||
import { useEffect, useState } from 'preact/hooks';
|
import { useState } from 'preact/hooks';
|
||||||
import { httpPost } from '../../lib/http';
|
import { httpPost } from '../../lib/http';
|
||||||
|
import { getRelativeTimeString } from '../../lib/date';
|
||||||
|
|
||||||
type ResourceProgressType = {
|
type ResourceProgressType = {
|
||||||
resourceType: 'roadmap' | 'best-practice';
|
resourceType: 'roadmap' | 'best-practice';
|
||||||
resourceId: string;
|
resourceId: string;
|
||||||
title: string;
|
title: string;
|
||||||
|
updatedAt: string;
|
||||||
totalCount: number;
|
totalCount: number;
|
||||||
doneCount: number;
|
doneCount: number;
|
||||||
learningCount: number;
|
learningCount: number;
|
||||||
@@ -17,6 +19,7 @@ export function ResourceProgress(props: ResourceProgressType) {
|
|||||||
const [isConfirming, setIsConfirming] = useState(false);
|
const [isConfirming, setIsConfirming] = useState(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
updatedAt,
|
||||||
resourceType,
|
resourceType,
|
||||||
resourceId,
|
resourceId,
|
||||||
title,
|
title,
|
||||||
@@ -71,16 +74,30 @@ export function ResourceProgress(props: ResourceProgressType) {
|
|||||||
width: `${progressPercentage}%`,
|
width: `${progressPercentage}%`,
|
||||||
}}
|
}}
|
||||||
></span>
|
></span>
|
||||||
<span className="relative flex-1 cursor-pointer">{title}</span>
|
<span className="relative flex-1 cursor-pointer truncate">
|
||||||
<span className="cursor-pointer text-sm text-gray-400">
|
{title}
|
||||||
5 hours ago
|
</span>
|
||||||
|
<span className="ml-1 cursor-pointer text-sm text-gray-400">
|
||||||
|
{getRelativeTimeString(updatedAt)}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</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">
|
<p className="sm:space-between flex flex-row items-start rounded-b-md border border-t-0 px-2 py-2 text-xs text-gray-500">
|
||||||
<span className="flex-1 gap-1 flex">
|
<span className="hidden flex-1 gap-1 sm:flex">
|
||||||
<span>{doneCount} done</span> •
|
{doneCount > 0 && (
|
||||||
{ learningCount > 0 && <><span>{learningCount} in progress</span> •</> }
|
<>
|
||||||
{ skippedCount > 0 && <><span>{skippedCount} skipped</span> •</> }
|
<span>{doneCount} done</span> •
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{learningCount > 0 && (
|
||||||
|
<>
|
||||||
|
<span>{learningCount} in progress</span> •
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{skippedCount > 0 && (
|
||||||
|
<>
|
||||||
|
<span>{skippedCount} skipped</span> •
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<span>{totalCount} total</span>
|
<span>{totalCount} total</span>
|
||||||
</span>
|
</span>
|
||||||
{!isConfirming && (
|
{!isConfirming && (
|
||||||
@@ -101,10 +118,19 @@ export function ResourceProgress(props: ResourceProgressType) {
|
|||||||
|
|
||||||
{isConfirming && (
|
{isConfirming && (
|
||||||
<span>
|
<span>
|
||||||
<span className='hidden sm:inline'>Are you sure?{' '}</span>
|
Are you sure?{' '}
|
||||||
<span className='inline sm:hidden'>Sure?{' '}</span>
|
<button
|
||||||
<button onClick={clearProgress} className="ml-1 mr-1 underline text-red-500 hover:text-red-800">Yes</button>{' '}
|
onClick={clearProgress}
|
||||||
<button onClick={() => setIsConfirming(false)} className="underline text-red-500 hover:text-red-800">No</button>
|
className="ml-1 mr-1 text-red-500 underline hover:text-red-800"
|
||||||
|
>
|
||||||
|
Yes
|
||||||
|
</button>{' '}
|
||||||
|
<button
|
||||||
|
onClick={() => setIsConfirming(false)}
|
||||||
|
className="text-red-500 underline hover:text-red-800"
|
||||||
|
>
|
||||||
|
No
|
||||||
|
</button>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
|
@@ -26,8 +26,8 @@ import AccountDropdown from './AccountDropdown.astro';
|
|||||||
<a href='/videos' class='text-gray-400 hover:text-white'>Videos</a>
|
<a href='/videos' class='text-gray-400 hover:text-white'>Videos</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<kbd data-command-menu class="hidden md:flex items-center text-gray-400 border border-gray-800 rounded-md px-2.5 py-1 text-sm hover:bg-gray-800 hover:cursor-pointer">
|
<kbd data-command-menu class="hidden sm:flex items-center text-gray-400 border border-gray-800 rounded-md px-2.5 py-1 text-sm hover:bg-gray-800 hover:cursor-pointer">
|
||||||
<!-- <Icon icon='search' class='h-3 w-3 mr-2' /> -->
|
<Icon icon='search' class='h-3 w-3 mr-2' />
|
||||||
<kbd class='font-sans mr-1'>⌘</kbd><kbd class='font-sans'>K</kbd>
|
<kbd class='font-sans mr-1'>⌘</kbd><kbd class='font-sans'>K</kbd>
|
||||||
</kbd>
|
</kbd>
|
||||||
</li>
|
</li>
|
||||||
|
30
src/lib/date.ts
Normal file
30
src/lib/date.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
export function getRelativeTimeString(date: string): string {
|
||||||
|
if (!Intl?.RelativeTimeFormat) {
|
||||||
|
return date;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rtf = new Intl.RelativeTimeFormat('en', {
|
||||||
|
numeric: 'auto',
|
||||||
|
style: 'narrow',
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentDate = new Date();
|
||||||
|
const targetDate = new Date(date);
|
||||||
|
const diffInMilliseconds = currentDate.getTime() - targetDate.getTime();
|
||||||
|
|
||||||
|
const diffInMinutes = Math.round(diffInMilliseconds / (1000 * 60));
|
||||||
|
const diffInHours = Math.round(diffInMilliseconds / (1000 * 60 * 60));
|
||||||
|
const diffInDays = Math.round(diffInMilliseconds / (1000 * 60 * 60 * 24));
|
||||||
|
|
||||||
|
let relativeTime;
|
||||||
|
|
||||||
|
if (diffInMinutes < 60) {
|
||||||
|
relativeTime = rtf.format(-diffInMinutes, 'minute');
|
||||||
|
} else if (diffInHours < 24) {
|
||||||
|
relativeTime = rtf.format(-diffInHours, 'hour');
|
||||||
|
} else {
|
||||||
|
relativeTime = rtf.format(-diffInDays, 'day');
|
||||||
|
}
|
||||||
|
|
||||||
|
return relativeTime;
|
||||||
|
}
|
Reference in New Issue
Block a user