feat: team personal progress only

feat/personal-progress
Arik Chakma 7 months ago
parent a5c28f09a7
commit 4df03197ac
  1. 23
      src/components/CreateTeam/CreateTeamForm.tsx
  2. 54
      src/components/CreateTeam/Step1.tsx
  3. 84
      src/components/TeamActivity/TeamActivityPage.tsx
  4. 40
      src/components/TeamSettings/UpdateTeamForm.tsx
  5. 1
      tsconfig.json

@ -9,7 +9,7 @@ import { pageProgressMessage } from '../../stores/page';
import type { TeamResourceConfig } from './RoadmapSelector'; import type { TeamResourceConfig } from './RoadmapSelector';
import { Step3 } from './Step3'; import { Step3 } from './Step3';
import { Step4 } from './Step4'; import { Step4 } from './Step4';
import {useToast} from "../../hooks/use-toast"; import { useToast } from '../../hooks/use-toast';
export interface TeamDocument { export interface TeamDocument {
_id?: string; _id?: string;
@ -22,6 +22,7 @@ export interface TeamDocument {
linkedIn?: string; linkedIn?: string;
}; };
type: ValidTeamType; type: ValidTeamType;
personalProgressOnly?: boolean;
canMemberSendInvite: boolean; canMemberSendInvite: boolean;
teamSize?: ValidTeamSize; teamSize?: ValidTeamSize;
createdAt: Date; createdAt: Date;
@ -40,10 +41,10 @@ export function CreateTeamForm() {
async function loadTeam( async function loadTeam(
teamIdToFetch: string, teamIdToFetch: string,
requiredStepIndex: number | string requiredStepIndex: number | string,
) { ) {
const { response, error } = await httpGet<TeamDocument>( const { response, error } = await httpGet<TeamDocument>(
`${import.meta.env.PUBLIC_API_URL}/v1-get-team/${teamIdToFetch}` `${import.meta.env.PUBLIC_API_URL}/v1-get-team/${teamIdToFetch}`,
); );
if (error || !response) { if (error || !response) {
@ -70,7 +71,7 @@ export function CreateTeamForm() {
async function loadTeamResourceConfig(teamId: string) { async function loadTeamResourceConfig(teamId: string) {
const { error, response } = await httpGet<TeamResourceConfig>( const { error, response } = await httpGet<TeamResourceConfig>(
`${import.meta.env.PUBLIC_API_URL}/v1-get-team-resource-config/${teamId}` `${import.meta.env.PUBLIC_API_URL}/v1-get-team-resource-config/${teamId}`,
); );
if (error || !Array.isArray(response)) { if (error || !Array.isArray(response)) {
console.error(error); console.error(error);
@ -96,7 +97,7 @@ export function CreateTeamForm() {
}, [teamId, queryStepIndex]); }, [teamId, queryStepIndex]);
const [selectedTeamType, setSelectedTeamType] = useState<ValidTeamType>( const [selectedTeamType, setSelectedTeamType] = useState<ValidTeamType>(
team?.type || 'company' team?.type || 'company',
); );
const [completedSteps, setCompletedSteps] = useState([0]); const [completedSteps, setCompletedSteps] = useState([0]);
@ -191,13 +192,17 @@ export function CreateTeamForm() {
return ( return (
<div className={'mx-auto max-w-[700px] py-1 md:py-6'}> <div className={'mx-auto max-w-[700px] py-1 md:py-6'}>
<div className={'mb-3 md:mb-8 pb-3 md:pb-0 border-b md:border-b-0 flex flex-col items-start md:items-center'}> <div
<h1 className={'text-xl md:text-4xl font-bold'}>Create Team</h1> className={
<p className={'mt-1 md:mt-2 text-sm md:text-base text-gray-500'}> 'mb-3 flex flex-col items-start border-b pb-3 md:mb-8 md:items-center md:border-b-0 md:pb-0'
}
>
<h1 className={'text-xl font-bold md:text-4xl'}>Create Team</h1>
<p className={'mt-1 text-sm text-gray-500 md:mt-2 md:text-base'}>
Complete the steps below to create your team Complete the steps below to create your team
</p> </p>
</div> </div>
<div className="mb-8 mt-8 hidden sm:flex w-full"> <div className="mb-8 mt-8 hidden w-full sm:flex">
<Stepper <Stepper
activeIndex={stepIndex} activeIndex={stepIndex}
completeSteps={completedSteps} completeSteps={completedSteps}

@ -46,7 +46,10 @@ export function Step1(props: Step1Props) {
const [linkedInUrl, setLinkedInUrl] = useState(team?.links?.linkedIn || ''); const [linkedInUrl, setLinkedInUrl] = useState(team?.links?.linkedIn || '');
const [gitHubUrl, setGitHubUrl] = useState(team?.links?.github || ''); const [gitHubUrl, setGitHubUrl] = useState(team?.links?.github || '');
const [teamSize, setTeamSize] = useState<ValidTeamSize>( const [teamSize, setTeamSize] = useState<ValidTeamSize>(
team?.teamSize || ('' as any) team?.teamSize || ('' as any),
);
const [personalProgressOnly, setPersonalProgressOnly] = useState(
team?.personalProgressOnly ?? true,
); );
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => { const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
@ -74,7 +77,8 @@ export function Step1(props: Step1Props) {
}), }),
roadmapIds: [], roadmapIds: [],
bestPracticeIds: [], bestPracticeIds: [],
} personalProgressOnly,
},
)); ));
if (error || !response?._id) { if (error || !response?._id) {
@ -96,7 +100,8 @@ export function Step1(props: Step1Props) {
teamSize, teamSize,
linkedInUrl: linkedInUrl || undefined, linkedInUrl: linkedInUrl || undefined,
}), }),
} personalProgressOnly,
},
)); ));
if (error || (response as any)?.status !== 'ok') { if (error || (response as any)?.status !== 'ok') {
@ -116,6 +121,7 @@ export function Step1(props: Step1Props) {
}, },
type: selectedTeamType, type: selectedTeamType,
teamSize: teamSize!, teamSize: teamSize!,
personalProgressOnly,
}); });
} }
}; };
@ -168,7 +174,10 @@ export function Step1(props: Step1Props) {
{selectedTeamType === 'company' && ( {selectedTeamType === 'company' && (
<div className="mt-4 flex w-full flex-col"> <div className="mt-4 flex w-full flex-col">
<label htmlFor="website" className="text-sm leading-none text-slate-500"> <label
htmlFor="website"
className="text-sm leading-none text-slate-500"
>
Company LinkedIn URL Company LinkedIn URL
</label> </label>
<input <input
@ -187,7 +196,10 @@ export function Step1(props: Step1Props) {
)} )}
<div className="mt-4 flex w-full flex-col"> <div className="mt-4 flex w-full flex-col">
<label htmlFor="website" className="text-sm leading-none text-slate-500"> <label
htmlFor="website"
className="text-sm leading-none text-slate-500"
>
GitHub Organization URL GitHub Organization URL
</label> </label>
<input <input
@ -221,16 +233,40 @@ export function Step1(props: Step1Props) {
setTeamSize((e.target as HTMLSelectElement).value as any) setTeamSize((e.target as HTMLSelectElement).value as any)
} }
> >
<option value=""> <option value="">Select team size</option>
Select team size
</option>
{validTeamSizes.map((size) => ( {validTeamSizes.map((size) => (
<option key={size} value={size}>{size} people</option> <option key={size} value={size}>
{size} people
</option>
))} ))}
</select> </select>
</div> </div>
)} )}
<div className="mt-4 flex h-[42px] w-full items-center rounded-lg border border-gray-300 px-3 py-2 shadow-sm">
<label
htmlFor="personal-progress-only"
className="flex items-center gap-2 text-sm leading-none text-slate-500"
>
<input
type="checkbox"
name="personal-progress-only"
id="personal-progress-only"
checked={personalProgressOnly}
onChange={(e) =>
setPersonalProgressOnly((e.target as HTMLInputElement).checked)
}
/>
<span>Members can only see their personal progress</span>
</label>
</div>
{personalProgressOnly && (
<p className="mt-2 rounded-lg border border-orange-300 bg-orange-50 p-2 text-sm text-orange-700">
Only admins and managers will be able to see the progress of members
</p>
)}
{error && ( {error && (
<div className="mt-4 flex w-full flex-col"> <div className="mt-4 flex w-full flex-col">
<span className="text-sm text-red-500">{error}</span> <span className="text-sm text-red-500">{error}</span>

@ -98,38 +98,70 @@ export function TeamActivityPage() {
}, [teamId]); }, [teamId]);
const { users, activities } = teamActivities?.data; const { users, activities } = teamActivities?.data;
const usersWithActivities = useMemo(() => { const validActivities = useMemo(() => {
const validActivities = activities.filter((activity) => { return activities?.filter((activity) => {
return ( return (
activity.activity.length > 0 && activity.activity.length > 0 &&
activity.activity.some((t) => (t?.topicIds?.length || 0) > 0) activity.activity.some((t) => (t?.topicIds?.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?.topicIds?.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 users return enrichedUsers;
.map((user) => {
const userActivities = validActivities
.filter((activity) => activity.userId === user._id)
.flatMap((activity) => activity.activity)
.filter((activity) => (activity?.topicIds?.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()
);
});
}, [users, activities]); }, [users, activities]);
if (!teamId) { if (!teamId) {

@ -24,6 +24,7 @@ export function UpdateTeamForm() {
const [gitHub, setGitHub] = useState(''); const [gitHub, setGitHub] = useState('');
const [teamType, setTeamType] = useState(''); const [teamType, setTeamType] = useState('');
const [teamSize, setTeamSize] = useState(''); const [teamSize, setTeamSize] = useState('');
const [personalProgressOnly, setPersonalProgressOnly] = useState(true);
const validTeamSizes = [ const validTeamSizes = [
'0-1', '0-1',
'2-10', '2-10',
@ -55,11 +56,12 @@ export function UpdateTeamForm() {
website, website,
type: teamType, type: teamType,
gitHubUrl: gitHub || undefined, gitHubUrl: gitHub || undefined,
personalProgressOnly,
...(teamType === 'company' && { ...(teamType === 'company' && {
teamSize, teamSize,
linkedInUrl: linkedIn || undefined, linkedInUrl: linkedIn || undefined,
}), }),
} },
); );
if (error) { if (error) {
@ -77,7 +79,7 @@ export function UpdateTeamForm() {
async function loadTeam() { async function loadTeam() {
const { response, error } = await httpGet<TeamDocument>( const { response, error } = await httpGet<TeamDocument>(
`${import.meta.env.PUBLIC_API_URL}/v1-get-team/${teamId}` `${import.meta.env.PUBLIC_API_URL}/v1-get-team/${teamId}`,
); );
if (error || !response) { if (error || !response) {
console.log(error); console.log(error);
@ -90,6 +92,7 @@ export function UpdateTeamForm() {
setLinkedIn(response?.links?.linkedIn || ''); setLinkedIn(response?.links?.linkedIn || '');
setGitHub(response?.links?.github || ''); setGitHub(response?.links?.github || '');
setTeamType(response.type); setTeamType(response.type);
setPersonalProgressOnly(response.personalProgressOnly ?? true);
if (response.teamSize) { if (response.teamSize) {
setTeamSize(response.teamSize); setTeamSize(response.teamSize);
} }
@ -205,16 +208,14 @@ export function UpdateTeamForm() {
<select <select
name="type" name="type"
id="type" id="type"
className="mt-2 block w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1" className="mt-2 block h-[42px] w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1"
disabled={isDisabled} disabled={isDisabled}
value={teamType || ''} value={teamType || ''}
onChange={(e) => onChange={(e) =>
setTeamType((e.target as HTMLSelectElement).value as any) setTeamType((e.target as HTMLSelectElement).value as any)
} }
> >
<option value=""> <option value="">Select type</option>
Select type
</option>
<option value="company">Company</option> <option value="company">Company</option>
<option value="study_group">Study Group</option> <option value="study_group">Study Group</option>
</select> </select>
@ -231,7 +232,7 @@ export function UpdateTeamForm() {
<select <select
name="team-size" name="team-size"
id="team-size" id="team-size"
className="mt-2 block w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1" className="mt-2 block h-[42px] w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1"
required={teamType === 'company'} required={teamType === 'company'}
disabled={isDisabled} disabled={isDisabled}
value={teamSize} value={teamSize}
@ -249,6 +250,31 @@ export function UpdateTeamForm() {
</div> </div>
)} )}
<div className="mt-4 flex h-[42px] w-full items-center rounded-lg border border-gray-300 px-3 py-2 shadow-sm">
<label
htmlFor="personal-progress-only"
className="flex items-center gap-2 text-sm leading-none text-slate-500"
>
<input
type="checkbox"
name="personal-progress-only"
id="personal-progress-only"
disabled={isDisabled}
checked={personalProgressOnly}
onChange={(e) =>
setPersonalProgressOnly((e.target as HTMLInputElement).checked)
}
/>
<span>Members can only see their personal progress</span>
</label>
</div>
{personalProgressOnly && (
<p className="mt-2 rounded-lg border border-orange-300 bg-orange-50 p-2 text-sm text-orange-700">
Only admins and managers will be able to see the progress of members
</p>
)}
<div className="mt-4 flex w-full flex-col"> <div className="mt-4 flex w-full flex-col">
<button <button
type="submit" type="submit"

@ -1,6 +1,7 @@
{ {
"extends": "astro/tsconfigs/strict", "extends": "astro/tsconfigs/strict",
"compilerOptions": { "compilerOptions": {
"module": "ESNext",
"moduleResolution": "node", "moduleResolution": "node",
"jsx": "react-jsx", "jsx": "react-jsx",
"jsxImportSource": "react" "jsxImportSource": "react"

Loading…
Cancel
Save