mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2025-03-15 12:49:43 +01:00
feat: team dashboard (#7213)
* fix: add team roadmaps * feat: implement add member * feat: separate team dashboard page * UI changes for team dashboard * Add team activity dashboard --------- Co-authored-by: Kamran Ahmed <kamranahmed.se@gmail.com>
This commit is contained in:
parent
3f7e50907a
commit
8b0c536750
@ -3,6 +3,6 @@
|
||||
"enabled": false
|
||||
},
|
||||
"_variables": {
|
||||
"lastUpdateCheck": 1728161578172
|
||||
"lastUpdateCheck": 1728296475293
|
||||
}
|
||||
}
|
@ -56,9 +56,11 @@ export function ActivityStream(props: ActivityStreamProps) {
|
||||
|
||||
return (
|
||||
<div className={cn('mx-0 px-0 py-5 md:-mx-10 md:px-8 md:py-8', className)}>
|
||||
<h2 className="mb-3 text-xs uppercase text-gray-400">
|
||||
Learning Activity
|
||||
</h2>
|
||||
{activities.length > 0 && (
|
||||
<h2 className="mb-3 text-xs uppercase text-gray-400">
|
||||
Learning Activity
|
||||
</h2>
|
||||
)}
|
||||
|
||||
{selectedActivity && (
|
||||
<ActivityTopicsModal
|
||||
|
@ -4,7 +4,7 @@ export function EmptyActivity() {
|
||||
return (
|
||||
<div className="rounded-md">
|
||||
<div className="flex flex-col items-center p-7 text-center">
|
||||
<RoadmapIcon className="mb-2 w-[60px] h-[60px] sm:h-[120px] sm:w-[120px] opacity-10" />
|
||||
<RoadmapIcon className="mb-2 h-14 w-14 opacity-10" />
|
||||
|
||||
<h2 className="text-lg sm:text-xl font-bold">No Progress</h2>
|
||||
<p className="my-1 sm:my-2 max-w-[400px] text-gray-500 text-sm sm:text-base">
|
||||
|
@ -4,26 +4,11 @@ export function EmptyStream() {
|
||||
return (
|
||||
<div className="rounded-md">
|
||||
<div className="flex flex-col items-center p-7 text-center">
|
||||
<List className="mb-4 h-[60px] w-[60px] opacity-10 sm:h-[60px] sm:w-[60px]" />
|
||||
<List className="mb-4 h-14 w-14 opacity-10" />
|
||||
|
||||
<h2 className="text-lg font-bold sm:text-xl">No Activities</h2>
|
||||
<h2 className="text-lg font-bold sm:text-xl">No Activity</h2>
|
||||
<p className="my-1 max-w-[400px] text-balance text-sm text-gray-500 sm:my-2 sm:text-base">
|
||||
Activities will appear here as you start tracking your
|
||||
<a href="/roadmaps" className="mt-4 text-blue-500 hover:underline">
|
||||
Roadmaps
|
||||
</a>
|
||||
,
|
||||
<a
|
||||
href="/best-practices"
|
||||
className="mt-4 text-blue-500 hover:underline"
|
||||
>
|
||||
Best Practices
|
||||
</a>
|
||||
or
|
||||
<a href="/questions" className="mt-4 text-blue-500 hover:underline">
|
||||
Questions
|
||||
</a>
|
||||
progress.
|
||||
Activities will appear here as you start tracking your progress.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -166,8 +166,8 @@ export function RoadmapSelector(props: RoadmapSelectorProps) {
|
||||
{showSelectRoadmapModal && (
|
||||
<SelectRoadmapModal
|
||||
onClose={() => setShowSelectRoadmapModal(false)}
|
||||
teamResourceConfig={teamResources}
|
||||
allRoadmaps={allRoadmaps.filter(r => r.renderer === 'editor')}
|
||||
teamResourceConfig={teamResources.map((r) => r.resourceId)}
|
||||
allRoadmaps={allRoadmaps.filter((r) => r.renderer === 'editor')}
|
||||
teamId={teamId}
|
||||
onRoadmapAdd={(roadmapId) => {
|
||||
addTeamResource(roadmapId).finally(() => {
|
||||
|
@ -10,7 +10,7 @@ export type SelectRoadmapModalProps = {
|
||||
teamId: string;
|
||||
allRoadmaps: PageType[];
|
||||
onClose: () => void;
|
||||
teamResourceConfig: TeamResourceConfig;
|
||||
teamResourceConfig: string[];
|
||||
onRoadmapAdd: (roadmapId: string) => void;
|
||||
onRoadmapRemove: (roadmapId: string) => void;
|
||||
};
|
||||
@ -100,9 +100,7 @@ export function SelectRoadmapModal(props: SelectRoadmapModalProps) {
|
||||
{roleBasedRoadmaps.length > 0 && (
|
||||
<div className="mb-5 flex flex-wrap items-center gap-2">
|
||||
{roleBasedRoadmaps.map((roadmap) => {
|
||||
const isSelected = !!teamResourceConfig?.find(
|
||||
(r) => r.resourceId === roadmap.id,
|
||||
);
|
||||
const isSelected = teamResourceConfig.includes(roadmap.id);
|
||||
|
||||
return (
|
||||
<SelectRoadmapModalItem
|
||||
@ -126,9 +124,7 @@ export function SelectRoadmapModal(props: SelectRoadmapModalProps) {
|
||||
</span>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{skillBasedRoadmaps.map((roadmap) => {
|
||||
const isSelected = !!teamResourceConfig.find(
|
||||
(r) => r.resourceId === roadmap.id,
|
||||
);
|
||||
const isSelected = teamResourceConfig.includes(roadmap.id);
|
||||
|
||||
return (
|
||||
<SelectRoadmapModalItem
|
||||
@ -148,12 +144,14 @@ export function SelectRoadmapModal(props: SelectRoadmapModalProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-t-yellow-300 text-yellow-900 bg-yellow-100 px-4 py-3 text-sm">
|
||||
<h2 className='font-medium text-base text-yellow-900 mb-1'>More Official Roadmaps Coming Soon</h2>
|
||||
<div className="border-t border-t-yellow-300 bg-yellow-100 px-4 py-3 text-sm text-yellow-900">
|
||||
<h2 className="mb-1 text-base font-medium text-yellow-900">
|
||||
More Official Roadmaps Coming Soon
|
||||
</h2>
|
||||
<p>
|
||||
We are currently adding more of our official
|
||||
roadmaps to this list. If you don't see the roadmap you are
|
||||
looking for, please check back later.
|
||||
We are currently adding more of our official roadmaps to this
|
||||
list. If you don't see the roadmap you are looking for, please
|
||||
check back later.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -8,21 +8,29 @@ import { DashboardTab } from './DashboardTab';
|
||||
import { PersonalDashboard, type BuiltInRoadmap } from './PersonalDashboard';
|
||||
import { TeamDashboard } from './TeamDashboard';
|
||||
import { getUser } from '../../lib/jwt';
|
||||
import { useParams } from '../../hooks/use-params';
|
||||
|
||||
type DashboardPageProps = {
|
||||
builtInRoleRoadmaps?: BuiltInRoadmap[];
|
||||
builtInSkillRoadmaps?: BuiltInRoadmap[];
|
||||
builtInBestPractices?: BuiltInRoadmap[];
|
||||
isTeamPage?: boolean;
|
||||
};
|
||||
|
||||
export function DashboardPage(props: DashboardPageProps) {
|
||||
const { builtInRoleRoadmaps, builtInBestPractices, builtInSkillRoadmaps } =
|
||||
props;
|
||||
const {
|
||||
builtInRoleRoadmaps,
|
||||
builtInBestPractices,
|
||||
builtInSkillRoadmaps,
|
||||
isTeamPage = false,
|
||||
} = props;
|
||||
|
||||
const currentUser = getUser();
|
||||
const toast = useToast();
|
||||
const teamList = useStore($teamList);
|
||||
|
||||
const { t: currTeamId } = useParams();
|
||||
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [selectedTeamId, setSelectedTeamId] = useState<string>();
|
||||
|
||||
@ -43,8 +51,14 @@ export function DashboardPage(props: DashboardPageProps) {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getAllTeams().finally(() => setIsLoading(false));
|
||||
}, []);
|
||||
getAllTeams().finally(() => {
|
||||
if (currTeamId) {
|
||||
setSelectedTeamId(currTeamId);
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
});
|
||||
}, [currTeamId]);
|
||||
|
||||
const userAvatar =
|
||||
currentUser?.avatar && !isLoading
|
||||
@ -57,8 +71,8 @@ export function DashboardPage(props: DashboardPageProps) {
|
||||
<div className="mb-6 flex flex-wrap items-center gap-1.5 sm:mb-8">
|
||||
<DashboardTab
|
||||
label="Personal"
|
||||
isActive={!selectedTeamId}
|
||||
onClick={() => setSelectedTeamId(undefined)}
|
||||
isActive={!selectedTeamId && !isTeamPage}
|
||||
href="/dashboard"
|
||||
avatar={userAvatar}
|
||||
/>
|
||||
|
||||
@ -86,10 +100,7 @@ export function DashboardPage(props: DashboardPageProps) {
|
||||
href: `/respond-invite?i=${team.memberId}`,
|
||||
}
|
||||
: {
|
||||
href: `/team/activity?t=${team._id}`,
|
||||
// onClick: () => {
|
||||
// setSelectedTeamId(team._id);
|
||||
// },
|
||||
href: `/team?t=${team._id}`,
|
||||
})}
|
||||
avatar={avatarUrl}
|
||||
/>
|
||||
@ -105,14 +116,21 @@ export function DashboardPage(props: DashboardPageProps) {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!selectedTeamId && (
|
||||
{!selectedTeamId && !isTeamPage && (
|
||||
<PersonalDashboard
|
||||
builtInRoleRoadmaps={builtInRoleRoadmaps}
|
||||
builtInSkillRoadmaps={builtInSkillRoadmaps}
|
||||
builtInBestPractices={builtInBestPractices}
|
||||
/>
|
||||
)}
|
||||
{selectedTeamId && <TeamDashboard teamId={selectedTeamId} />}
|
||||
|
||||
{(selectedTeamId || isTeamPage) && (
|
||||
<TeamDashboard
|
||||
builtInRoleRoadmaps={builtInRoleRoadmaps!}
|
||||
builtInSkillRoadmaps={builtInSkillRoadmaps!}
|
||||
teamId={selectedTeamId!}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
303
src/components/Dashboard/DashboardTeamRoadmaps.tsx
Normal file
303
src/components/Dashboard/DashboardTeamRoadmaps.tsx
Normal file
@ -0,0 +1,303 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { ResourceProgress } from '../Activity/ResourceProgress';
|
||||
import { RoadmapIcon } from '../ReactIcons/RoadmapIcon';
|
||||
import type { UserProgress } from '../TeamProgress/TeamProgressPage';
|
||||
import { LoadingProgress } from './LoadingProgress';
|
||||
import { PickRoadmapOptionModal } from '../TeamRoadmaps/PickRoadmapOptionModal';
|
||||
import { SelectRoadmapModal } from '../CreateTeam/SelectRoadmapModal';
|
||||
import { CreateRoadmapModal } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapModal';
|
||||
import { ContentConfirmationModal } from '../CreateTeam/ContentConfirmationModal';
|
||||
import { httpGet, httpPut } from '../../lib/http';
|
||||
import type { PageType } from '../CommandMenu/CommandMenu';
|
||||
import { useToast } from '../../hooks/use-toast';
|
||||
import type { TeamResourceConfig } from '../CreateTeam/RoadmapSelector';
|
||||
import { pageProgressMessage } from '../../stores/page';
|
||||
import type { BuiltInRoadmap } from './PersonalDashboard';
|
||||
import { MapIcon, Users2 } from 'lucide-react';
|
||||
|
||||
type DashboardTeamRoadmapsProps = {
|
||||
isLoading: boolean;
|
||||
teamId: string;
|
||||
learningRoadmapsToShow: (UserProgress & {
|
||||
defaultRoadmapId?: string;
|
||||
})[];
|
||||
canManageCurrentTeam: boolean;
|
||||
onUpdate: () => void;
|
||||
|
||||
builtInRoleRoadmaps: BuiltInRoadmap[];
|
||||
builtInSkillRoadmaps: BuiltInRoadmap[];
|
||||
};
|
||||
|
||||
export function DashboardTeamRoadmaps(props: DashboardTeamRoadmapsProps) {
|
||||
const {
|
||||
isLoading,
|
||||
teamId,
|
||||
learningRoadmapsToShow,
|
||||
canManageCurrentTeam,
|
||||
onUpdate,
|
||||
|
||||
builtInRoleRoadmaps,
|
||||
builtInSkillRoadmaps,
|
||||
} = props;
|
||||
|
||||
const toast = useToast();
|
||||
const [isPickingOptions, setIsPickingOptions] = useState(false);
|
||||
const [isAddingRoadmap, setIsAddingRoadmap] = useState(false);
|
||||
const [isCreatingRoadmap, setIsCreatingRoadmap] = useState(false);
|
||||
const [confirmationContentId, setConfirmationContentId] = useState<string>();
|
||||
|
||||
const allRoadmaps = useMemo(
|
||||
() =>
|
||||
builtInRoleRoadmaps.concat(builtInSkillRoadmaps).map((r) => {
|
||||
return {
|
||||
id: r.id,
|
||||
title: r.title,
|
||||
url: r.url,
|
||||
group: 'Roadmaps',
|
||||
renderer: r.renderer || 'balsamiq',
|
||||
metadata: r.metadata,
|
||||
};
|
||||
}),
|
||||
[builtInRoleRoadmaps, builtInSkillRoadmaps],
|
||||
);
|
||||
|
||||
async function onAdd(roadmapId: string, shouldCopyContent = false) {
|
||||
if (!teamId) {
|
||||
return;
|
||||
}
|
||||
|
||||
toast.loading('Adding roadmap');
|
||||
pageProgressMessage.set('Adding roadmap');
|
||||
const roadmap = allRoadmaps.find((r) => r.id === roadmapId);
|
||||
const { error, response } = await httpPut<TeamResourceConfig>(
|
||||
`${
|
||||
import.meta.env.PUBLIC_API_URL
|
||||
}/v1-update-team-resource-config/${teamId}`,
|
||||
{
|
||||
teamId: teamId,
|
||||
resourceId: roadmapId,
|
||||
resourceType: 'roadmap',
|
||||
removed: [],
|
||||
renderer: roadmap?.renderer || 'balsamiq',
|
||||
shouldCopyContent,
|
||||
},
|
||||
);
|
||||
|
||||
if (error || !response) {
|
||||
toast.error(error?.message || 'Error adding roadmap');
|
||||
return;
|
||||
}
|
||||
|
||||
onUpdate();
|
||||
toast.success('Roadmap added');
|
||||
if (roadmap?.renderer === 'editor') {
|
||||
setIsAddingRoadmap(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteResource(roadmapId: string) {
|
||||
if (!teamId) {
|
||||
return;
|
||||
}
|
||||
|
||||
toast.loading('Deleting roadmap');
|
||||
pageProgressMessage.set(`Deleting roadmap from team`);
|
||||
const { error, response } = await httpPut<TeamResourceConfig>(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-delete-team-resource-config/${
|
||||
teamId
|
||||
}`,
|
||||
{
|
||||
resourceId: roadmapId,
|
||||
resourceType: 'roadmap',
|
||||
},
|
||||
);
|
||||
|
||||
if (error || !response) {
|
||||
toast.error(error?.message || 'Something went wrong');
|
||||
return;
|
||||
}
|
||||
|
||||
toast.success('Roadmap removed');
|
||||
onUpdate();
|
||||
}
|
||||
|
||||
async function onRemove(resourceId: string) {
|
||||
pageProgressMessage.set('Removing roadmap');
|
||||
|
||||
deleteResource(resourceId).finally(() => {
|
||||
pageProgressMessage.set('');
|
||||
});
|
||||
}
|
||||
|
||||
const pickRoadmapOptionModal = isPickingOptions && (
|
||||
<PickRoadmapOptionModal
|
||||
onClose={() => setIsPickingOptions(false)}
|
||||
showDefaultRoadmapsModal={() => {
|
||||
setIsAddingRoadmap(true);
|
||||
setIsPickingOptions(false);
|
||||
}}
|
||||
showCreateCustomRoadmapModal={() => {
|
||||
setIsCreatingRoadmap(true);
|
||||
setIsPickingOptions(false);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const filteredAllRoadmaps = allRoadmaps.filter(
|
||||
(r) => !learningRoadmapsToShow.find((c) => c?.defaultRoadmapId === r.id),
|
||||
);
|
||||
|
||||
const addRoadmapModal = isAddingRoadmap && (
|
||||
<SelectRoadmapModal
|
||||
onClose={() => setIsAddingRoadmap(false)}
|
||||
teamResourceConfig={learningRoadmapsToShow.map((r) => r.resourceId)}
|
||||
allRoadmaps={filteredAllRoadmaps.filter((r) => r.renderer === 'editor')}
|
||||
teamId={teamId}
|
||||
onRoadmapAdd={(roadmapId: string) => {
|
||||
const isEditorRoadmap = allRoadmaps.find(
|
||||
(r) => r.id === roadmapId && r.renderer === 'editor',
|
||||
);
|
||||
|
||||
if (!isEditorRoadmap) {
|
||||
onAdd(roadmapId).finally(() => {
|
||||
pageProgressMessage.set('');
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setIsAddingRoadmap(false);
|
||||
setConfirmationContentId(roadmapId);
|
||||
}}
|
||||
onRoadmapRemove={(roadmapId: string) => {
|
||||
if (confirm('Are you sure you want to remove this roadmap?')) {
|
||||
onRemove(roadmapId).finally(() => {});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const confirmationContentIdModal = confirmationContentId && (
|
||||
<ContentConfirmationModal
|
||||
onClose={() => {
|
||||
setConfirmationContentId('');
|
||||
}}
|
||||
onClick={(shouldCopy) => {
|
||||
onAdd(confirmationContentId, shouldCopy).finally(() => {
|
||||
pageProgressMessage.set('');
|
||||
setConfirmationContentId('');
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const createRoadmapModal = isCreatingRoadmap && (
|
||||
<CreateRoadmapModal
|
||||
teamId={teamId}
|
||||
onClose={() => {
|
||||
setIsCreatingRoadmap(false);
|
||||
}}
|
||||
onCreated={() => {
|
||||
setIsCreatingRoadmap(false);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const roadmapHeading = (
|
||||
<div className="mb-3 flex h-[20px] items-center justify-between gap-2 text-xs">
|
||||
<h2 className="uppercase text-gray-400">Roadmaps</h2>
|
||||
<span className="mx-3 h-[1px] flex-grow bg-gray-200" />
|
||||
{canManageCurrentTeam && (
|
||||
<a
|
||||
href={`/team/roadmaps?t=${teamId}`}
|
||||
className="flex flex-row items-center rounded-full bg-gray-400 px-2.5 py-0.5 text-xs text-white transition-colors hover:bg-black"
|
||||
>
|
||||
<MapIcon className="mr-1.5 size-3" strokeWidth={2.5} />
|
||||
Roadmaps
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
if (!isLoading && learningRoadmapsToShow.length === 0) {
|
||||
return (
|
||||
<>
|
||||
{roadmapHeading}
|
||||
<div className="flex flex-col items-center rounded-md border bg-white p-4 py-10">
|
||||
{pickRoadmapOptionModal}
|
||||
{addRoadmapModal}
|
||||
{createRoadmapModal}
|
||||
{confirmationContentIdModal}
|
||||
|
||||
<RoadmapIcon className="mb-4 h-14 w-14 opacity-10" />
|
||||
|
||||
<h2 className="text-lg font-semibold sm:text-lg">No roadmaps</h2>
|
||||
<p className="my-1 max-w-[400px] text-balance text-sm text-gray-500 sm:my-2 sm:text-base">
|
||||
{canManageCurrentTeam
|
||||
? 'Add a roadmap to start tracking your team'
|
||||
: 'Ask your team admin to add some roadmaps'}
|
||||
</p>
|
||||
|
||||
{canManageCurrentTeam && (
|
||||
<button
|
||||
className="mt-1 rounded-lg bg-black px-3 py-1 text-sm font-medium text-white hover:bg-gray-900"
|
||||
onClick={() => setIsPickingOptions(true)}
|
||||
>
|
||||
Add roadmap
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{pickRoadmapOptionModal}
|
||||
{addRoadmapModal}
|
||||
{createRoadmapModal}
|
||||
{confirmationContentIdModal}
|
||||
|
||||
{roadmapHeading}
|
||||
{isLoading && <LoadingProgress />}
|
||||
{!isLoading && learningRoadmapsToShow.length > 0 && (
|
||||
<div className="grid grid-cols-1 gap-1.5 sm:grid-cols-3">
|
||||
{learningRoadmapsToShow.map((roadmap) => {
|
||||
const learningCount = roadmap.learning || 0;
|
||||
const doneCount = roadmap.done || 0;
|
||||
const totalCount = roadmap.total || 0;
|
||||
const skippedCount = roadmap.skipped || 0;
|
||||
|
||||
return (
|
||||
<ResourceProgress
|
||||
key={roadmap.resourceId}
|
||||
isCustomResource={roadmap?.isCustomResource || false}
|
||||
doneCount={doneCount > totalCount ? totalCount : doneCount}
|
||||
learningCount={
|
||||
learningCount > totalCount ? totalCount : learningCount
|
||||
}
|
||||
totalCount={totalCount}
|
||||
skippedCount={skippedCount}
|
||||
resourceId={roadmap.resourceId}
|
||||
resourceType="roadmap"
|
||||
updatedAt={roadmap.updatedAt}
|
||||
title={roadmap.resourceTitle}
|
||||
showActions={false}
|
||||
roadmapSlug={roadmap.roadmapSlug}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{canManageCurrentTeam && (
|
||||
<button
|
||||
onClick={() => setIsPickingOptions(true)}
|
||||
className="group relative flex w-full items-center justify-center overflow-hidden rounded-md border border-dashed border-gray-300 bg-white px-3 py-2 text-center text-sm text-gray-500 transition-all hover:border-gray-400 hover:text-black"
|
||||
>
|
||||
+ Add Roadmap
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
@ -17,6 +17,7 @@ import { DashboardAiRoadmaps } from './DashboardAiRoadmaps.tsx';
|
||||
import type { AllowedProfileVisibility } from '../../api/user.ts';
|
||||
import { PencilIcon, type LucideIcon } from 'lucide-react';
|
||||
import { cn } from '../../lib/classname.ts';
|
||||
import type { AllowedRoadmapRenderer } from '../../lib/roadmap.ts';
|
||||
|
||||
type UserDashboardResponse = {
|
||||
name: string;
|
||||
@ -42,6 +43,8 @@ export type BuiltInRoadmap = {
|
||||
description: string;
|
||||
isFavorite?: boolean;
|
||||
relatedRoadmapIds?: string[];
|
||||
renderer?: AllowedRoadmapRenderer;
|
||||
metadata?: Record<string, any>;
|
||||
};
|
||||
|
||||
type PersonalDashboardProps = {
|
||||
@ -350,13 +353,11 @@ function DashboardCard(props: DashboardCardProps) {
|
||||
} = props;
|
||||
|
||||
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">
|
||||
<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" />
|
||||
|
@ -3,29 +3,35 @@ import type { TeamMember } from '../TeamProgress/TeamProgressPage';
|
||||
import { httpGet } from '../../lib/http';
|
||||
import { useToast } from '../../hooks/use-toast';
|
||||
import { getUser } from '../../lib/jwt';
|
||||
import { LoadingProgress } from './LoadingProgress';
|
||||
import { ResourceProgress } from '../Activity/ResourceProgress';
|
||||
import { TeamActivityPage } from '../TeamActivity/TeamActivityPage';
|
||||
import { cn } from '../../lib/classname';
|
||||
import { Tooltip } from '../Tooltip';
|
||||
import { DashboardTeamRoadmaps } from './DashboardTeamRoadmaps';
|
||||
import type { BuiltInRoadmap } from './PersonalDashboard';
|
||||
import { InviteMemberPopup } from '../TeamMembers/InviteMemberPopup';
|
||||
import { Users, Users2 } from 'lucide-react';
|
||||
|
||||
type TeamDashboardProps = {
|
||||
builtInRoleRoadmaps: BuiltInRoadmap[];
|
||||
builtInSkillRoadmaps: BuiltInRoadmap[];
|
||||
teamId: string;
|
||||
};
|
||||
|
||||
export function TeamDashboard(props: TeamDashboardProps) {
|
||||
const { teamId } = props;
|
||||
const { teamId, builtInRoleRoadmaps, builtInSkillRoadmaps } = props;
|
||||
|
||||
const toast = useToast();
|
||||
const currentUser = getUser();
|
||||
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [teamMembers, setTeamMembers] = useState<TeamMember[]>([]);
|
||||
const [isInvitingMember, setIsInvitingMember] = useState(false);
|
||||
|
||||
async function getTeamProgress() {
|
||||
const { response, error } = await httpGet<TeamMember[]>(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-get-team-progress/${teamId}`,
|
||||
);
|
||||
|
||||
if (error || !response) {
|
||||
toast.error(error?.message || 'Failed to get team progress');
|
||||
return;
|
||||
@ -54,12 +60,8 @@ export function TeamDashboard(props: TeamDashboardProps) {
|
||||
getTeamProgress().finally(() => setIsLoading(false));
|
||||
}, [teamId]);
|
||||
|
||||
if (!currentUser) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const currentMember = teamMembers.find(
|
||||
(member) => member.email === currentUser.email,
|
||||
(member) => member.email === currentUser?.email,
|
||||
);
|
||||
const learningRoadmapsToShow =
|
||||
currentMember?.progress?.filter(
|
||||
@ -67,53 +69,58 @@ export function TeamDashboard(props: TeamDashboardProps) {
|
||||
) || [];
|
||||
|
||||
const allMembersWithoutCurrentUser = teamMembers.sort((a, b) => {
|
||||
if (a.email === currentUser.email) {
|
||||
if (a.email === currentUser?.email) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (b.email === currentUser.email) {
|
||||
if (b.email === currentUser?.email) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
|
||||
const canManageCurrentTeam = ['admin', 'manager'].includes(
|
||||
currentMember?.role!,
|
||||
);
|
||||
|
||||
return (
|
||||
<section className="mt-8">
|
||||
<h2 className="mb-3 text-xs uppercase text-gray-400">Roadmaps</h2>
|
||||
{isLoading && <LoadingProgress />}
|
||||
{!isLoading && learningRoadmapsToShow.length > 0 && (
|
||||
<div className="grid grid-cols-1 gap-1.5 sm:grid-cols-3">
|
||||
{learningRoadmapsToShow.map((roadmap) => {
|
||||
const learningCount = roadmap.learning || 0;
|
||||
const doneCount = roadmap.done || 0;
|
||||
const totalCount = roadmap.total || 0;
|
||||
const skippedCount = roadmap.skipped || 0;
|
||||
|
||||
return (
|
||||
<ResourceProgress
|
||||
key={roadmap.resourceId}
|
||||
isCustomResource={roadmap?.isCustomResource || false}
|
||||
doneCount={doneCount > totalCount ? totalCount : doneCount}
|
||||
learningCount={
|
||||
learningCount > totalCount ? totalCount : learningCount
|
||||
}
|
||||
totalCount={totalCount}
|
||||
skippedCount={skippedCount}
|
||||
resourceId={roadmap.resourceId}
|
||||
resourceType="roadmap"
|
||||
updatedAt={roadmap.updatedAt}
|
||||
title={roadmap.resourceTitle}
|
||||
showActions={false}
|
||||
roadmapSlug={roadmap.roadmapSlug}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{isInvitingMember && (
|
||||
<InviteMemberPopup
|
||||
onInvited={() => {
|
||||
toast.success('Invite sent');
|
||||
getTeamProgress().finally(() => null);
|
||||
setIsInvitingMember(false);
|
||||
}}
|
||||
onClose={() => {
|
||||
setIsInvitingMember(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<h2 className="mb-3 mt-6 text-xs uppercase text-gray-400">
|
||||
<DashboardTeamRoadmaps
|
||||
isLoading={isLoading}
|
||||
teamId={teamId}
|
||||
learningRoadmapsToShow={learningRoadmapsToShow}
|
||||
canManageCurrentTeam={canManageCurrentTeam}
|
||||
onUpdate={getTeamProgress}
|
||||
builtInRoleRoadmaps={builtInRoleRoadmaps}
|
||||
builtInSkillRoadmaps={builtInSkillRoadmaps}
|
||||
/>
|
||||
|
||||
<h2 className="mb-3 mt-6 flex h-[20px] items-center justify-between text-xs uppercase text-gray-400">
|
||||
Team Members
|
||||
<span className="flex-grow h-[1px] bg-gray-200 mx-3" />
|
||||
{canManageCurrentTeam && (
|
||||
<a
|
||||
href={`/team/members?t=${teamId}`}
|
||||
className="flex flex-row items-center rounded-full bg-gray-400 px-2.5 py-0.5 text-xs text-white transition-colors hover:bg-black"
|
||||
>
|
||||
<Users2 className="mr-1.5 size-3" strokeWidth={2.5} />
|
||||
Members
|
||||
</a>
|
||||
)}
|
||||
</h2>
|
||||
{isLoading && <TeamMemberLoading className="mb-6" />}
|
||||
{!isLoading && (
|
||||
@ -123,7 +130,11 @@ export function TeamDashboard(props: TeamDashboardProps) {
|
||||
? `${import.meta.env.PUBLIC_AVATAR_BASE_URL}/${member.avatar}`
|
||||
: '/images/default-avatar.png';
|
||||
return (
|
||||
<span className="group relative" key={member.email}>
|
||||
<a
|
||||
className="group relative"
|
||||
key={member.email}
|
||||
href={`/team/member?t=${teamId}&m=${member._id}`}
|
||||
>
|
||||
<figure className="relative aspect-square size-8 overflow-hidden rounded-md bg-gray-100">
|
||||
<img
|
||||
src={avatar}
|
||||
@ -134,13 +145,30 @@ export function TeamDashboard(props: TeamDashboardProps) {
|
||||
<Tooltip position="top-center" additionalClass="text-sm">
|
||||
{member.name}
|
||||
</Tooltip>
|
||||
</span>
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
|
||||
{canManageCurrentTeam && (
|
||||
<button
|
||||
className="group relative"
|
||||
onClick={() => setIsInvitingMember(true)}
|
||||
>
|
||||
<span className="relative flex aspect-square size-8 items-center justify-center overflow-hidden rounded-md border border-dashed bg-gray-100">
|
||||
+
|
||||
</span>
|
||||
<Tooltip position="top-center" additionalClass="text-sm">
|
||||
Add Member
|
||||
</Tooltip>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<TeamActivityPage teamId={teamId} />
|
||||
<TeamActivityPage
|
||||
teamId={teamId}
|
||||
canManageCurrentTeam={canManageCurrentTeam}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
@ -102,7 +102,7 @@ export function TeamActivityItem(props: TeamActivityItemProps) {
|
||||
return (
|
||||
<li
|
||||
key={user._id}
|
||||
className="flex flex-wrap items-center gap-1 rounded-md border px-2 py-2.5 text-sm"
|
||||
className="flex flex-wrap items-center gap-1 rounded-md border px-2 py-2.5 text-sm bg-white"
|
||||
>
|
||||
{actionType === 'in_progress' && (
|
||||
<>
|
||||
@ -158,7 +158,7 @@ export function TeamActivityItem(props: TeamActivityItemProps) {
|
||||
const activityLimit = showAll ? activities.length : 5;
|
||||
|
||||
return (
|
||||
<li key={user._id} className="overflow-hidden rounded-md border">
|
||||
<li key={user._id} className="overflow-hidden bg-white rounded-md border">
|
||||
<h3 className="flex flex-wrap items-center gap-1 bg-gray-100 px-2 py-2.5 text-sm">
|
||||
{username} has {activities.length} updates in {uniqueResourcesCount}
|
||||
resource(s)
|
||||
|
@ -9,6 +9,12 @@ import { TeamActivityItem } from './TeamActivityItem';
|
||||
import { TeamActivityTopicsModal } from './TeamActivityTopicsModal';
|
||||
import { TeamEmptyStream } from './TeamEmptyStream';
|
||||
import { Pagination } from '../Pagination/Pagination';
|
||||
import {
|
||||
ChartNoAxesGantt,
|
||||
CircleDashed,
|
||||
Flag,
|
||||
LoaderCircle,
|
||||
} from 'lucide-react';
|
||||
|
||||
export type TeamStreamActivity = {
|
||||
_id?: string;
|
||||
@ -51,10 +57,11 @@ type GetTeamActivityResponse = {
|
||||
|
||||
type TeamActivityPageProps = {
|
||||
teamId?: string;
|
||||
canManageCurrentTeam?: boolean;
|
||||
};
|
||||
|
||||
export function TeamActivityPage(props: TeamActivityPageProps) {
|
||||
const { teamId: defaultTeamId } = props;
|
||||
const { teamId: defaultTeamId, canManageCurrentTeam = false } = props;
|
||||
const { t: teamId = defaultTeamId } = getUrlParams();
|
||||
|
||||
const toast = useToast();
|
||||
@ -182,13 +189,44 @@ export function TeamActivityPage(props: TeamActivityPageProps) {
|
||||
return enrichedUsers;
|
||||
}, [users, activities]);
|
||||
|
||||
if (!teamId) {
|
||||
window.location.href = '/';
|
||||
return;
|
||||
}
|
||||
const sectionHeading = (
|
||||
<h3 className="mb-3 flex h-[20px] w-full items-center justify-between text-xs uppercase text-gray-400">
|
||||
Team Activity
|
||||
<span className="mx-3 h-[1px] flex-grow bg-gray-200" />
|
||||
{canManageCurrentTeam && (
|
||||
<a
|
||||
href={`/team/progress?t=${teamId}`}
|
||||
className="flex flex-row items-center rounded-full bg-gray-400 px-2.5 py-0.5 text-xs text-white transition-colors hover:bg-black"
|
||||
>
|
||||
<ChartNoAxesGantt className="mr-1.5 size-3" strokeWidth={2.5} />
|
||||
Progresses
|
||||
</a>
|
||||
)}
|
||||
</h3>
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return null;
|
||||
return (
|
||||
<>
|
||||
{sectionHeading}
|
||||
<div className="flex flex-col gap-2">
|
||||
{Array.from({ length: 4 }).map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="h-[70px] w-full animate-pulse rounded-lg border bg-gray-100"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (!teamId) {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.location.href = '/';
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
@ -202,9 +240,7 @@ export function TeamActivityPage(props: TeamActivityPageProps) {
|
||||
|
||||
{usersWithActivities.length > 0 ? (
|
||||
<>
|
||||
<h3 className="mb-4 flex w-full items-center justify-between text-xs uppercase text-gray-400">
|
||||
Team Activity
|
||||
</h3>
|
||||
{sectionHeading}
|
||||
<ul className="mb-4 mt-2 flex flex-col gap-3">
|
||||
{usersWithActivities.map((user, index) => {
|
||||
return (
|
||||
@ -233,7 +269,12 @@ export function TeamActivityPage(props: TeamActivityPageProps) {
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<TeamEmptyStream teamId={teamId} />
|
||||
<>
|
||||
{sectionHeading}
|
||||
<div className="rounded-lg border bg-white p-4">
|
||||
<TeamEmptyStream teamId={teamId} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
@ -10,7 +10,7 @@ export function TeamEmptyStream(props: TeamActivityItemProps) {
|
||||
return (
|
||||
<div className="rounded-md">
|
||||
<div className="flex flex-col items-center p-7 text-center sm:p-14">
|
||||
<ListTodo className="mb-4 h-[60px] w-[60px] opacity-10 sm:h-[60px] sm:w-[60px]" />
|
||||
<ListTodo className="mb-4 h-14 w-14 opacity-10" />
|
||||
|
||||
<h2 className="text-lg font-semibold sm:text-lg">No Activity</h2>
|
||||
<p className="my-1 max-w-[400px] text-balance text-sm text-gray-500 sm:my-2 sm:text-base">
|
||||
|
@ -10,18 +10,11 @@ export function TeamMemberEmptyPage(props: TeamMemberEmptyPageProps) {
|
||||
return (
|
||||
<div className="rounded-md">
|
||||
<div className="flex flex-col items-center p-7 text-center">
|
||||
<RoadmapIcon className="mb-2 h-[60px] w-[60px] opacity-10 sm:h-[120px] sm:w-[120px]" />
|
||||
<RoadmapIcon className="mb-2 h-14 w-14 opacity-10" />
|
||||
|
||||
<h2 className="text-lg font-bold sm:text-xl">No Progress</h2>
|
||||
<p className="my-1 max-w-[400px] text-balance text-sm text-gray-500 sm:my-2 sm:text-base">
|
||||
Progress will appear here as they start tracking their{' '}
|
||||
<a
|
||||
href={`/team/roadmaps?t=${teamId}`}
|
||||
className="mt-4 text-blue-500 hover:underline"
|
||||
>
|
||||
Roadmaps
|
||||
</a>{' '}
|
||||
progress.
|
||||
Progress will appear here as they start tracking their roadmaps.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -7,10 +7,11 @@ import { type AllowedRoles, RoleDropdown } from '../CreateTeam/RoleDropdown';
|
||||
type InviteMemberPopupProps = {
|
||||
onInvited: () => void;
|
||||
onClose: () => void;
|
||||
teamId?: string;
|
||||
};
|
||||
|
||||
export function InviteMemberPopup(props: InviteMemberPopupProps) {
|
||||
const { onClose, onInvited } = props;
|
||||
const { onClose, onInvited, teamId: defaultTeamId } = props;
|
||||
|
||||
const popupBodyRef = useRef<HTMLDivElement>(null);
|
||||
const emailRef = useRef<HTMLInputElement>(null);
|
||||
@ -18,7 +19,7 @@ export function InviteMemberPopup(props: InviteMemberPopupProps) {
|
||||
const [email, setEmail] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const { teamId } = useTeamId();
|
||||
const { teamId = defaultTeamId } = useTeamId();
|
||||
|
||||
useEffect(() => {
|
||||
emailRef?.current?.focus();
|
||||
@ -31,7 +32,7 @@ export function InviteMemberPopup(props: InviteMemberPopupProps) {
|
||||
|
||||
const { response, error } = await httpPost(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-invite-member/${teamId}`,
|
||||
{ email, role: selectedRole }
|
||||
{ email, role: selectedRole },
|
||||
);
|
||||
|
||||
if (error || !response) {
|
||||
@ -92,7 +93,7 @@ export function InviteMemberPopup(props: InviteMemberPopupProps) {
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<p className=" rounded-md border border-red-300 bg-red-50 p-2 text-sm text-red-700">
|
||||
<p className="rounded-md border border-red-300 bg-red-50 p-2 text-sm text-red-700">
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
|
@ -233,7 +233,7 @@ export function TeamRoadmaps() {
|
||||
const addRoadmapModal = isAddingRoadmap && (
|
||||
<SelectRoadmapModal
|
||||
onClose={() => setIsAddingRoadmap(false)}
|
||||
teamResourceConfig={teamResources}
|
||||
teamResourceConfig={teamResources.map((c) => c.resourceId)}
|
||||
allRoadmaps={filteredAllRoadmaps.filter((r) => r.renderer === 'editor')}
|
||||
teamId={teamId}
|
||||
onRoadmapAdd={(roadmapId: string) => {
|
||||
@ -309,9 +309,9 @@ export function TeamRoadmaps() {
|
||||
{createRoadmapModal}
|
||||
{confirmationContentIdModal}
|
||||
|
||||
<RoadmapIcon className="mb-4 h-24 w-24 opacity-10" />
|
||||
<RoadmapIcon className="mb-3 h-14 w-14 opacity-10" />
|
||||
|
||||
<h3 className="mb-1 text-2xl font-bold text-gray-900">No roadmaps</h3>
|
||||
<h3 className="mb-1 text-xl font-bold text-gray-900">No roadmaps</h3>
|
||||
<p className="text-base text-gray-500">
|
||||
{canManageCurrentTeam
|
||||
? 'Add a roadmap to start tracking your team'
|
||||
@ -320,7 +320,7 @@ export function TeamRoadmaps() {
|
||||
|
||||
{canManageCurrentTeam && (
|
||||
<button
|
||||
className="mt-4 rounded-lg bg-black px-4 py-2 font-medium text-white hover:bg-gray-900"
|
||||
className="mt-3 rounded-md bg-black px-3 py-1.5 font-medium text-white hover:bg-gray-900 text-sm"
|
||||
onClick={() => setIsPickingOptions(true)}
|
||||
>
|
||||
Add roadmap
|
||||
|
48
src/data/projects/log-analyser.md
Normal file
48
src/data/projects/log-analyser.md
Normal file
@ -0,0 +1,48 @@
|
||||
---
|
||||
title: 'Log Analysis Tool'
|
||||
description: 'Write a simple tool to analyze logs from the command line.'
|
||||
isNew: true
|
||||
sort: 3
|
||||
difficulty: 'beginner'
|
||||
nature: 'CLI'
|
||||
skills:
|
||||
- 'linux'
|
||||
- 'bash'
|
||||
- 'shell scripting'
|
||||
seo:
|
||||
title: 'Log Analysis Tool'
|
||||
description: 'Build a simple CLI tool to analyze logs from the command line.'
|
||||
keywords:
|
||||
- 'log analysis tool'
|
||||
- 'devops project idea'
|
||||
roadmapIds:
|
||||
- 'devops'
|
||||
- 'linux'
|
||||
---
|
||||
|
||||
The goal of this project is to help you practice some basic shell scripting skills. You will write a simple tool to analyze logs from the command line.
|
||||
|
||||
## Requirements
|
||||
|
||||
Download the sample nginx access log file from [here](https://gist.githubusercontent.com/kamranahmedse/e66c3b9ea89a1a030d3b739eeeef22d0/raw/77fb3ac837a73c4f0206e78a236d885590b7ae35/nginx-access.log). The log file contains the following fields:
|
||||
|
||||
- IP address
|
||||
- Date and time
|
||||
- Request method and path
|
||||
- Response status code
|
||||
- Response size
|
||||
- Referrer
|
||||
- User agent
|
||||
|
||||
You are required to create a shell script that reads the log file and provides the following information:
|
||||
|
||||
```text
|
||||
Top 5 IP addresses with the most requests:
|
||||
45.76.135.253 - 1000 requests
|
||||
142.93.143.8 - 600 requests
|
||||
178.128.94.113 - 50 requests
|
||||
43.224.43.187 - 30 requests
|
||||
178.128.94.113 - 20 requests
|
||||
|
||||
|
||||
```
|
@ -19,6 +19,10 @@ const enrichedRoleRoadmaps = roleRoadmaps
|
||||
title: frontmatter.briefTitle,
|
||||
description: frontmatter.briefDescription,
|
||||
relatedRoadmapIds: frontmatter.relatedRoadmaps,
|
||||
renderer: frontmatter.renderer,
|
||||
metadata: {
|
||||
tags: frontmatter.tags,
|
||||
},
|
||||
};
|
||||
});
|
||||
const enrichedSkillRoadmaps = skillRoadmaps
|
||||
@ -33,6 +37,10 @@ const enrichedSkillRoadmaps = skillRoadmaps
|
||||
frontmatter.briefTitle === 'Go' ? 'Go Roadmap' : frontmatter.briefTitle,
|
||||
description: frontmatter.briefDescription,
|
||||
relatedRoadmapIds: frontmatter.relatedRoadmaps,
|
||||
renderer: frontmatter.renderer,
|
||||
metadata: {
|
||||
tags: frontmatter.tags,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -1,15 +1,68 @@
|
||||
---
|
||||
import AccountSidebar from '../../components/AccountSidebar.astro';
|
||||
import { TeamsList } from '../../components/TeamsList';
|
||||
import AccountLayout from '../../layouts/AccountLayout.astro';
|
||||
import { DashboardPage } from '../../components/Dashboard/DashboardPage';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getAllBestPractices } from '../../lib/best-practice';
|
||||
import { getRoadmapsByTag } from '../../lib/roadmap';
|
||||
|
||||
const roleRoadmaps = await getRoadmapsByTag('role-roadmap');
|
||||
const skillRoadmaps = await getRoadmapsByTag('skill-roadmap');
|
||||
const bestPractices = await getAllBestPractices();
|
||||
|
||||
const enrichedRoleRoadmaps = roleRoadmaps
|
||||
.filter((roadmapItem) => !roadmapItem.frontmatter.isHidden)
|
||||
.map((roadmap) => {
|
||||
const { frontmatter } = roadmap;
|
||||
|
||||
return {
|
||||
id: roadmap.id,
|
||||
url: `/${roadmap.id}`,
|
||||
title: frontmatter.briefTitle,
|
||||
description: frontmatter.briefDescription,
|
||||
relatedRoadmapIds: frontmatter.relatedRoadmaps,
|
||||
renderer: frontmatter.renderer,
|
||||
metadata: {
|
||||
tags: frontmatter.tags,
|
||||
},
|
||||
};
|
||||
});
|
||||
const enrichedSkillRoadmaps = skillRoadmaps
|
||||
.filter((roadmapItem) => !roadmapItem.frontmatter.isHidden)
|
||||
.map((roadmap) => {
|
||||
const { frontmatter } = roadmap;
|
||||
|
||||
return {
|
||||
id: roadmap.id,
|
||||
url: `/${roadmap.id}`,
|
||||
title:
|
||||
frontmatter.briefTitle === 'Go' ? 'Go Roadmap' : frontmatter.briefTitle,
|
||||
description: frontmatter.briefDescription,
|
||||
relatedRoadmapIds: frontmatter.relatedRoadmaps,
|
||||
renderer: frontmatter.renderer,
|
||||
metadata: {
|
||||
tags: frontmatter.tags,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const enrichedBestPractices = bestPractices.map((bestPractice) => {
|
||||
const { frontmatter } = bestPractice;
|
||||
|
||||
return {
|
||||
id: bestPractice.id,
|
||||
url: `/best-practices/${bestPractice.id}`,
|
||||
title: frontmatter.briefTitle,
|
||||
description: frontmatter.briefDescription,
|
||||
};
|
||||
});
|
||||
---
|
||||
|
||||
<AccountLayout
|
||||
title='Update Profile'
|
||||
noIndex={true}
|
||||
initialLoadingMessage={'Loading teams'}
|
||||
>
|
||||
<AccountSidebar hasDesktopSidebar={false} activePageId='team' activePageTitle='Teams'>
|
||||
<TeamsList client:only="react" />
|
||||
</AccountSidebar>
|
||||
</AccountLayout>
|
||||
<BaseLayout title='Dashboard' noIndex={true}>
|
||||
<DashboardPage
|
||||
builtInRoleRoadmaps={enrichedRoleRoadmaps}
|
||||
builtInSkillRoadmaps={enrichedSkillRoadmaps}
|
||||
builtInBestPractices={enrichedBestPractices}
|
||||
isTeamPage={true}
|
||||
client:load
|
||||
/>
|
||||
<div slot='open-source-banner'></div>
|
||||
</BaseLayout>
|
||||
|
Loading…
x
Reference in New Issue
Block a user