Skip to content

Commit

Permalink
feat: implement team activity stream (#5565)
Browse files Browse the repository at this point in the history
* wip

* feat: implement team activity page

* feat: add pagination

* fix: add max height

* Team activity updates

* Remove invalid activityes

* Team activity page

* fix: team roadmap versions not working

* Add team activity items

---------

Co-authored-by: Kamran Ahmed <kamranahmed.se@gmail.com>
  • Loading branch information
arikchakma and kamranahmedse committed Apr 30, 2024
1 parent 8ceedad commit f2a2ac9
Show file tree
Hide file tree
Showing 22 changed files with 2,891 additions and 2,270 deletions.
2 changes: 1 addition & 1 deletion src/components/Activity/ActivityTopicsModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ export function ActivityTopicsModal(props: ActivityTopicDetailsProps) {
/>
</a>
</span>
<ul className="flex flex-col gap-1">
<ul className="flex max-h-[50vh] flex-col gap-1 overflow-y-auto max-md:max-h-full">
{topicIds.map((topicId) => {
const topicTitle = topicTitles[topicId] || 'Unknown Topic';

Expand Down
1 change: 1 addition & 0 deletions src/components/Authenticator/authenticator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ function handleGuest() {
'/account',
'/team',
'/team/progress',
'/team/activity',
'/team/roadmaps',
'/team/new',
'/team/members',
Expand Down
2 changes: 1 addition & 1 deletion src/components/CreateTeam/Step4.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export function Step4({ team }: Step4Props) {
Your team has been created. Happy learning!
</p>
<a
href={`/team/progress?t=${team._id}`}
href={`/team/activity?t=${team._id}`}
className="mt-4 rounded-md bg-black px-5 py-1.5 text-sm text-white"
>
View Team
Expand Down
14 changes: 6 additions & 8 deletions src/components/FrameRenderer/renderer.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
import { wireframeJSONToSVG } from 'roadmap-renderer';
import { httpPost } from '../../lib/http';
import { isLoggedIn } from '../../lib/jwt';
import type {
ResourceProgressType,
ResourceType,
} from '../../lib/resource-progress';
import {
refreshProgressCounters,
renderResourceProgress,
renderTopicProgress,
updateResourceProgress,
} from '../../lib/resource-progress';
import type {
ResourceProgressType,
ResourceType,
} from '../../lib/resource-progress';
import { pageProgressMessage } from '../../stores/page';
import { showLoginPopup } from '../../lib/popup';
import { replaceChildren } from '../../lib/dom.ts';
import {setUrlParams} from "../../lib/browser.ts";
import { setUrlParams } from '../../lib/browser.ts';

export class Renderer {
resourceId: string;
Expand Down Expand Up @@ -94,7 +93,6 @@ export class Renderer {
})
.then((svg) => {
replaceChildren(this.containerEl!, svg);
// this.containerEl?.replaceChildren(svg);
})
.then(() => {
return renderResourceProgress(
Expand Down Expand Up @@ -143,7 +141,7 @@ export class Renderer {
const newJsonFileSlug = newJsonUrl.split('/').pop()?.replace('.json', '');

const type = this.resourceType[0]; // r for roadmap, b for best-practices
setUrlParams({ [type]: newJsonFileSlug! })
setUrlParams({ [type]: newJsonFileSlug! });

this.jsonToSvg(newJsonUrl)?.then(() => {});
}
Expand Down
2 changes: 1 addition & 1 deletion src/components/HeroSection/HeroRoadmaps.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ export function HeroRoadmaps(props: ProgressListProps) {
Team{' '}
<a
className="mx-1 font-medium underline underline-offset-2 transition-colors hover:text-gray-300"
href={`/team/progress?t=${currentTeam?.id}`}
href={`/team/activity?t=${currentTeam?.id}`}
>
{teamName}
</a>
Expand Down
2 changes: 1 addition & 1 deletion src/components/Navigation/DropdownTeamList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export function DropdownTeamList(props: DropdownTeamListProps) {
if (team.status === 'invited') {
pageLink = `/respond-invite?i=${team.memberId}`;
} else if (team.status === 'joined') {
pageLink = `/team/progress?t=${team._id}`;
pageLink = `/team/activity?t=${team._id}`;
}

return (
Expand Down
2 changes: 1 addition & 1 deletion src/components/Notification/NotificationPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export function NotificationPage() {
}

if (status === 'accept') {
window.location.href = `/team/progress?t=${response.teamId}`;
window.location.href = `/team/activity?t=${response.teamId}`;
} else {
window.dispatchEvent(
new CustomEvent('refresh-notification', {
Expand Down
8 changes: 4 additions & 4 deletions src/components/RespondInviteForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export function RespondInviteForm() {
window.location.href = '/';
return;
}
window.location.href = `/team/progress?t=${response.teamId}`;
window.location.href = `/team/activity?t=${response.teamId}`;
}

if (isLoadingInvite) {
Expand Down Expand Up @@ -106,7 +106,7 @@ export function RespondInviteForm() {

return (
<div className="container text-center">
<BuildingIcon className="mx-auto mb-4 mt-24 w-20 opacity-20" />
<BuildingIcon className="mx-auto mb-4 mt-24 w-20 h-20 opacity-20" />

<h2 className={'mb-1 text-2xl font-bold'}>Join Team</h2>
<p className="mb-3 text-base leading-6 text-gray-600">
Expand Down Expand Up @@ -139,7 +139,7 @@ export function RespondInviteForm() {
pageProgressMessage.set('');
})
}
className="flex-grow cursor-pointer rounded-lg bg-gray-200 px-3 py-2 text-center"
className="flex-grow cursor-pointer rounded-lg hover:bg-gray-300 bg-gray-200 px-3 py-2 text-center"
>
Accept
</button>
Expand All @@ -150,7 +150,7 @@ export function RespondInviteForm() {
pageProgressMessage.set('');
})
}
className="flex-grow cursor-pointer rounded-lg bg-red-500 px-3 py-2 text-white disabled:opacity-40"
className="flex-grow cursor-pointer rounded-lg bg-red-500 hover:bg-red-600 px-3 py-2 text-white disabled:opacity-40"
>
Reject
</button>
Expand Down
195 changes: 195 additions & 0 deletions src/components/TeamActivity/TeamActivityItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import { useState } from 'react';
import { getRelativeTimeString } from '../../lib/date';
import type { TeamStreamActivity } from './TeamActivityPage';
import { ChevronsDown, ChevronsUp } from 'lucide-react';

type TeamActivityItemProps = {
onTopicClick?: (activity: TeamStreamActivity) => void;
user: {
activities: TeamStreamActivity[];
_id: string;
name: string;
avatar?: string | undefined;
username?: string | undefined;
};
};

export function TeamActivityItem(props: TeamActivityItemProps) {
const { user, onTopicClick } = props;
const { activities } = user;

const [showAll, setShowAll] = useState(false);

const resourceLink = (activity: TeamStreamActivity) => {
const {
resourceId,
resourceTitle,
resourceType,
isCustomResource,
resourceSlug,
} = activity;

const resourceUrl =
resourceType === 'question'
? `/questions/${resourceId}`
: resourceType === 'best-practice'
? `/best-practices/${resourceId}`
: isCustomResource && resourceType === 'roadmap'
? `/r/${resourceSlug}`
: `/${resourceId}`;

return (
<a
className="font-medium underline transition-colors hover:cursor-pointer hover:text-black"
target="_blank"
href={resourceUrl}
>
{resourceTitle}
</a>
);
};

const timeAgo = (date: string | Date) => (
<span className="ml-1 text-xs text-gray-400">
{getRelativeTimeString(new Date(date).toISOString())}
</span>
);
const userAvatar = user.avatar
? `${import.meta.env.PUBLIC_AVATAR_BASE_URL}/${user.avatar}`
: '/images/default-avatar.png';

const username = (
<>
<img
className="mr-1 inline-block h-5 w-5 rounded-full"
src={userAvatar}
alt={user.name}
/>
<span className="font-medium">{user?.name || 'Unknown'}</span>{' '}
</>
);

if (activities.length === 1) {
const activity = activities[0];
const { actionType, topicIds } = activity;
const topicCount = topicIds?.length || 0;

return (
<li
key={user._id}
className="flex items-center flex-wrap gap-1 rounded-md border px-2 py-2.5 text-sm"
>
{actionType === 'in_progress' && (
<>
{username} started{' '}
<button
className="font-medium underline underline-offset-2 hover:text-black"
onClick={() => onTopicClick?.(activity)}
>
{topicCount} topic{topicCount > 1 ? 's' : ''}
</button>{' '}
in {resourceLink(activity)} {timeAgo(activity.updatedAt)}
</>
)}

{actionType === 'done' && (
<>
{username} completed{' '}
<button
className="font-medium underline underline-offset-2 hover:text-black"
onClick={() => onTopicClick?.(activity)}
>
{topicCount} topic{topicCount > 1 ? 's' : ''}
</button>{' '}
in {resourceLink(activity)} {timeAgo(activity.updatedAt)}
</>
)}
{actionType === 'answered' && (
<>
{username} answered {topicCount} question
{topicCount > 1 ? 's' : ''} in {resourceLink(activity)}{' '}
{timeAgo(activity.updatedAt)}
</>
)}
</li>
);
}

const uniqueResourcesCount = new Set(
activities.map((activity) => activity.resourceId),
).size;

const activityLimit = showAll ? activities.length : 5;

return (
<li key={user._id} className="rounded-md border overflow-hidden">
<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}{' '}
resources
</h3>
<div className="py-3">
<ul className="flex flex-col gap-2 ml-2 sm:ml-[36px]">
{activities.slice(0, activityLimit).map((activity) => {
const { actionType, topicIds } = activity;
const topicCount = topicIds?.length || 0;

return (
<li key={activity._id} className="text-sm text-gray-600">
{actionType === 'in_progress' && (
<>
Started{' '}
<button
className="font-medium underline underline-offset-2 hover:text-black"
onClick={() => onTopicClick?.(activity)}
>
{topicCount} topic{topicCount > 1 ? 's' : ''}
</button>{' '}
in {resourceLink(activity)} {timeAgo(activity.updatedAt)}
</>
)}
{actionType === 'done' && (
<>
Completed{' '}
<button
className="font-medium underline underline-offset-2 hover:text-black"
onClick={() => onTopicClick?.(activity)}
>
{topicCount} topic{topicCount > 1 ? 's' : ''}
</button>{' '}
in {resourceLink(activity)} {timeAgo(activity.updatedAt)}
</>
)}
{actionType === 'answered' && (
<>
Answered {topicCount} question
{topicCount > 1 ? 's' : ''} in {resourceLink(activity)}{' '}
{timeAgo(activity.updatedAt)}
</>
)}
</li>
);
})}
</ul>

{activities.length > 5 && (
<button
className="mt-3 flex items-center gap-2 rounded-md border border-gray-300 p-1 text-xs uppercase tracking-wide text-gray-600 transition-colors hover:border-black hover:bg-black hover:text-white"
onClick={() => setShowAll(!showAll)}
>
{showAll ? (
<>
<ChevronsUp size={14} />
Show less
</>
) : (
<>
<ChevronsDown size={14} />
Show more
</>
)}
</button>
)}
</div>
</li>
);
}

0 comments on commit f2a2ac9

Please sign in to comment.