Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement team activity stream #5565

Merged
merged 10 commits into from Apr 30, 2024
Merged
2 changes: 1 addition & 1 deletion src/components/Activity/ActivityTopicsModal.tsx
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
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
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
@@ -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
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
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
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
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
@@ -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>
);
}