diff --git a/apps/client/src/pages/ProjectSettings.tsx b/apps/client/src/pages/ProjectSettings.tsx new file mode 100644 index 0000000..5af34ec --- /dev/null +++ b/apps/client/src/pages/ProjectSettings.tsx @@ -0,0 +1,146 @@ +import axios from 'axios'; +import { useMutation, useQueryClient, useSuspenseQuery } from '@tanstack/react-query'; +import { useParams } from '@tanstack/react-router'; +import { z } from 'zod'; +import { UserPlus } from 'lucide-react'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useAuth } from '@/contexts/authContext'; +import TabView from '@/components/TabView'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; +import { GetProjectMembersResponseDTO, InviteProjectMemberRequestDTO } from '@/types/project'; + +const formSchema = z.object({ + username: z + .string() + .min(1, 'Username is required.') + .regex(/^[a-zA-Z0-9 ]*$/, 'Only English letters and numbers are allowed.'), +}); + +function ProjectSettings() { + const auth = useAuth(); + const queryClient = useQueryClient(); + const { project } = useParams({ from: '/_auth/$project/settings' }); + const { data: members } = useSuspenseQuery({ + queryKey: ['project', project, 'members'], + queryFn: async () => { + try { + const members = await axios.get( + `/api/project/${project}/members`, + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${auth.accessToken}`, + }, + } + ); + return members.data.result; + } catch { + throw new Error('Failed to fetch members'); + } + }, + }); + const { isPending, mutate } = useMutation({ + mutationFn: async (data: InviteProjectMemberRequestDTO) => { + await axios.post(`/api/project/${project}/invite`, data, { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${auth.accessToken}`, + }, + }); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['project', project, 'members'] }); + }, + onError: (error) => { + alert('Failed to invite member'); + console.log('Failed to invite member', error); + }, + }); + const { + register, + handleSubmit, + formState: { errors }, + } = useForm({ + resolver: zodResolver(formSchema), + }); + + const onSubmit = (data: InviteProjectMemberRequestDTO) => { + mutate({ ...data, projectId: Number(project) }); + }; + + return ( + + Settings + +
+ + + Team Members + Manage your team members. + + +
+ {members.map((member) => ( +
+
+
+
+

{member.username}

+

+ {member.role === 'ADMIN' ? 'Owner' : 'Contributor'} +

+
+
+
+ ))} +
+ + + + + + Invite New Member + + Send an invitation to a new team member. + + + +
+
+ + +
+
+
+
+
+ + + ); +} + +export default ProjectSettings; diff --git a/apps/client/src/routes/_auth.$project.settings.tsx b/apps/client/src/routes/_auth.$project.settings.tsx index c22b861..1a59e78 100644 --- a/apps/client/src/routes/_auth.$project.settings.tsx +++ b/apps/client/src/routes/_auth.$project.settings.tsx @@ -1,14 +1,9 @@ import axios from 'axios'; -import { useSuspenseQuery } from '@tanstack/react-query'; import { createFileRoute } from '@tanstack/react-router'; -import { UserPlus } from 'lucide-react'; -import { useAuth } from '@/contexts/authContext'; -import TabView from '@/components/TabView'; -import { Button } from '@/components/ui/button'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; -import { Input } from '@/components/ui/input'; import { GetProjectMembersResponseDTO } from '@/types/project'; +import ProjectSettings from '@/pages/ProjectSettings'; + export const Route = createFileRoute('/_auth/$project/settings')({ loader: ({ context: { auth, queryClient }, params: { project } }) => { queryClient.ensureQueryData({ @@ -34,108 +29,3 @@ export const Route = createFileRoute('/_auth/$project/settings')({ errorComponent: () =>
Failed to fetch members
, component: ProjectSettings, }); - -function ProjectSettings() { - const auth = useAuth(); - const { project } = Route.useParams(); - const { data: members } = useSuspenseQuery({ - queryKey: ['project', project, 'members'], - queryFn: async () => { - try { - const members = await axios.get( - `/api/project/${project}/members`, - { - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${auth.accessToken}`, - }, - } - ); - return members.data.result; - } catch { - throw new Error('Failed to fetch members'); - } - }, - }); - - return ( - - Settings - -
- - - Team Members - Manage your team members. - - -
- {members.map((member) => ( -
-
-
-
-

{member.username}

-

- {member.role === 'ADMIN' ? 'Owner' : 'Contributor'} -

-
-
-
- ))} -
- - - - - - Invite New Member - - Send an invitation to a new team member. - - - -
-
- -
- -
-
-
- - {/* - - Pending Invitations - - Manage your pending team invitations. - - - -
- {invitations.map((invite) => ( -
-
-
-
-

{invite.username}

-
-
-
- ))} -
- - */} -
- - - ); -} diff --git a/apps/client/src/types/project.ts b/apps/client/src/types/project.ts index 1f528b3..3b591c7 100644 --- a/apps/client/src/types/project.ts +++ b/apps/client/src/types/project.ts @@ -26,3 +26,8 @@ export interface GetProjectMembersResponseDTO { message: string; result: ProjectMember[]; } + +export interface InviteProjectMemberRequestDTO { + username: string; + projectId: number; +}