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>
pull/7371/head
Arik Chakma 2 months ago committed by GitHub
parent 3f7e50907a
commit 8b0c536750
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      .astro/settings.json
  2. 2
      src/components/Activity/ActivityStream.tsx
  3. 2
      src/components/Activity/EmptyActivity.tsx
  4. 21
      src/components/Activity/EmptyStream.tsx
  5. 4
      src/components/CreateTeam/RoadmapSelector.tsx
  6. 22
      src/components/CreateTeam/SelectRoadmapModal.tsx
  7. 42
      src/components/Dashboard/DashboardPage.tsx
  8. 303
      src/components/Dashboard/DashboardTeamRoadmaps.tsx
  9. 13
      src/components/Dashboard/PersonalDashboard.tsx
  10. 114
      src/components/Dashboard/TeamDashboard.tsx
  11. 4
      src/components/TeamActivity/TeamActivityItem.tsx
  12. 57
      src/components/TeamActivity/TeamActivityPage.tsx
  13. 2
      src/components/TeamActivity/TeamEmptyStream.tsx
  14. 11
      src/components/TeamMemberDetails/TeamMemberEmptyPage.tsx
  15. 9
      src/components/TeamMembers/InviteMemberPopup.tsx
  16. 8
      src/components/TeamRoadmapsList/TeamRoadmaps.tsx
  17. 48
      src/data/projects/log-analyser.md
  18. 8
      src/pages/dashboard.astro
  19. 77
      src/pages/team/index.astro

@ -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)}>
{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&nbsp;
<a href="/roadmaps" className="mt-4 text-blue-500 hover:underline">
Roadmaps
</a>
,&nbsp;
<a
href="/best-practices"
className="mt-4 text-blue-500 hover:underline"
>
Best Practices
</a>
&nbsp;or&nbsp;
<a href="/questions" className="mt-4 text-blue-500 hover:underline">
Questions
</a>
&nbsp;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>
);

@ -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,
)}
<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"
>
<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;
});
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;
const canManageCurrentTeam = ['admin', 'manager'].includes(
currentMember?.role!,
);
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}
<section className="mt-8">
{isInvitingMember && (
<InviteMemberPopup
onInvited={() => {
toast.success('Invite sent');
getTeamProgress().finally(() => null);
setIsInvitingMember(false);
}}
onClose={() => {
setIsInvitingMember(false);
}}
/>
);
})}
</div>
)}
<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}
&nbsp;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,14 +189,45 @@ 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 (
<>
{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) {
/>
</>
) : (
<>
{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

@ -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…
Cancel
Save