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 { httpGet } from '../../lib/http';
import { cn } from '../../../editor/utils/classname';
import { useParams } from '../../hooks/use-params';
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 type { TeamListResponse } from '../TeamDropdown/TeamDropdown';
import { DashboardTabButton } from './DashboardTabButton';
import { PersonalDashboard, type BuiltInRoadmap } from './PersonalDashboard';
import { TeamDashboard } from './TeamDashboard';
import { getUser } from '../../lib/jwt';
import { useParams } from '../../hooks/use-params';
import { cn } from '../../../editor/utils/classname';
type DashboardPageProps = {
builtInRoleRoadmaps?: BuiltInRoadmap[];
@ -69,55 +69,57 @@ export function DashboardPage(props: DashboardPageProps) {
return (
<>
<div
className={cn('bg-[#151b2e] py-5', {
className={cn('bg-slate-900', {
'striped-loader-slate': isLoading,
})}
>
<div className="container flex flex-wrap items-center gap-1.5">
<DashboardTabButton
label="Personal"
isActive={!selectedTeamId && !isTeamPage}
href="/dashboard"
avatar={userAvatar}
/>
<div className="bg-slate-800/30 py-5">
<div className="container flex flex-wrap items-center gap-1.5">
<DashboardTabButton
label="Personal"
isActive={!selectedTeamId && !isTeamPage}
href="/dashboard"
avatar={userAvatar}
/>
{!isLoading && (
<>
{teamList.map((team) => {
const { avatar } = team;
const avatarUrl = avatar
? `${import.meta.env.PUBLIC_AVATAR_BASE_URL}/${avatar}`
: '/images/default-avatar.png';
return (
<DashboardTabButton
key={team._id}
label={team.name}
isActive={team._id === selectedTeamId}
{...(team.status === 'invited'
? {
href: `/respond-invite?i=${team.memberId}`,
}
: {
href: `/team?t=${team._id}`,
})}
avatar={avatarUrl}
/>
);
})}
<DashboardTabButton
label="+ Create Team"
isActive={false}
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"
/>
</>
)}
{!isLoading && (
<>
{teamList.map((team) => {
const { avatar } = team;
const avatarUrl = avatar
? `${import.meta.env.PUBLIC_AVATAR_BASE_URL}/${avatar}`
: '/images/default-avatar.png';
return (
<DashboardTabButton
key={team._id}
label={team.name}
isActive={team._id === selectedTeamId}
{...(team.status === 'invited'
? {
href: `/respond-invite?i=${team.memberId}`,
}
: {
href: `/team?t=${team._id}`,
})}
avatar={avatarUrl}
/>
);
})}
<DashboardTabButton
label="+ Create Team"
isActive={false}
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"
/>
</>
)}
</div>
</div>
</div>
<div className="">
{!selectedTeamId && !isTeamPage && (
<div className="min-h-screen pb-20">
<div className="bg-slate-900">
<PersonalDashboard
builtInRoleRoadmaps={builtInRoleRoadmaps}
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 { $accountStreak, type StreakResponse } from '../../stores/streak';
import { CheckEmoji } from '../ReactIcons/CheckEmoji.tsx';
import { ConstructionEmoji } from '../ReactIcons/ConstructionEmoji.tsx';
import { BookEmoji } from '../ReactIcons/BookEmoji.tsx';
import { DashboardAiRoadmaps } from './DashboardAiRoadmaps.tsx';
import {
CheckCircle,
ChevronsDownUp,
FolderGit2,
Zap,
type LucideIcon,
} from 'lucide-react';
import { useEffect, useState } from 'react';
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 { httpGet } from '../../lib/http';
import type { AllowedRoadmapRenderer } from '../../lib/roadmap.ts';
import { $accountStreak, type StreakResponse } from '../../stores/streak';
import type { PageType } from '../CommandMenu/CommandMenu';
import {
FavoriteRoadmaps,
HeroRoadmap,
type AIRoadmapType,
} from '../HeroSection/FavoriteRoadmaps.tsx';
import type { ProjectStatusDocument } from '../Projects/ListProjectSolutions';
import type { UserProgress } from '../TeamProgress/TeamProgressPage';
type UserDashboardResponse = {
name: string;
@ -42,6 +41,7 @@ export type BuiltInRoadmap = {
title: string;
description: string;
isFavorite?: boolean;
isNew?: boolean;
relatedRoadmapIds?: string[];
renderer?: AllowedRoadmapRenderer;
metadata?: Record<string, any>;
@ -53,6 +53,87 @@ type PersonalDashboardProps = {
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) {
const {
builtInRoleRoadmaps = [],
@ -236,181 +317,130 @@ export function PersonalDashboard(props: PersonalDashboardProps) {
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 (
<section className="container">
{isLoading ? (
<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 || []}
<div>
<DashboardStats
isLoading={isLoading}
accountStreak={accountStreak}
topicDoneToday={personalDashboardDetails?.topicDoneToday || 0}
topicsDoneToday={personalDashboardDetails?.topicDoneToday}
finishedProjectsCount={
enrichedProjects?.filter((p) => p.submittedAt && p.repositoryUrl)
.length
}
/>
<ListDashboardCustomProgress
progresses={customRoadmaps}
isLoading={isLoading}
/>
<DashboardAiRoadmaps
roadmaps={aiGeneratedRoadmaps}
<FavoriteRoadmaps
progress={learningRoadmapsToShow}
customRoadmaps={customRoadmaps}
aiRoadmaps={aiGeneratedRoadmaps}
projects={enrichedProjects || []}
isLoading={isLoading}
/>
<RecommendedRoadmaps
roadmaps={recommendedRoadmaps}
isLoading={isLoading}
/>
</section>
);
}
<div className="relative mt-6 border-t border-t-[#1e293c] pt-12">
<div className="container">
<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
</h2>
type DashboardCardProps = {
icon?: JSXElementConstructor<any>;
imgUrl?: string;
title: string;
description: string;
href: string;
externalLinkIcon?: LucideIcon;
externalLinkText?: string;
externalLinkHref?: string;
className?: string;
};
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2 md:grid-cols-3">
{builtInRoleRoadmaps.map((roadmap) => {
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="roadmap"
resourceTitle={roadmap.title}
isFavorite={roadmap.isFavorite}
percentageDone={percentageDone}
isNew={roadmap.isNew}
url={`/${roadmap.id}`}
/>
);
})}
</div>
</div>
</div>
function DashboardCard(props: DashboardCardProps) {
const {
icon: Icon,
imgUrl,
title,
description,
href,
externalLinkHref,
externalLinkIcon: ExternalLinkIcon,
externalLinkText,
className,
} = props;
<div className="relative mt-12 border-t border-t-[#1e293c] pt-12">
<div className="container">
<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">
Skill Based Roadmaps
</h2>
return (
<div className={cn('relative overflow-hidden', className)}>
<a
href={href}
className="flex flex-col rounded-lg border border-gray-300 bg-white hover:border-gray-400 hover:bg-gray-50"
>
{Icon && (
<div className="px-4 pb-3 pt-4">
<Icon className="size-6" />
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2 md:grid-cols-3">
{builtInSkillRoadmaps.map((roadmap) => {
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="roadmap"
resourceTitle={roadmap.title}
isFavorite={roadmap.isFavorite}
percentageDone={percentageDone}
isNew={roadmap.isNew}
url={`/${roadmap.id}`}
/>
);
})}
</div>
)}
</div>
</div>
{imgUrl && (
<div className="px-4 pb-1.5 pt-3.5">
<img src={imgUrl} alt={title} className="size-8 rounded-full" />
</div>
)}
<div className="relative mt-12 border-t border-t-[#1e293c] pt-12">
<div className="container">
<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">
Best Practices
</h2>
<div className="flex grow flex-col justify-center gap-0.5 p-4">
<h3 className="truncate font-medium text-black">{title}</h3>
<p className="text-xs text-black">{description}</p>
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2 md:grid-cols-3">
{builtInBestPractices.map((roadmap) => {
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>
</a>
{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,
Eye,
EyeOff,
CircleDashed,
Circle,
} from 'lucide-react';
import type { ReactNode } from 'react';
import type { ResourceType } from '../../lib/resource-progress.ts';
@ -38,6 +40,7 @@ type ProgressRoadmapProps = {
isFavorite?: boolean;
isTrackable?: boolean;
isNew?: boolean;
};
export function HeroRoadmap(props: ProgressRoadmapProps) {
@ -50,6 +53,7 @@ export function HeroRoadmap(props: ProgressRoadmapProps) {
isFavorite,
allowFavorite = true,
isTrackable = true,
isNew = false,
} = props;
return (
@ -82,6 +86,16 @@ export function HeroRoadmap(props: ProgressRoadmapProps) {
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>
);
}
@ -132,22 +146,22 @@ export function HeroProject({ project }: HeroProjectProps) {
return (
<a
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">
<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}
</h3>
<span
className={`flex-shrink-0 rounded-full px-2 py-0.5 text-xs ${
project.submittedAt && project.repositoryUrl
? 'bg-green-950 text-green-200'
: 'bg-yellow-950 text-yellow-200'
}`}
className={cn(
'absolute -right-2 -top-2 flex flex-shrink-0 items-center gap-1 rounded-full text-xs uppercase tracking-wide',
{
'text-green-600/50': project.submittedAt && project.repositoryUrl,
'text-yellow-600': !project.submittedAt || !project.repositoryUrl,
},
)}
>
{project.submittedAt && project.repositoryUrl
? 'Submitted'
: 'Started'}
{project.submittedAt && project.repositoryUrl ? 'Done' : ''}
</span>
</div>
<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 className="border-b border-b-slate-800/70">
<div className="">
<div className="container">
<HeroTitle
icon={
(<FolderKanban className="mr-1.5 h-[14px] w-[14px]" />) as any
}
isLoading={isLoading}
title="Your projects"
title="Your active projects"
rightContent={
completedProjects.length > 0 && (
<button

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

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

Loading…
Cancel
Save