mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2025-08-11 19:53:59 +02:00
feat: team personal progress only (#5586)
* feat: team personal progress only * fix: default false
This commit is contained in:
@@ -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,7 @@ 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 handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
||||||
@@ -74,7 +74,7 @@ export function Step1(props: Step1Props) {
|
|||||||
}),
|
}),
|
||||||
roadmapIds: [],
|
roadmapIds: [],
|
||||||
bestPracticeIds: [],
|
bestPracticeIds: [],
|
||||||
}
|
},
|
||||||
));
|
));
|
||||||
|
|
||||||
if (error || !response?._id) {
|
if (error || !response?._id) {
|
||||||
@@ -96,7 +96,7 @@ export function Step1(props: Step1Props) {
|
|||||||
teamSize,
|
teamSize,
|
||||||
linkedInUrl: linkedInUrl || undefined,
|
linkedInUrl: linkedInUrl || undefined,
|
||||||
}),
|
}),
|
||||||
}
|
},
|
||||||
));
|
));
|
||||||
|
|
||||||
if (error || (response as any)?.status !== 'ok') {
|
if (error || (response as any)?.status !== 'ok') {
|
||||||
@@ -168,7 +168,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 +190,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,11 +227,11 @@ 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>
|
||||||
|
@@ -98,23 +98,50 @@ 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]);
|
||||||
|
|
||||||
return users
|
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) => {
|
.map((user) => {
|
||||||
const userActivities = validActivities
|
const userActivities = uniqueActivities
|
||||||
.filter((activity) => activity.userId === user._id)
|
.filter((activity) => activity.userId === user._id)
|
||||||
.flatMap((activity) => activity.activity)
|
.flatMap((activity) => activity.activity)
|
||||||
.filter((activity) => (activity?.topicIds?.length || 0) > 0)
|
.filter((activity) => (activity?.topicIds?.length || 0) > 0)
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
return (
|
return (
|
||||||
new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
|
new Date(b.updatedAt).getTime() -
|
||||||
|
new Date(a.updatedAt).getTime()
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -130,6 +157,11 @@ export function TeamActivityPage() {
|
|||||||
new Date(a.activities[0].updatedAt).getTime()
|
new Date(a.activities[0].updatedAt).getTime()
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
enrichedUsers.push(...usersWithUniqueActivities);
|
||||||
|
}
|
||||||
|
|
||||||
|
return enrichedUsers;
|
||||||
}, [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(false);
|
||||||
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 ?? false);
|
||||||
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"
|
||||||
|
Reference in New Issue
Block a user