Add builtin roadmaps and best practices

pull/8189/head
Kamran Ahmed 3 months ago
parent 3101d8ae5d
commit d9c25d8dff
  1. 98
      src/components/Dashboard/DashboardPage.tsx
  2. 382
      src/components/Dashboard/PersonalDashboard.tsx
  3. 38
      src/components/HeroSection/FavoriteRoadmaps.tsx
  4. 2
      src/pages/dashboard.astro
  5. 4
      src/styles/global.css

@ -1,15 +1,15 @@
import { useStore } from '@nanostores/react';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { httpGet } from '../../lib/http'; import { cn } from '../../../editor/utils/classname';
import { useParams } from '../../hooks/use-params';
import { useToast } from '../../hooks/use-toast'; import { useToast } from '../../hooks/use-toast';
import { useStore } from '@nanostores/react'; import { httpGet } from '../../lib/http';
import { getUser } from '../../lib/jwt';
import { $teamList } from '../../stores/team'; import { $teamList } from '../../stores/team';
import type { TeamListResponse } from '../TeamDropdown/TeamDropdown'; import type { TeamListResponse } from '../TeamDropdown/TeamDropdown';
import { DashboardTabButton } from './DashboardTabButton'; import { DashboardTabButton } from './DashboardTabButton';
import { PersonalDashboard, type BuiltInRoadmap } from './PersonalDashboard'; import { PersonalDashboard, type BuiltInRoadmap } from './PersonalDashboard';
import { TeamDashboard } from './TeamDashboard'; import { TeamDashboard } from './TeamDashboard';
import { getUser } from '../../lib/jwt';
import { useParams } from '../../hooks/use-params';
import { cn } from '../../../editor/utils/classname';
type DashboardPageProps = { type DashboardPageProps = {
builtInRoleRoadmaps?: BuiltInRoadmap[]; builtInRoleRoadmaps?: BuiltInRoadmap[];
@ -69,55 +69,57 @@ export function DashboardPage(props: DashboardPageProps) {
return ( return (
<> <>
<div <div
className={cn('bg-[#151b2e] py-5', { className={cn('bg-slate-900', {
'striped-loader-slate': isLoading, 'striped-loader-slate': isLoading,
})} })}
> >
<div className="container flex flex-wrap items-center gap-1.5"> <div className="bg-slate-800/30 py-5">
<DashboardTabButton <div className="container flex flex-wrap items-center gap-1.5">
label="Personal" <DashboardTabButton
isActive={!selectedTeamId && !isTeamPage} label="Personal"
href="/dashboard" isActive={!selectedTeamId && !isTeamPage}
avatar={userAvatar} href="/dashboard"
/> avatar={userAvatar}
/>
{!isLoading && ( {!isLoading && (
<> <>
{teamList.map((team) => { {teamList.map((team) => {
const { avatar } = team; const { avatar } = team;
const avatarUrl = avatar const avatarUrl = avatar
? `${import.meta.env.PUBLIC_AVATAR_BASE_URL}/${avatar}` ? `${import.meta.env.PUBLIC_AVATAR_BASE_URL}/${avatar}`
: '/images/default-avatar.png'; : '/images/default-avatar.png';
return ( return (
<DashboardTabButton <DashboardTabButton
key={team._id} key={team._id}
label={team.name} label={team.name}
isActive={team._id === selectedTeamId} isActive={team._id === selectedTeamId}
{...(team.status === 'invited' {...(team.status === 'invited'
? { ? {
href: `/respond-invite?i=${team.memberId}`, href: `/respond-invite?i=${team.memberId}`,
} }
: { : {
href: `/team?t=${team._id}`, href: `/team?t=${team._id}`,
})} })}
avatar={avatarUrl} avatar={avatarUrl}
/> />
); );
})} })}
<DashboardTabButton <DashboardTabButton
label="+ Create Team" label="+ Create Team"
isActive={false} isActive={false}
href="/team/new" href="/team/new"
className="border border-dashed border-slate-700 bg-transparent px-3 text-[13px] text-sm text-gray-500 hover:border-solid hover:border-slate-700 hover:text-gray-400" className="border border-dashed border-slate-700 bg-transparent px-3 text-[13px] text-sm text-gray-500 hover:border-solid hover:border-slate-700 hover:text-gray-400"
/> />
</> </>
)} )}
</div>
</div> </div>
</div> </div>
<div className=""> <div className="">
{!selectedTeamId && !isTeamPage && ( {!selectedTeamId && !isTeamPage && (
<div className="min-h-screen pb-20"> <div className="bg-slate-900">
<PersonalDashboard <PersonalDashboard
builtInRoleRoadmaps={builtInRoleRoadmaps} builtInRoleRoadmaps={builtInRoleRoadmaps}
builtInSkillRoadmaps={builtInSkillRoadmaps} builtInSkillRoadmaps={builtInSkillRoadmaps}
@ -139,9 +141,3 @@ export function DashboardPage(props: DashboardPageProps) {
</> </>
); );
} }
function DashboardTabSkeleton() {
return (
<div className="h-[30px] w-[114px] animate-pulse rounded-md border bg-white"></div>
);
}

@ -1,27 +1,26 @@
import { type JSXElementConstructor, useEffect, useState } from 'react';
import { httpGet } from '../../lib/http';
import type { UserProgress } from '../TeamProgress/TeamProgressPage';
import type { ProjectStatusDocument } from '../Projects/ListProjectSolutions';
import type { PageType } from '../CommandMenu/CommandMenu';
import { useToast } from '../../hooks/use-toast';
import { getCurrentPeriod } from '../../lib/date';
import { ListDashboardCustomProgress } from './ListDashboardCustomProgress';
import { RecommendedRoadmaps } from './RecommendedRoadmaps';
import { ProgressStack } from './ProgressStack';
import { useStore } from '@nanostores/react'; import { useStore } from '@nanostores/react';
import { $accountStreak, type StreakResponse } from '../../stores/streak'; import {
import { CheckEmoji } from '../ReactIcons/CheckEmoji.tsx'; CheckCircle,
import { ConstructionEmoji } from '../ReactIcons/ConstructionEmoji.tsx'; ChevronsDownUp,
import { BookEmoji } from '../ReactIcons/BookEmoji.tsx'; FolderGit2,
import { DashboardAiRoadmaps } from './DashboardAiRoadmaps.tsx'; Zap,
type LucideIcon,
} from 'lucide-react';
import { useEffect, useState } from 'react';
import type { AllowedProfileVisibility } from '../../api/user.ts'; import type { AllowedProfileVisibility } from '../../api/user.ts';
import { PencilIcon, type LucideIcon } from 'lucide-react'; import { useToast } from '../../hooks/use-toast';
import { cn } from '../../lib/classname.ts'; import { cn } from '../../lib/classname.ts';
import { httpGet } from '../../lib/http';
import type { AllowedRoadmapRenderer } from '../../lib/roadmap.ts'; import type { AllowedRoadmapRenderer } from '../../lib/roadmap.ts';
import { $accountStreak, type StreakResponse } from '../../stores/streak';
import type { PageType } from '../CommandMenu/CommandMenu';
import { import {
FavoriteRoadmaps, FavoriteRoadmaps,
HeroRoadmap,
type AIRoadmapType, type AIRoadmapType,
} from '../HeroSection/FavoriteRoadmaps.tsx'; } from '../HeroSection/FavoriteRoadmaps.tsx';
import type { ProjectStatusDocument } from '../Projects/ListProjectSolutions';
import type { UserProgress } from '../TeamProgress/TeamProgressPage';
type UserDashboardResponse = { type UserDashboardResponse = {
name: string; name: string;
@ -42,6 +41,7 @@ export type BuiltInRoadmap = {
title: string; title: string;
description: string; description: string;
isFavorite?: boolean; isFavorite?: boolean;
isNew?: boolean;
relatedRoadmapIds?: string[]; relatedRoadmapIds?: string[];
renderer?: AllowedRoadmapRenderer; renderer?: AllowedRoadmapRenderer;
metadata?: Record<string, any>; metadata?: Record<string, any>;
@ -53,6 +53,87 @@ type PersonalDashboardProps = {
builtInBestPractices?: BuiltInRoadmap[]; builtInBestPractices?: BuiltInRoadmap[];
}; };
type DashboardStatsProps = {
accountStreak?: StreakResponse;
topicsDoneToday?: number;
finishedProjectsCount?: number;
isLoading: boolean;
};
type DashboardStatItemProps = {
icon: LucideIcon;
iconClassName: string;
value: number;
label: string;
isLoading: boolean;
};
function DashboardStatItem(props: DashboardStatItemProps) {
const { icon: Icon, iconClassName, value, label, isLoading } = props;
return (
<div
className={cn(
'flex items-center gap-1.5 rounded-lg bg-slate-800/50 py-2 pl-3 pr-3',
{
'striped-loader-slate striped-loader-slate-fast text-transparent':
isLoading,
},
)}
>
<Icon
size={16}
className={cn(iconClassName, { 'text-transparent': isLoading })}
/>
<span>
<span className="tabular-nums">{value}</span> {label}
</span>
</div>
);
}
function DashboardStats(props: DashboardStatsProps) {
const {
accountStreak,
topicsDoneToday = 0,
finishedProjectsCount = 0,
isLoading,
} = props;
return (
<div className="container flex items-center justify-between gap-2 pb-2 pt-6 text-sm text-slate-400">
<div className="flex items-center gap-2">
<DashboardStatItem
icon={Zap}
iconClassName="text-yellow-500"
value={accountStreak?.count || 0}
label="day streak"
isLoading={isLoading}
/>
<DashboardStatItem
icon={CheckCircle}
iconClassName="text-green-500"
value={topicsDoneToday}
label="topics done today"
isLoading={isLoading}
/>
<DashboardStatItem
icon={FolderGit2}
iconClassName="text-blue-500"
value={finishedProjectsCount}
label="projects finished"
isLoading={isLoading}
/>
</div>
<button className="flex items-center gap-1 rounded-lg border border-transparent py-1.5 pl-3 pr-3 text-xs uppercase tracking-wide text-slate-400 hover:border-slate-800 hover:bg-slate-800">
<ChevronsDownUp className="size-3" />
<span>Collapse All</span>
</button>
</div>
);
}
export function PersonalDashboard(props: PersonalDashboardProps) { export function PersonalDashboard(props: PersonalDashboardProps) {
const { const {
builtInRoleRoadmaps = [], builtInRoleRoadmaps = [],
@ -236,181 +317,130 @@ export function PersonalDashboard(props: PersonalDashboardProps) {
const { username } = personalDashboardDetails || {}; const { username } = personalDashboardDetails || {};
// later on it will be switching of version based on localstorage
if (true) {
return (
<div className="min-h-screen bg-slate-900">
<FavoriteRoadmaps
progress={learningRoadmapsToShow}
customRoadmaps={customRoadmaps}
aiRoadmaps={aiGeneratedRoadmaps}
projects={enrichedProjects || []}
isLoading={isLoading}
/>
</div>
);
}
return ( return (
<section className="container"> <div>
{isLoading ? ( <DashboardStats
<div className="h-7 w-1/4 animate-pulse rounded-lg bg-gray-200"></div>
) : (
<div className="flex flex-col items-start justify-between gap-1 sm:flex-row sm:items-center">
<h2 className="text-lg font-medium">
Hi {name}, good {getCurrentPeriod()}!
</h2>
<a
href="/home"
className="rounded-full bg-gray-200 px-2.5 py-1 text-xs font-medium text-gray-700 hover:bg-gray-300 hover:text-black"
>
Visit Homepage
</a>
</div>
)}
<div className="mt-4 grid grid-cols-1 gap-2 sm:grid-cols-2 md:grid-cols-4">
{isLoading ? (
<>
<DashboardCardSkeleton />
<DashboardCardSkeleton />
<DashboardCardSkeleton />
<DashboardCardSkeleton />
</>
) : (
<>
<DashboardCard
imgUrl={avatarLink}
title={name!}
description={
username ? 'View your profile' : 'Setup your profile'
}
href={username ? `/u/${username}` : '/account/update-profile'}
{...(username && {
externalLinkIcon: PencilIcon,
externalLinkHref: '/account/update-profile',
externalLinkText: 'Edit',
})}
className={
!username
? 'border-dashed border-gray-500 bg-gray-100 hover:border-gray-500 hover:bg-gray-200'
: ''
}
/>
<DashboardCard
icon={BookEmoji}
title="Visit Roadmaps"
description="Learn new skills"
href="/roadmaps"
/>
<DashboardCard
icon={ConstructionEmoji}
title="Build Projects"
description="Practice what you learn"
href="/projects"
/>
<DashboardCard
icon={CheckEmoji}
title="Best Practices"
description="Do things the right way"
href="/best-practices"
/>
</>
)}
</div>
<ProgressStack
progresses={learningRoadmapsToShow}
projects={enrichedProjects || []}
isLoading={isLoading} isLoading={isLoading}
accountStreak={accountStreak} accountStreak={accountStreak}
topicDoneToday={personalDashboardDetails?.topicDoneToday || 0} topicsDoneToday={personalDashboardDetails?.topicDoneToday}
finishedProjectsCount={
enrichedProjects?.filter((p) => p.submittedAt && p.repositoryUrl)
.length
}
/> />
<ListDashboardCustomProgress <FavoriteRoadmaps
progresses={customRoadmaps} progress={learningRoadmapsToShow}
isLoading={isLoading} customRoadmaps={customRoadmaps}
/> aiRoadmaps={aiGeneratedRoadmaps}
projects={enrichedProjects || []}
<DashboardAiRoadmaps
roadmaps={aiGeneratedRoadmaps}
isLoading={isLoading} isLoading={isLoading}
/> />
<RecommendedRoadmaps <div className="relative mt-6 border-t border-t-[#1e293c] pt-12">
roadmaps={recommendedRoadmaps} <div className="container">
isLoading={isLoading} <h2 className="text-md font-regular absolute -top-[17px] flex rounded-lg border border-[#1e293c] bg-slate-900 px-3 py-1 text-slate-400 sm:left-1/2 sm:-translate-x-1/2">
/> Role Based Roadmaps
</section> </h2>
);
}
type DashboardCardProps = { <div className="grid grid-cols-1 gap-2 sm:grid-cols-2 md:grid-cols-3">
icon?: JSXElementConstructor<any>; {builtInRoleRoadmaps.map((roadmap) => {
imgUrl?: string; const roadmapProgress = learningRoadmapsToShow.find(
title: string; (lr) => lr.resourceId === roadmap.id,
description: string; );
href: string;
externalLinkIcon?: LucideIcon; const percentageDone =
externalLinkText?: string; (((roadmapProgress?.skipped || 0) +
externalLinkHref?: string; (roadmapProgress?.done || 0)) /
className?: string; (roadmapProgress?.total || 1)) *
}; 100;
return (
<HeroRoadmap
key={roadmap.id}
resourceId={roadmap.id}
resourceType="roadmap"
resourceTitle={roadmap.title}
isFavorite={roadmap.isFavorite}
percentageDone={percentageDone}
isNew={roadmap.isNew}
url={`/${roadmap.id}`}
/>
);
})}
</div>
</div>
</div>
function DashboardCard(props: DashboardCardProps) { <div className="relative mt-12 border-t border-t-[#1e293c] pt-12">
const { <div className="container">
icon: Icon, <h2 className="text-md font-regular absolute -top-[17px] flex rounded-lg border border-[#1e293c] bg-slate-900 px-3 py-1 text-slate-400 sm:left-1/2 sm:-translate-x-1/2">
imgUrl, Skill Based Roadmaps
title, </h2>
description,
href,
externalLinkHref,
externalLinkIcon: ExternalLinkIcon,
externalLinkText,
className,
} = props;
return ( <div className="grid grid-cols-1 gap-2 sm:grid-cols-2 md:grid-cols-3">
<div className={cn('relative overflow-hidden', className)}> {builtInSkillRoadmaps.map((roadmap) => {
<a const roadmapProgress = learningRoadmapsToShow.find(
href={href} (lr) => lr.resourceId === roadmap.id,
className="flex flex-col rounded-lg border border-gray-300 bg-white hover:border-gray-400 hover:bg-gray-50" );
>
{Icon && ( const percentageDone =
<div className="px-4 pb-3 pt-4"> (((roadmapProgress?.skipped || 0) +
<Icon className="size-6" /> (roadmapProgress?.done || 0)) /
(roadmapProgress?.total || 1)) *
100;
return (
<HeroRoadmap
key={roadmap.id}
resourceId={roadmap.id}
resourceType="roadmap"
resourceTitle={roadmap.title}
isFavorite={roadmap.isFavorite}
percentageDone={percentageDone}
isNew={roadmap.isNew}
url={`/${roadmap.id}`}
/>
);
})}
</div> </div>
)} </div>
</div>
{imgUrl && ( <div className="relative mt-12 border-t border-t-[#1e293c] pt-12">
<div className="px-4 pb-1.5 pt-3.5"> <div className="container">
<img src={imgUrl} alt={title} className="size-8 rounded-full" /> <h2 className="text-md font-regular absolute -top-[17px] flex rounded-lg border border-[#1e293c] bg-slate-900 px-3 py-1 text-slate-400 sm:left-1/2 sm:-translate-x-1/2">
</div> Best Practices
)} </h2>
<div className="flex grow flex-col justify-center gap-0.5 p-4"> <div className="grid grid-cols-1 gap-2 sm:grid-cols-2 md:grid-cols-3">
<h3 className="truncate font-medium text-black">{title}</h3> {builtInBestPractices.map((roadmap) => {
<p className="text-xs text-black">{description}</p> const roadmapProgress = learningRoadmapsToShow.find(
(lr) => lr.resourceId === roadmap.id,
);
const percentageDone =
(((roadmapProgress?.skipped || 0) +
(roadmapProgress?.done || 0)) /
(roadmapProgress?.total || 1)) *
100;
return (
<HeroRoadmap
key={roadmap.id}
resourceId={roadmap.id}
resourceType="best-practice"
resourceTitle={roadmap.title}
isFavorite={roadmap.isFavorite}
percentageDone={percentageDone}
isNew={roadmap.isNew}
url={`/best-practices/${roadmap.id}`}
/>
);
})}
</div>
</div> </div>
</a> </div>
{externalLinkHref && (
<a
href={externalLinkHref}
className="absolute right-1 top-1 flex items-center gap-1.5 rounded-md bg-gray-200 p-1 px-2 text-xs text-gray-600 hover:bg-gray-300 hover:text-black"
>
{ExternalLinkIcon && <ExternalLinkIcon className="size-3" />}
{externalLinkText}
</a>
)}
</div> </div>
); );
} }
function DashboardCardSkeleton() {
return (
<div className="h-[128px] animate-pulse rounded-lg border border-gray-300 bg-white"></div>
);
}

@ -8,6 +8,8 @@ import {
ChevronUp, ChevronUp,
Eye, Eye,
EyeOff, EyeOff,
CircleDashed,
Circle,
} from 'lucide-react'; } from 'lucide-react';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import type { ResourceType } from '../../lib/resource-progress.ts'; import type { ResourceType } from '../../lib/resource-progress.ts';
@ -38,6 +40,7 @@ type ProgressRoadmapProps = {
isFavorite?: boolean; isFavorite?: boolean;
isTrackable?: boolean; isTrackable?: boolean;
isNew?: boolean;
}; };
export function HeroRoadmap(props: ProgressRoadmapProps) { export function HeroRoadmap(props: ProgressRoadmapProps) {
@ -50,6 +53,7 @@ export function HeroRoadmap(props: ProgressRoadmapProps) {
isFavorite, isFavorite,
allowFavorite = true, allowFavorite = true,
isTrackable = true, isTrackable = true,
isNew = false,
} = props; } = props;
return ( return (
@ -82,6 +86,16 @@ export function HeroRoadmap(props: ProgressRoadmapProps) {
favorite={isFavorite} favorite={isFavorite}
/> />
)} )}
{isNew && (
<span className="absolute bottom-1.5 right-2 flex items-center rounded-br rounded-tl text-xs font-medium text-purple-300">
<span className="mr-1.5 flex h-2 w-2">
<span className="absolute inline-flex h-2 w-2 animate-ping rounded-full bg-purple-400 opacity-75" />
<span className="relative inline-flex h-2 w-2 rounded-full bg-purple-500" />
</span>
New
</span>
)}
</a> </a>
); );
} }
@ -132,22 +146,22 @@ export function HeroProject({ project }: HeroProjectProps) {
return ( return (
<a <a
href={`/projects/${project.projectId}`} href={`/projects/${project.projectId}`}
className="group relative flex flex-col justify-between gap-2 rounded-md border border-slate-800 bg-slate-900 p-4 hover:border-slate-600" className="group relative flex flex-col justify-between gap-2 rounded-md border border-slate-800 bg-slate-900 p-3.5 hover:border-slate-600"
> >
<div className="relative z-10 flex items-start justify-between gap-2"> <div className="relative z-10 flex items-start justify-between gap-2">
<h3 className="truncate font-medium text-slate-200 group-hover:text-slate-100"> <h3 className="truncate font-medium text-slate-300 group-hover:text-slate-100">
{project.title} {project.title}
</h3> </h3>
<span <span
className={`flex-shrink-0 rounded-full px-2 py-0.5 text-xs ${ className={cn(
project.submittedAt && project.repositoryUrl 'absolute -right-2 -top-2 flex flex-shrink-0 items-center gap-1 rounded-full text-xs uppercase tracking-wide',
? 'bg-green-950 text-green-200' {
: 'bg-yellow-950 text-yellow-200' 'text-green-600/50': project.submittedAt && project.repositoryUrl,
}`} 'text-yellow-600': !project.submittedAt || !project.repositoryUrl,
},
)}
> >
{project.submittedAt && project.repositoryUrl {project.submittedAt && project.repositoryUrl ? 'Done' : ''}
? 'Submitted'
: 'Started'}
</span> </span>
</div> </div>
<div className="relative z-10 flex items-center gap-2 text-xs text-slate-400"> <div className="relative z-10 flex items-center gap-2 text-xs text-slate-400">
@ -290,14 +304,14 @@ export function FavoriteRoadmaps(props: FavoriteRoadmapsProps) {
</div> </div>
</div> </div>
<div className="border-b border-b-slate-800/70"> <div className="">
<div className="container"> <div className="container">
<HeroTitle <HeroTitle
icon={ icon={
(<FolderKanban className="mr-1.5 h-[14px] w-[14px]" />) as any (<FolderKanban className="mr-1.5 h-[14px] w-[14px]" />) as any
} }
isLoading={isLoading} isLoading={isLoading}
title="Your projects" title="Your active projects"
rightContent={ rightContent={
completedProjects.length > 0 && ( completedProjects.length > 0 && (
<button <button

@ -20,6 +20,7 @@ const enrichedRoleRoadmaps = roleRoadmaps
description: frontmatter.briefDescription, description: frontmatter.briefDescription,
relatedRoadmapIds: frontmatter.relatedRoadmaps, relatedRoadmapIds: frontmatter.relatedRoadmaps,
renderer: frontmatter.renderer, renderer: frontmatter.renderer,
isNew: frontmatter.isNew,
metadata: { metadata: {
tags: frontmatter.tags, tags: frontmatter.tags,
}, },
@ -38,6 +39,7 @@ const enrichedSkillRoadmaps = skillRoadmaps
description: frontmatter.briefDescription, description: frontmatter.briefDescription,
relatedRoadmapIds: frontmatter.relatedRoadmaps, relatedRoadmapIds: frontmatter.relatedRoadmaps,
renderer: frontmatter.renderer, renderer: frontmatter.renderer,
isNew: frontmatter.isNew,
metadata: { metadata: {
tags: frontmatter.tags, tags: frontmatter.tags,
}, },

@ -140,6 +140,10 @@ a > code:before {
animation: barberpole 30s linear infinite; animation: barberpole 30s linear infinite;
} }
.striped-loader-slate-fast {
animation: barberpole 10s linear infinite;
}
@keyframes barberpole { @keyframes barberpole {
100% { 100% {
background-position: 100% 100%; background-position: 100% 100%;

Loading…
Cancel
Save