computer-scienceangular-roadmapbackend-roadmapblockchain-roadmapdba-roadmapdeveloper-roadmapdevops-roadmapfrontend-roadmapgo-roadmaphactoberfestjava-roadmapjavascript-roadmapnodejs-roadmappython-roadmapqa-roadmapreact-roadmaproadmapstudy-planvue-roadmapweb3-roadmap
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
281 lines
7.4 KiB
281 lines
7.4 KiB
import { useEffect, useState, useMemo } from 'react'; |
|
import { useToast } from '../../hooks/use-toast'; |
|
import { getUrlParams } from '../../lib/browser'; |
|
import { httpGet } from '../../lib/http'; |
|
import type { ResourceType } from '../../lib/resource-progress'; |
|
import type { AllowedActivityActionType } from '../Activity/ActivityStream'; |
|
import { pageProgressMessage } from '../../stores/page'; |
|
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; |
|
resourceType: ResourceType | 'question'; |
|
resourceId: string; |
|
resourceTitle: string; |
|
resourceSlug?: string; |
|
isCustomResource?: boolean; |
|
actionType: AllowedActivityActionType; |
|
topicTitles?: string[]; |
|
createdAt: Date; |
|
updatedAt: Date; |
|
}; |
|
|
|
export interface TeamActivityStreamDocument { |
|
_id?: string; |
|
teamId: string; |
|
userId: string; |
|
activity: TeamStreamActivity[]; |
|
createdAt: Date; |
|
updatedAt: Date; |
|
} |
|
|
|
type GetTeamActivityResponse = { |
|
data: { |
|
users: { |
|
_id: string; |
|
name: string; |
|
avatar?: string; |
|
username?: string; |
|
memberId?: string; |
|
}[]; |
|
activities: TeamActivityStreamDocument[]; |
|
}; |
|
totalCount: number; |
|
totalPages: number; |
|
currPage: number; |
|
perPage: number; |
|
}; |
|
|
|
type TeamActivityPageProps = { |
|
teamId?: string; |
|
canManageCurrentTeam?: boolean; |
|
}; |
|
|
|
export function TeamActivityPage(props: TeamActivityPageProps) { |
|
const { teamId: defaultTeamId, canManageCurrentTeam = false } = props; |
|
const { t: teamId = defaultTeamId } = getUrlParams(); |
|
|
|
const toast = useToast(); |
|
|
|
const [isLoading, setIsLoading] = useState(true); |
|
const [selectedActivity, setSelectedActivity] = |
|
useState<TeamStreamActivity | null>(null); |
|
const [teamActivities, setTeamActivities] = useState<GetTeamActivityResponse>( |
|
{ |
|
data: { |
|
users: [], |
|
activities: [], |
|
}, |
|
totalCount: 0, |
|
totalPages: 0, |
|
currPage: 1, |
|
perPage: 21, |
|
}, |
|
); |
|
const [currPage, setCurrPage] = useState(1); |
|
|
|
const getTeamProgress = async (currPage: number = 1) => { |
|
const { response, error } = await httpGet<GetTeamActivityResponse>( |
|
`${import.meta.env.PUBLIC_API_URL}/v1-get-team-activity/${teamId}`, |
|
{ |
|
currPage, |
|
}, |
|
); |
|
if (error || !response) { |
|
toast.error(error?.message || 'Failed to get team activity'); |
|
return; |
|
} |
|
|
|
setTeamActivities(response); |
|
setCurrPage(response.currPage); |
|
}; |
|
|
|
useEffect(() => { |
|
if (!teamId) { |
|
return; |
|
} |
|
|
|
setIsLoading(true); |
|
setTeamActivities({ |
|
data: { |
|
users: [], |
|
activities: [], |
|
}, |
|
totalCount: 0, |
|
totalPages: 0, |
|
currPage: 1, |
|
perPage: 21, |
|
}); |
|
setCurrPage(1); |
|
getTeamProgress().then(() => { |
|
pageProgressMessage.set(''); |
|
setIsLoading(false); |
|
}); |
|
}, [teamId]); |
|
|
|
const { users, activities } = teamActivities?.data; |
|
const validActivities = useMemo(() => { |
|
return activities?.filter((activity) => { |
|
return ( |
|
activity.activity.length > 0 && |
|
activity.activity.some((t) => (t?.topicTitles?.length || 0) > 0) |
|
); |
|
}); |
|
}, [activities]); |
|
|
|
const sortedUniqueCreatedAt = useMemo(() => { |
|
return new Set( |
|
validActivities |
|
?.map((activity) => new Date(activity.createdAt).setHours(0, 0, 0, 0)) |
|
.sort((a, b) => { |
|
return new Date(b).getTime() - new Date(a).getTime(); |
|
}), |
|
); |
|
}, [validActivities]); |
|
|
|
const usersWithActivities = useMemo(() => { |
|
const enrichedUsers: { |
|
_id: string; |
|
name: string; |
|
avatar?: string; |
|
username?: string; |
|
activities: TeamStreamActivity[]; |
|
}[] = []; |
|
|
|
for (const uniqueCreatedAt of sortedUniqueCreatedAt) { |
|
const uniqueActivities = validActivities.filter( |
|
(activity) => |
|
new Date(activity.createdAt).setHours(0, 0, 0, 0) === uniqueCreatedAt, |
|
); |
|
|
|
const usersWithUniqueActivities = users |
|
.map((user) => { |
|
const userActivities = uniqueActivities |
|
.filter((activity) => activity.userId === user._id) |
|
.flatMap((activity) => activity.activity) |
|
.filter((activity) => (activity?.topicTitles?.length || 0) > 0) |
|
.sort((a, b) => { |
|
return ( |
|
new Date(b.updatedAt).getTime() - |
|
new Date(a.updatedAt).getTime() |
|
); |
|
}); |
|
|
|
return { |
|
...user, |
|
activities: userActivities, |
|
}; |
|
}) |
|
.filter((user) => user.activities.length > 0) |
|
.sort((a, b) => { |
|
return ( |
|
new Date(b.activities[0].updatedAt).getTime() - |
|
new Date(a.activities[0].updatedAt).getTime() |
|
); |
|
}); |
|
|
|
enrichedUsers.push(...usersWithUniqueActivities); |
|
} |
|
|
|
return enrichedUsers; |
|
}, [users, activities]); |
|
|
|
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 ( |
|
<> |
|
{selectedActivity && ( |
|
<TeamActivityTopicsModal |
|
activity={selectedActivity} |
|
onClose={() => setSelectedActivity(null)} |
|
/> |
|
)} |
|
|
|
{usersWithActivities.length > 0 ? ( |
|
<> |
|
{sectionHeading} |
|
<ul className="mb-4 mt-2 flex flex-col gap-3"> |
|
{usersWithActivities.map((user, index) => { |
|
return ( |
|
<TeamActivityItem |
|
key={`${user._id}-${index}`} |
|
user={user} |
|
teamId={teamId} |
|
onTopicClick={setSelectedActivity} |
|
/> |
|
); |
|
})} |
|
</ul> |
|
|
|
<Pagination |
|
currPage={currPage} |
|
totalPages={teamActivities.totalPages} |
|
totalCount={teamActivities.totalCount} |
|
perPage={teamActivities.perPage} |
|
onPageChange={(page) => { |
|
setCurrPage(page); |
|
pageProgressMessage.set('Loading...'); |
|
getTeamProgress(page).finally(() => { |
|
pageProgressMessage.set(''); |
|
}); |
|
}} |
|
/> |
|
</> |
|
) : ( |
|
<> |
|
{sectionHeading} |
|
<div className="rounded-lg border bg-white p-4"> |
|
<TeamEmptyStream teamId={teamId} /> |
|
</div> |
|
</> |
|
)} |
|
</> |
|
); |
|
}
|
|
|