mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2025-09-25 00:21:28 +02:00
Improve personal dashboard design
This commit is contained in:
@@ -4,11 +4,12 @@ import { useToast } from '../../hooks/use-toast';
|
|||||||
import { useStore } from '@nanostores/react';
|
import { useStore } from '@nanostores/react';
|
||||||
import { $teamList } from '../../stores/team';
|
import { $teamList } from '../../stores/team';
|
||||||
import type { TeamListResponse } from '../TeamDropdown/TeamDropdown';
|
import type { TeamListResponse } from '../TeamDropdown/TeamDropdown';
|
||||||
import { DashboardTab } from './DashboardTab';
|
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 { getUser } from '../../lib/jwt';
|
||||||
import { useParams } from '../../hooks/use-params';
|
import { useParams } from '../../hooks/use-params';
|
||||||
|
import { cn } from '../../../editor/utils/classname';
|
||||||
|
|
||||||
type DashboardPageProps = {
|
type DashboardPageProps = {
|
||||||
builtInRoleRoadmaps?: BuiltInRoadmap[];
|
builtInRoleRoadmaps?: BuiltInRoadmap[];
|
||||||
@@ -66,23 +67,20 @@ export function DashboardPage(props: DashboardPageProps) {
|
|||||||
: '/images/default-avatar.png';
|
: '/images/default-avatar.png';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 pb-20 pt-8">
|
<>
|
||||||
<div className="container">
|
<div
|
||||||
<div className="mb-6 flex flex-wrap items-center gap-1.5 sm:mb-8">
|
className={cn('bg-[#151b2e] py-5', {
|
||||||
<DashboardTab
|
'striped-loader-slate': isLoading,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div className="container flex flex-wrap items-center gap-1.5">
|
||||||
|
<DashboardTabButton
|
||||||
label="Personal"
|
label="Personal"
|
||||||
isActive={!selectedTeamId && !isTeamPage}
|
isActive={!selectedTeamId && !isTeamPage}
|
||||||
href="/dashboard"
|
href="/dashboard"
|
||||||
avatar={userAvatar}
|
avatar={userAvatar}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{isLoading && (
|
|
||||||
<>
|
|
||||||
<DashboardTabSkeleton />
|
|
||||||
<DashboardTabSkeleton />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!isLoading && (
|
{!isLoading && (
|
||||||
<>
|
<>
|
||||||
{teamList.map((team) => {
|
{teamList.map((team) => {
|
||||||
@@ -91,7 +89,7 @@ export function DashboardPage(props: DashboardPageProps) {
|
|||||||
? `${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 (
|
||||||
<DashboardTab
|
<DashboardTabButton
|
||||||
key={team._id}
|
key={team._id}
|
||||||
label={team.name}
|
label={team.name}
|
||||||
isActive={team._id === selectedTeamId}
|
isActive={team._id === selectedTeamId}
|
||||||
@@ -106,33 +104,39 @@ export function DashboardPage(props: DashboardPageProps) {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
<DashboardTab
|
<DashboardTabButton
|
||||||
label="+ Create Team"
|
label="+ Create Team"
|
||||||
isActive={false}
|
isActive={false}
|
||||||
href="/team/new"
|
href="/team/new"
|
||||||
className="border border-dashed border-gray-300 bg-transparent px-3 text-[13px] text-sm text-gray-500 hover:border-gray-600 hover:text-black"
|
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 && (
|
{!selectedTeamId && !isTeamPage && (
|
||||||
<PersonalDashboard
|
<div className="min-h-screen pb-20">
|
||||||
builtInRoleRoadmaps={builtInRoleRoadmaps}
|
<PersonalDashboard
|
||||||
builtInSkillRoadmaps={builtInSkillRoadmaps}
|
builtInRoleRoadmaps={builtInRoleRoadmaps}
|
||||||
builtInBestPractices={builtInBestPractices}
|
builtInSkillRoadmaps={builtInSkillRoadmaps}
|
||||||
/>
|
builtInBestPractices={builtInBestPractices}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(selectedTeamId || isTeamPage) && (
|
{(selectedTeamId || isTeamPage) && (
|
||||||
<TeamDashboard
|
<div className="container">
|
||||||
builtInRoleRoadmaps={builtInRoleRoadmaps!}
|
<TeamDashboard
|
||||||
builtInSkillRoadmaps={builtInSkillRoadmaps!}
|
builtInRoleRoadmaps={builtInRoleRoadmaps!}
|
||||||
teamId={selectedTeamId!}
|
builtInSkillRoadmaps={builtInSkillRoadmaps!}
|
||||||
/>
|
teamId={selectedTeamId!}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -11,7 +11,7 @@ type DashboardTabProps = {
|
|||||||
icon?: ReactNode;
|
icon?: ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function DashboardTab(props: DashboardTabProps) {
|
export function DashboardTabButton(props: DashboardTabProps) {
|
||||||
const { isActive, onClick, label, className, href, avatar, icon } = props;
|
const { isActive, onClick, label, className, href, avatar, icon } = props;
|
||||||
|
|
||||||
const Slot = href ? 'a' : 'button';
|
const Slot = href ? 'a' : 'button';
|
||||||
@@ -20,8 +20,10 @@ export function DashboardTab(props: DashboardTabProps) {
|
|||||||
<Slot
|
<Slot
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex h-[30px] shrink-0 items-center gap-1 rounded-md border bg-white p-1.5 px-2 text-sm leading-none text-gray-600',
|
'flex h-[30px] shrink-0 items-center gap-1 rounded-md border border-slate-700 bg-slate-800 p-1.5 pl-2 pr-3 text-sm leading-none text-gray-400 transition-colors hover:bg-slate-700',
|
||||||
isActive ? 'border-gray-500 bg-gray-200 text-gray-900' : '',
|
isActive
|
||||||
|
? 'border-slate-200 bg-slate-200 text-gray-900 hover:bg-slate-200'
|
||||||
|
: '',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...(href ? { href } : {})}
|
{...(href ? { href } : {})}
|
||||||
@@ -30,7 +32,7 @@ export function DashboardTab(props: DashboardTabProps) {
|
|||||||
<img
|
<img
|
||||||
src={avatar}
|
src={avatar}
|
||||||
alt="avatar"
|
alt="avatar"
|
||||||
className="h-4 w-4 mr-0.5 rounded-full object-cover"
|
className="mr-0.5 h-4 w-4 rounded-full object-cover"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{icon}
|
{icon}
|
@@ -18,6 +18,10 @@ import type { AllowedProfileVisibility } from '../../api/user.ts';
|
|||||||
import { PencilIcon, type LucideIcon } from 'lucide-react';
|
import { PencilIcon, type LucideIcon } from 'lucide-react';
|
||||||
import { cn } from '../../lib/classname.ts';
|
import { cn } from '../../lib/classname.ts';
|
||||||
import type { AllowedRoadmapRenderer } from '../../lib/roadmap.ts';
|
import type { AllowedRoadmapRenderer } from '../../lib/roadmap.ts';
|
||||||
|
import {
|
||||||
|
FavoriteRoadmaps,
|
||||||
|
type AIRoadmapType,
|
||||||
|
} from '../HeroSection/FavoriteRoadmaps.tsx';
|
||||||
|
|
||||||
type UserDashboardResponse = {
|
type UserDashboardResponse = {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -28,11 +32,7 @@ type UserDashboardResponse = {
|
|||||||
profileVisibility: AllowedProfileVisibility;
|
profileVisibility: AllowedProfileVisibility;
|
||||||
progresses: UserProgress[];
|
progresses: UserProgress[];
|
||||||
projects: ProjectStatusDocument[];
|
projects: ProjectStatusDocument[];
|
||||||
aiRoadmaps: {
|
aiRoadmaps: AIRoadmapType[];
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
slug: string;
|
|
||||||
}[];
|
|
||||||
topicDoneToday: number;
|
topicDoneToday: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -138,7 +138,9 @@ export function PersonalDashboard(props: PersonalDashboardProps) {
|
|||||||
return () => window.removeEventListener('refresh-favorites', loadProgress);
|
return () => window.removeEventListener('refresh-favorites', loadProgress);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const learningRoadmapsToShow = (personalDashboardDetails?.progresses || [])
|
const learningRoadmapsToShow: UserProgress[] = (
|
||||||
|
personalDashboardDetails?.progresses || []
|
||||||
|
)
|
||||||
.filter((progress) => !progress.isCustomResource)
|
.filter((progress) => !progress.isCustomResource)
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
const updatedAtA = new Date(a.updatedAt);
|
const updatedAtA = new Date(a.updatedAt);
|
||||||
@@ -156,7 +158,10 @@ export function PersonalDashboard(props: PersonalDashboardProps) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const aiGeneratedRoadmaps = personalDashboardDetails?.aiRoadmaps || [];
|
const aiGeneratedRoadmaps = personalDashboardDetails?.aiRoadmaps || [];
|
||||||
const customRoadmaps = (personalDashboardDetails?.progresses || [])
|
|
||||||
|
const customRoadmaps: UserProgress[] = (
|
||||||
|
personalDashboardDetails?.progresses || []
|
||||||
|
)
|
||||||
.filter((progress) => progress.isCustomResource)
|
.filter((progress) => progress.isCustomResource)
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
const updatedAtA = new Date(a.updatedAt);
|
const updatedAtA = new Date(a.updatedAt);
|
||||||
@@ -231,8 +236,23 @@ 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>
|
<section className="container">
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="h-7 w-1/4 animate-pulse rounded-lg bg-gray-200"></div>
|
<div className="h-7 w-1/4 animate-pulse rounded-lg bg-gray-200"></div>
|
||||||
) : (
|
) : (
|
||||||
|
@@ -1,161 +1,274 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { FolderKanban, MapIcon, Plus, Sparkle, ThumbsUp } from 'lucide-react';
|
||||||
import { EmptyProgress } from './EmptyProgress';
|
import type { ReactNode } from 'react';
|
||||||
import { httpGet } from '../../lib/http';
|
import type { ResourceType } from '../../lib/resource-progress.ts';
|
||||||
import { HeroRoadmaps, type HeroTeamRoadmaps } from './HeroRoadmaps';
|
import { CreateRoadmapButton } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapButton.tsx';
|
||||||
import { isLoggedIn } from '../../lib/jwt';
|
import { MarkFavorite } from '../FeaturedItems/MarkFavorite.tsx';
|
||||||
import type { AllowedMemberRoles } from '../ShareOptions/ShareTeamMemberList.tsx';
|
import { CheckIcon } from '../ReactIcons/CheckIcon.tsx';
|
||||||
|
import { Spinner } from '../ReactIcons/Spinner.tsx';
|
||||||
|
import type { UserProgress } from '../TeamProgress/TeamProgressPage.tsx';
|
||||||
|
import type { ProjectStatusDocument } from '../Projects/ListProjectSolutions.tsx';
|
||||||
|
import { getRelativeTimeString } from '../../lib/date';
|
||||||
|
|
||||||
|
export type AIRoadmapType = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
slug: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ProgressRoadmapProps = {
|
||||||
|
url: string;
|
||||||
|
percentageDone: number;
|
||||||
|
allowFavorite?: boolean;
|
||||||
|
|
||||||
export type UserProgressResponse = {
|
|
||||||
resourceId: string;
|
resourceId: string;
|
||||||
resourceType: 'roadmap' | 'best-practice';
|
resourceType: ResourceType;
|
||||||
resourceTitle: string;
|
resourceTitle: string;
|
||||||
isFavorite: boolean;
|
isFavorite?: boolean;
|
||||||
done: number;
|
};
|
||||||
learning: number;
|
|
||||||
skipped: number;
|
|
||||||
total: number;
|
|
||||||
updatedAt: Date;
|
|
||||||
isCustomResource: boolean;
|
|
||||||
roadmapSlug?: string;
|
|
||||||
team?: {
|
|
||||||
name: string;
|
|
||||||
id: string;
|
|
||||||
role: AllowedMemberRoles;
|
|
||||||
};
|
|
||||||
}[];
|
|
||||||
|
|
||||||
function renderProgress(progressList: UserProgressResponse) {
|
export function HeroRoadmap(props: ProgressRoadmapProps) {
|
||||||
progressList.forEach((progress) => {
|
const {
|
||||||
const href =
|
url,
|
||||||
progress.resourceType === 'best-practice'
|
percentageDone,
|
||||||
? `/best-practices/${progress.resourceId}`
|
resourceType,
|
||||||
: `/${progress.resourceId}`;
|
resourceId,
|
||||||
const element = document.querySelector(`a[href="${href}"]`);
|
resourceTitle,
|
||||||
if (!element) {
|
isFavorite,
|
||||||
return;
|
allowFavorite = true,
|
||||||
}
|
} = props;
|
||||||
|
|
||||||
window.dispatchEvent(
|
|
||||||
new CustomEvent('mark-favorite', {
|
|
||||||
detail: {
|
|
||||||
resourceId: progress.resourceId,
|
|
||||||
resourceType: progress.resourceType,
|
|
||||||
isFavorite: progress.isFavorite,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const totalDone = progress.done + progress.skipped;
|
|
||||||
const percentageDone = (totalDone / progress.total) * 100;
|
|
||||||
|
|
||||||
const progressBar: HTMLElement | null =
|
|
||||||
element.querySelector('[data-progress]');
|
|
||||||
if (progressBar) {
|
|
||||||
progressBar.style.width = `${percentageDone}%`;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProgressResponse = UserProgressResponse;
|
|
||||||
|
|
||||||
export function FavoriteRoadmaps() {
|
|
||||||
const isAuthenticated = isLoggedIn();
|
|
||||||
if (!isAuthenticated) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [isPreparing, setIsPreparing] = useState(true);
|
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
|
||||||
const [progress, setProgress] = useState<ProgressResponse>([]);
|
|
||||||
const [containerOpacity, setContainerOpacity] = useState(0);
|
|
||||||
|
|
||||||
function showProgressContainer() {
|
|
||||||
const heroEl = document.getElementById('hero-text')!;
|
|
||||||
if (!heroEl) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
heroEl.classList.add('opacity-0');
|
|
||||||
setTimeout(() => {
|
|
||||||
heroEl.parentElement?.removeChild(heroEl);
|
|
||||||
setIsPreparing(false);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
setContainerOpacity(100);
|
|
||||||
}, 50);
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadProgress() {
|
|
||||||
setIsLoading(true);
|
|
||||||
|
|
||||||
const { response: progressList, error } = await httpGet<ProgressResponse>(
|
|
||||||
`${import.meta.env.PUBLIC_API_URL}/v1-get-hero-roadmaps`,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (error || !progressList) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setProgress(progressList);
|
|
||||||
setIsLoading(false);
|
|
||||||
showProgressContainer();
|
|
||||||
|
|
||||||
// render progress on featured items
|
|
||||||
renderProgress(progressList);
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
loadProgress().finally(() => {
|
|
||||||
setIsLoading(false);
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
window.addEventListener('refresh-favorites', loadProgress);
|
|
||||||
return () => window.removeEventListener('refresh-favorites', loadProgress);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (isPreparing) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasProgress = progress?.length > 0;
|
|
||||||
const customRoadmaps = progress?.filter(
|
|
||||||
(p) => p.isCustomResource && !p.team?.name,
|
|
||||||
);
|
|
||||||
const defaultRoadmaps = progress?.filter((p) => !p.isCustomResource);
|
|
||||||
const teamRoadmaps: HeroTeamRoadmaps = progress
|
|
||||||
?.filter((p) => p.isCustomResource && p.team?.name)
|
|
||||||
.reduce((acc: HeroTeamRoadmaps, curr) => {
|
|
||||||
const currTeam = curr.team!;
|
|
||||||
if (!acc[currTeam.name]) {
|
|
||||||
acc[currTeam.name] = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
acc[currTeam.name].push(curr);
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<a
|
||||||
className={`transition-opacity duration-500 opacity-${containerOpacity}`}
|
href={url}
|
||||||
|
className="relative flex flex-col overflow-hidden rounded-md border border-slate-800 bg-slate-900 p-3 text-sm text-slate-400 hover:border-slate-600 hover:text-slate-300"
|
||||||
>
|
>
|
||||||
<div
|
<span title={resourceTitle} className="relative z-20 truncate">
|
||||||
className={`flex min-h-[192px] bg-gradient-to-b sm:min-h-[280px] ${
|
{resourceTitle}
|
||||||
hasProgress && `border-t border-t-[#1e293c]`
|
</span>
|
||||||
}`}
|
|
||||||
>
|
<span
|
||||||
<div className="container min-h-full">
|
className="absolute bottom-0 left-0 top-0 z-10 bg-[#172a3a]"
|
||||||
{!isLoading && progress?.length == 0 && <EmptyProgress />}
|
style={{ width: `${percentageDone}%` }}
|
||||||
{hasProgress && (
|
></span>
|
||||||
<HeroRoadmaps
|
|
||||||
teamRoadmaps={teamRoadmaps}
|
{allowFavorite && (
|
||||||
customRoadmaps={customRoadmaps}
|
<MarkFavorite
|
||||||
progress={defaultRoadmaps}
|
resourceId={resourceId}
|
||||||
isLoading={isLoading}
|
resourceType={resourceType}
|
||||||
/>
|
favorite={isFavorite}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type HeroTitleProps = {
|
||||||
|
icon: any;
|
||||||
|
isLoading?: boolean;
|
||||||
|
title: string | ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
function HeroTitle(props: HeroTitleProps) {
|
||||||
|
const { isLoading = false, title, icon } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<p className="mb-4 flex items-center text-sm text-gray-400">
|
||||||
|
{!isLoading && icon}
|
||||||
|
{isLoading && (
|
||||||
|
<span className="mr-1.5">
|
||||||
|
<Spinner />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{title}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type FavoriteRoadmapsProps = {
|
||||||
|
progress: UserProgress[];
|
||||||
|
projects: (ProjectStatusDocument & {
|
||||||
|
title: string;
|
||||||
|
})[];
|
||||||
|
customRoadmaps: UserProgress[];
|
||||||
|
aiRoadmaps: AIRoadmapType[];
|
||||||
|
isLoading: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type HeroProjectProps = {
|
||||||
|
project: ProjectStatusDocument & {
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<div className="relative z-10 flex items-start justify-between gap-2">
|
||||||
|
<h3 className="font-medium text-slate-200 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'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{project.submittedAt && project.repositoryUrl
|
||||||
|
? 'Submitted'
|
||||||
|
: 'In Progress'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="relative z-10 flex items-center gap-2 text-xs text-slate-400">
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<ThumbsUp className="h-3 w-3" />
|
||||||
|
{project.upvotes}
|
||||||
|
</span>
|
||||||
|
{project.startedAt && (
|
||||||
|
<span>Started {getRelativeTimeString(project.startedAt)}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="absolute inset-0 rounded-md bg-gradient-to-br from-slate-800/50 via-transparent to-transparent" />
|
||||||
|
{project.submittedAt && project.repositoryUrl && (
|
||||||
|
<div className="absolute inset-0 rounded-md bg-gradient-to-br from-green-950/20 via-transparent to-transparent" />
|
||||||
|
)}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FavoriteRoadmaps(props: FavoriteRoadmapsProps) {
|
||||||
|
const { progress, isLoading, customRoadmaps, aiRoadmaps, projects } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-5 pt-5">
|
||||||
|
<div className="border-b border-b-slate-800/70">
|
||||||
|
<div className="container">
|
||||||
|
<HeroTitle
|
||||||
|
icon={
|
||||||
|
(
|
||||||
|
<CheckIcon additionalClasses="mr-1.5 h-[14px] w-[14px]" />
|
||||||
|
) as any
|
||||||
|
}
|
||||||
|
isLoading={isLoading}
|
||||||
|
title="Your progress and bookmarks"
|
||||||
|
/>
|
||||||
|
{!isLoading && progress.length > 0 && (
|
||||||
|
<div className="grid grid-cols-1 gap-2 pb-5 sm:grid-cols-2 md:grid-cols-3">
|
||||||
|
{progress.map((resource) => (
|
||||||
|
<HeroRoadmap
|
||||||
|
key={`${resource.resourceType}-${resource.resourceId}`}
|
||||||
|
resourceId={resource.resourceId}
|
||||||
|
resourceType={resource.resourceType}
|
||||||
|
resourceTitle={resource.resourceTitle}
|
||||||
|
isFavorite={resource.isFavorite}
|
||||||
|
percentageDone={
|
||||||
|
((resource.skipped + resource.done) / resource.total) * 100
|
||||||
|
}
|
||||||
|
url={
|
||||||
|
resource.resourceType === 'roadmap'
|
||||||
|
? `/${resource.resourceId}`
|
||||||
|
: `/best-practices/${resource.resourceId}`
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<CreateRoadmapButton />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-b border-b-slate-800/70">
|
||||||
|
<div className="container">
|
||||||
|
<HeroTitle
|
||||||
|
icon={(<MapIcon className="mr-1.5 h-[14px] w-[14px]" />) as any}
|
||||||
|
isLoading={isLoading}
|
||||||
|
title="Your custom roadmaps"
|
||||||
|
/>
|
||||||
|
{!isLoading && customRoadmaps.length > 0 && (
|
||||||
|
<div className="grid grid-cols-1 gap-2 pb-5 sm:grid-cols-2 md:grid-cols-3">
|
||||||
|
{customRoadmaps.map((customRoadmap) => (
|
||||||
|
<HeroRoadmap
|
||||||
|
key={customRoadmap.resourceId}
|
||||||
|
resourceId={customRoadmap.resourceId}
|
||||||
|
resourceType={'roadmap'}
|
||||||
|
resourceTitle={customRoadmap.resourceTitle}
|
||||||
|
percentageDone={
|
||||||
|
((customRoadmap.skipped + customRoadmap.done) /
|
||||||
|
customRoadmap.total) *
|
||||||
|
100
|
||||||
|
}
|
||||||
|
url={`/r/${customRoadmap?.roadmapSlug}`}
|
||||||
|
allowFavorite={false}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<CreateRoadmapButton />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-b border-b-slate-800/70">
|
||||||
|
<div className="container">
|
||||||
|
<HeroTitle
|
||||||
|
icon={(<Sparkle className="mr-1.5 h-[14px] w-[14px]" />) as any}
|
||||||
|
isLoading={isLoading}
|
||||||
|
title="Your AI roadmaps"
|
||||||
|
/>
|
||||||
|
{!isLoading && aiRoadmaps.length > 0 && (
|
||||||
|
<div className="grid grid-cols-1 gap-2 pb-5 sm:grid-cols-2 md:grid-cols-3">
|
||||||
|
{aiRoadmaps.map((aiRoadmap) => (
|
||||||
|
<HeroRoadmap
|
||||||
|
key={aiRoadmap.id}
|
||||||
|
resourceId={aiRoadmap.id}
|
||||||
|
resourceType={'roadmap'}
|
||||||
|
resourceTitle={aiRoadmap.title}
|
||||||
|
url={`/ai/${aiRoadmap.slug}`}
|
||||||
|
percentageDone={0}
|
||||||
|
allowFavorite={false}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="/ai"
|
||||||
|
className={
|
||||||
|
'flex h-full w-full items-center justify-center gap-1 overflow-hidden rounded-md border border-dashed border-gray-800 p-3 text-sm text-gray-400 hover:border-gray-600 hover:bg-gray-900 hover:text-gray-300'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Plus size={16} />
|
||||||
|
Generate New
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-b border-b-slate-800/70">
|
||||||
|
<div className="container">
|
||||||
|
<HeroTitle
|
||||||
|
icon={
|
||||||
|
(<FolderKanban className="mr-1.5 h-[14px] w-[14px]" />) as any
|
||||||
|
}
|
||||||
|
isLoading={isLoading}
|
||||||
|
title="Your projects"
|
||||||
|
/>
|
||||||
|
{!isLoading && projects.length > 0 && (
|
||||||
|
<div className="grid grid-cols-1 gap-2 pb-5 sm:grid-cols-2 md:grid-cols-3">
|
||||||
|
{projects.map((project) => (
|
||||||
|
<HeroProject key={project._id} project={project} />
|
||||||
|
))}
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="/projects"
|
||||||
|
className="flex h-[120px] items-center justify-center gap-2 rounded-md border border-dashed border-slate-800 p-4 text-sm text-slate-400 hover:border-slate-600 hover:bg-slate-900/50 hover:text-slate-300"
|
||||||
|
>
|
||||||
|
<Plus size={16} />
|
||||||
|
Start a new project
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,264 +0,0 @@
|
|||||||
import type { UserProgressResponse } from './FavoriteRoadmaps';
|
|
||||||
import { CheckIcon } from '../ReactIcons/CheckIcon';
|
|
||||||
import { MarkFavorite } from '../FeaturedItems/MarkFavorite';
|
|
||||||
import { Spinner } from '../ReactIcons/Spinner';
|
|
||||||
import type { ResourceType } from '../../lib/resource-progress';
|
|
||||||
import { MapIcon, Users2 } from 'lucide-react';
|
|
||||||
import { CreateRoadmapButton } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapButton';
|
|
||||||
import { CreateRoadmapModal } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapModal';
|
|
||||||
import { type ReactNode, useState } from 'react';
|
|
||||||
import { FeatureAnnouncement } from '../FeatureAnnouncement.tsx';
|
|
||||||
|
|
||||||
type ProgressRoadmapProps = {
|
|
||||||
url: string;
|
|
||||||
percentageDone: number;
|
|
||||||
allowFavorite?: boolean;
|
|
||||||
|
|
||||||
resourceId: string;
|
|
||||||
resourceType: ResourceType;
|
|
||||||
resourceTitle: string;
|
|
||||||
isFavorite?: boolean;
|
|
||||||
};
|
|
||||||
function HeroRoadmap(props: ProgressRoadmapProps) {
|
|
||||||
const {
|
|
||||||
url,
|
|
||||||
percentageDone,
|
|
||||||
resourceType,
|
|
||||||
resourceId,
|
|
||||||
resourceTitle,
|
|
||||||
isFavorite,
|
|
||||||
allowFavorite = true,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<a
|
|
||||||
href={url}
|
|
||||||
className="relative flex flex-col overflow-hidden rounded-md border border-slate-800 bg-slate-900 p-3 text-sm text-slate-400 hover:border-slate-600 hover:text-slate-300"
|
|
||||||
>
|
|
||||||
<span className="relative z-20">{resourceTitle}</span>
|
|
||||||
|
|
||||||
<span
|
|
||||||
className="absolute bottom-0 left-0 top-0 z-10 bg-[#172a3a]"
|
|
||||||
style={{ width: `${percentageDone}%` }}
|
|
||||||
></span>
|
|
||||||
|
|
||||||
{allowFavorite && (
|
|
||||||
<MarkFavorite
|
|
||||||
resourceId={resourceId}
|
|
||||||
resourceType={resourceType}
|
|
||||||
favorite={isFavorite}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProgressTitleProps = {
|
|
||||||
icon: any;
|
|
||||||
isLoading?: boolean;
|
|
||||||
title: string | ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function HeroTitle(props: ProgressTitleProps) {
|
|
||||||
const { isLoading = false, title, icon } = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<p className="mb-4 flex items-center text-sm text-gray-400">
|
|
||||||
{!isLoading && icon}
|
|
||||||
{isLoading && (
|
|
||||||
<span className="mr-1.5">
|
|
||||||
<Spinner />
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{title}
|
|
||||||
</p>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
export type HeroTeamRoadmaps = Record<string, UserProgressResponse>;
|
|
||||||
|
|
||||||
type ProgressListProps = {
|
|
||||||
progress: UserProgressResponse;
|
|
||||||
customRoadmaps: UserProgressResponse;
|
|
||||||
teamRoadmaps?: HeroTeamRoadmaps;
|
|
||||||
isLoading?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function HeroRoadmaps(props: ProgressListProps) {
|
|
||||||
const {
|
|
||||||
teamRoadmaps = {},
|
|
||||||
progress,
|
|
||||||
isLoading = false,
|
|
||||||
customRoadmaps,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const [isCreatingRoadmap, setIsCreatingRoadmap] = useState(false);
|
|
||||||
const [creatingRoadmapTeamId, setCreatingRoadmapTeamId] = useState<string>();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="relative pb-12 pt-4 sm:pt-7">
|
|
||||||
<p className="mb-7 mt-2 text-sm">
|
|
||||||
<FeatureAnnouncement />
|
|
||||||
</p>
|
|
||||||
{isCreatingRoadmap && (
|
|
||||||
<CreateRoadmapModal
|
|
||||||
teamId={creatingRoadmapTeamId}
|
|
||||||
onClose={() => {
|
|
||||||
setIsCreatingRoadmap(false);
|
|
||||||
setCreatingRoadmapTeamId(undefined);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{
|
|
||||||
<HeroTitle
|
|
||||||
icon={
|
|
||||||
(<CheckIcon additionalClasses="mr-1.5 h-[14px] w-[14px]" />) as any
|
|
||||||
}
|
|
||||||
isLoading={isLoading}
|
|
||||||
title="Your progress and favorite roadmaps."
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2 md:grid-cols-3">
|
|
||||||
{progress.map((resource) => (
|
|
||||||
<HeroRoadmap
|
|
||||||
key={`${resource.resourceType}-${resource.resourceId}`}
|
|
||||||
resourceId={resource.resourceId}
|
|
||||||
resourceType={resource.resourceType}
|
|
||||||
resourceTitle={resource.resourceTitle}
|
|
||||||
isFavorite={resource.isFavorite}
|
|
||||||
percentageDone={
|
|
||||||
((resource.skipped + resource.done) / resource.total) * 100
|
|
||||||
}
|
|
||||||
url={
|
|
||||||
resource.resourceType === 'roadmap'
|
|
||||||
? `/${resource.resourceId}`
|
|
||||||
: `/best-practices/${resource.resourceId}`
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-5">
|
|
||||||
{
|
|
||||||
<HeroTitle
|
|
||||||
icon={<MapIcon className="mr-1.5 h-[14px] w-[14px]" />}
|
|
||||||
title="Your custom roadmaps"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
{customRoadmaps.length === 0 && (
|
|
||||||
<p className="rounded-md border border-dashed border-gray-800 p-2 text-sm text-gray-600">
|
|
||||||
You haven't created any custom roadmaps yet.{' '}
|
|
||||||
<button
|
|
||||||
className="text-gray-500 underline underline-offset-2 hover:text-gray-400"
|
|
||||||
onClick={() => setIsCreatingRoadmap(true)}
|
|
||||||
>
|
|
||||||
Create one!
|
|
||||||
</button>
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{customRoadmaps.length > 0 && (
|
|
||||||
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2 md:grid-cols-3">
|
|
||||||
{customRoadmaps.map((customRoadmap) => {
|
|
||||||
return (
|
|
||||||
<HeroRoadmap
|
|
||||||
key={customRoadmap.resourceId}
|
|
||||||
resourceId={customRoadmap.resourceId}
|
|
||||||
resourceType={'roadmap'}
|
|
||||||
resourceTitle={customRoadmap.resourceTitle}
|
|
||||||
percentageDone={
|
|
||||||
((customRoadmap.skipped + customRoadmap.done) /
|
|
||||||
customRoadmap.total) *
|
|
||||||
100
|
|
||||||
}
|
|
||||||
url={`/r/${customRoadmap?.roadmapSlug}`}
|
|
||||||
allowFavorite={false}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
<CreateRoadmapButton />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{Object.keys(teamRoadmaps).map((teamName) => {
|
|
||||||
const currentTeam: UserProgressResponse[0]['team'] =
|
|
||||||
teamRoadmaps?.[teamName]?.[0]?.team;
|
|
||||||
const roadmapsList = teamRoadmaps[teamName].filter(
|
|
||||||
(roadmap) => !!roadmap.resourceTitle,
|
|
||||||
);
|
|
||||||
const canManageTeam = ['admin', 'manager'].includes(currentTeam?.role!);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mt-5" key={teamName}>
|
|
||||||
{
|
|
||||||
<HeroTitle
|
|
||||||
icon={<Users2 className="mr-1.5 h-[14px] w-[14px]" />}
|
|
||||||
title={
|
|
||||||
<>
|
|
||||||
Team{' '}
|
|
||||||
<a
|
|
||||||
className="mx-1 font-medium underline underline-offset-2 transition-colors hover:text-gray-300"
|
|
||||||
href={`/team/activity?t=${currentTeam?.id}`}
|
|
||||||
>
|
|
||||||
{teamName}
|
|
||||||
</a>
|
|
||||||
Roadmaps
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
{roadmapsList.length === 0 && (
|
|
||||||
<p className="rounded-md border border-dashed border-gray-800 p-2 text-sm text-gray-600">
|
|
||||||
Team does not have any roadmaps yet.{' '}
|
|
||||||
{canManageTeam && (
|
|
||||||
<button
|
|
||||||
className="text-gray-500 underline underline-offset-2 hover:text-gray-400"
|
|
||||||
onClick={() => {
|
|
||||||
setCreatingRoadmapTeamId(currentTeam?.id);
|
|
||||||
setIsCreatingRoadmap(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Create one!
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{roadmapsList.length > 0 && (
|
|
||||||
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2 md:grid-cols-3">
|
|
||||||
{roadmapsList.map((customRoadmap) => {
|
|
||||||
return (
|
|
||||||
<HeroRoadmap
|
|
||||||
key={customRoadmap.resourceId}
|
|
||||||
resourceId={customRoadmap.resourceId}
|
|
||||||
resourceType={'roadmap'}
|
|
||||||
resourceTitle={customRoadmap.resourceTitle}
|
|
||||||
percentageDone={
|
|
||||||
((customRoadmap.skipped + customRoadmap.done) /
|
|
||||||
customRoadmap.total) *
|
|
||||||
100
|
|
||||||
}
|
|
||||||
url={`/r/${customRoadmap?.roadmapSlug}`}
|
|
||||||
allowFavorite={false}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
{canManageTeam && (
|
|
||||||
<CreateRoadmapButton
|
|
||||||
teamId={currentTeam?.id}
|
|
||||||
text="Create Team Roadmap"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@@ -1,8 +1,8 @@
|
|||||||
import { httpGet } from '../../lib/http';
|
import { httpGet } from '../../lib/http';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { pageProgressMessage } from '../../stores/page';
|
import { pageProgressMessage } from '../../stores/page';
|
||||||
import type { UserProgressResponse } from '../HeroSection/FavoriteRoadmaps';
|
|
||||||
import { SelectionButton } from './SelectionButton';
|
import { SelectionButton } from './SelectionButton';
|
||||||
|
import type { UserProgressResponse } from '../Roadmaps/RoadmapsPage';
|
||||||
|
|
||||||
type RoadmapSelectProps = {
|
type RoadmapSelectProps = {
|
||||||
selectedRoadmaps: string[];
|
selectedRoadmaps: string[];
|
||||||
|
@@ -10,8 +10,27 @@ import {
|
|||||||
} from '../../lib/browser.ts';
|
} from '../../lib/browser.ts';
|
||||||
import { RoadmapCard } from './RoadmapCard.tsx';
|
import { RoadmapCard } from './RoadmapCard.tsx';
|
||||||
import { httpGet } from '../../lib/http.ts';
|
import { httpGet } from '../../lib/http.ts';
|
||||||
import type { UserProgressResponse } from '../HeroSection/FavoriteRoadmaps.tsx';
|
|
||||||
import { isLoggedIn } from '../../lib/jwt.ts';
|
import { isLoggedIn } from '../../lib/jwt.ts';
|
||||||
|
import type { AllowedMemberRoles } from '../ShareOptions/ShareTeamMemberList.tsx';
|
||||||
|
|
||||||
|
export type UserProgressResponse = {
|
||||||
|
resourceId: string;
|
||||||
|
resourceType: 'roadmap' | 'best-practice';
|
||||||
|
resourceTitle: string;
|
||||||
|
isFavorite: boolean;
|
||||||
|
done: number;
|
||||||
|
learning: number;
|
||||||
|
skipped: number;
|
||||||
|
total: number;
|
||||||
|
updatedAt: Date;
|
||||||
|
isCustomResource: boolean;
|
||||||
|
roadmapSlug?: string;
|
||||||
|
team?: {
|
||||||
|
name: string;
|
||||||
|
id: string;
|
||||||
|
role: AllowedMemberRoles;
|
||||||
|
};
|
||||||
|
}[];
|
||||||
|
|
||||||
const groupNames = [
|
const groupNames = [
|
||||||
'Absolute Beginners',
|
'Absolute Beginners',
|
||||||
|
@@ -1,16 +1,15 @@
|
|||||||
|
import { useStore } from '@nanostores/react';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useAuth } from '../../hooks/use-auth';
|
||||||
|
import { useToast } from '../../hooks/use-toast';
|
||||||
|
import { getUrlParams, setUrlParams } from '../../lib/browser';
|
||||||
import { httpGet } from '../../lib/http';
|
import { httpGet } from '../../lib/http';
|
||||||
import { pageProgressMessage } from '../../stores/page';
|
import { pageProgressMessage } from '../../stores/page';
|
||||||
import { MemberProgressItem } from './MemberProgressItem';
|
|
||||||
import { useToast } from '../../hooks/use-toast';
|
|
||||||
import { useStore } from '@nanostores/react';
|
|
||||||
import { $currentTeam } from '../../stores/team';
|
import { $currentTeam } from '../../stores/team';
|
||||||
import { GroupRoadmapItem } from './GroupRoadmapItem';
|
import { GroupRoadmapItem } from './GroupRoadmapItem';
|
||||||
import { getUrlParams, setUrlParams } from '../../lib/browser';
|
|
||||||
import { useAuth } from '../../hooks/use-auth';
|
|
||||||
import { MemberProgressModal } from './MemberProgressModal';
|
|
||||||
import { MemberCustomProgressModal } from './MemberCustomProgressModal';
|
import { MemberCustomProgressModal } from './MemberCustomProgressModal';
|
||||||
import { canManageCurrentRoadmap } from '../../stores/roadmap.ts';
|
import { MemberProgressItem } from './MemberProgressItem';
|
||||||
|
import { MemberProgressModal } from './MemberProgressModal';
|
||||||
|
|
||||||
export type UserProgress = {
|
export type UserProgress = {
|
||||||
resourceTitle: string;
|
resourceTitle: string;
|
||||||
|
@@ -56,7 +56,7 @@ const enrichedBestPractices = bestPractices.map((bestPractice) => {
|
|||||||
});
|
});
|
||||||
---
|
---
|
||||||
|
|
||||||
<BaseLayout title='Dashboard' noIndex={true} permalink="/dashboard">
|
<BaseLayout title='Dashboard' noIndex={true} permalink='/dashboard'>
|
||||||
<DashboardPage
|
<DashboardPage
|
||||||
builtInRoleRoadmaps={enrichedRoleRoadmaps}
|
builtInRoleRoadmaps={enrichedRoleRoadmaps}
|
||||||
builtInSkillRoadmaps={enrichedSkillRoadmaps}
|
builtInSkillRoadmaps={enrichedSkillRoadmaps}
|
||||||
@@ -64,5 +64,5 @@ const enrichedBestPractices = bestPractices.map((bestPractice) => {
|
|||||||
client:load
|
client:load
|
||||||
/>
|
/>
|
||||||
<div slot='open-source-banner'></div>
|
<div slot='open-source-banner'></div>
|
||||||
<div slot="changelog-banner" />
|
<div slot='changelog-banner'></div>
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
|
@@ -128,8 +128,20 @@ a > code:before {
|
|||||||
animation: barberpole 15s linear infinite;
|
animation: barberpole 15s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.striped-loader-slate {
|
||||||
|
background-image: repeating-linear-gradient(
|
||||||
|
-45deg,
|
||||||
|
transparent,
|
||||||
|
transparent 5px,
|
||||||
|
hsla(0, 0%, 0%, 0.1) 5px,
|
||||||
|
hsla(0, 0%, 0%, 0.1) 10px
|
||||||
|
);
|
||||||
|
background-size: 200% 200%;
|
||||||
|
animation: barberpole 30s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes barberpole {
|
@keyframes barberpole {
|
||||||
100% {
|
100% {
|
||||||
background-position: 100% 100%;
|
background-position: 100% 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user