Skip to content

Commit 5cb8491

Browse files
fixes / ui changes and added functions for teams
1 parent 814cefc commit 5cb8491

File tree

26 files changed

+918
-436
lines changed

26 files changed

+918
-436
lines changed

src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/page.tsx

+74-26
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,19 @@ import { Search } from "@/components/Search";
33
import { TeamsCardList } from "@/components/Teams/TeamsCardList";
44
import { Button } from "@/components/ui/button";
55
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
6-
import { getOrganizationTitle } from "@/data/user/organizations";
7-
import { getAllProjectsInOrganization } from "@/data/user/projects";
6+
import { Separator } from "@/components/ui/separator";
7+
import { T } from "@/components/ui/Typography";
8+
import { getLoggedInUserOrganizationRole, getOrganizationTitle } from "@/data/user/organizations";
9+
import { getAllProjectsInOrganization, getProjectsForUser } from "@/data/user/projects";
810
import { getSlimTeamById, getTeams } from "@/data/user/teams";
11+
import { Tables } from "@/lib/database.types";
12+
import { Enum } from "@/types";
13+
import { serverGetLoggedInUser } from "@/utils/server/serverGetLoggedInUser";
914
import {
1015
organizationParamSchema,
1116
projectsfilterSchema
1217
} from "@/utils/zod-schemas/params";
13-
import { Layers, Plus } from "lucide-react";
18+
import { Layers, Plus, Settings } from "lucide-react";
1419
import type { Metadata } from 'next';
1520
import Link from "next/link";
1621
import { Suspense } from "react";
@@ -23,24 +28,46 @@ import TeamsLoadingFallback from "./TeamsLoadingFallback";
2328
async function Projects({
2429
organizationId,
2530
filters,
31+
userId,
32+
userRole,
2633
}: {
2734
organizationId: string;
2835
filters: z.infer<typeof projectsfilterSchema>;
36+
userId: string;
37+
userRole: Enum<'organization_member_role'>;
2938
}) {
30-
const projects = await getAllProjectsInOrganization({
31-
organizationId,
32-
...filters,
33-
});
39+
let projects: Tables<'projects'>[];
40+
41+
if (userRole === 'admin') {
42+
projects = await getAllProjectsInOrganization({
43+
organizationId,
44+
...filters,
45+
});
46+
} else {
47+
projects = await getProjectsForUser({
48+
userId,
49+
userRole,
50+
organizationId,
51+
...filters,
52+
});
53+
}
54+
3455
const projectWithTeamNames = await Promise.all(projects.map(async (project) => {
3556
if (project.team_id) {
36-
const team = await getSlimTeamById(project.team_id);
37-
const projectWithTeamName = { ...project, teamName: team.name };
38-
return projectWithTeamName;
57+
try {
58+
const team = await getSlimTeamById(project.team_id);
59+
return { ...project, teamName: team?.name || 'Unknown Team' };
60+
} catch (error) {
61+
console.error(`Error fetching team for project ${project.id}:`, error);
62+
return { ...project };
63+
}
3964
}
4065
return project;
4166
}));
67+
4268
return <ProjectsCardList projects={projectWithTeamNames} />;
4369
}
70+
4471
async function Teams({
4572
organizationId,
4673
filters,
@@ -65,38 +92,60 @@ export type DashboardProps = {
6592
async function Dashboard({ params, searchParams }: DashboardProps) {
6693
const { organizationId } = organizationParamSchema.parse(params);
6794
const validatedSearchParams = projectsfilterSchema.parse(searchParams);
95+
const { id: userId } = await serverGetLoggedInUser();
96+
const userRole = await getLoggedInUserOrganizationRole(organizationId);
6897

6998
return (
7099
<DashboardClientWrapper>
100+
<div className="flex justify-between gap-4 w-full">
101+
<T.H2>Dashboard</T.H2>
102+
<Link href={`/org/${organizationId}/settings`}>
103+
<Button className="w-fit" variant="outline">
104+
<Settings className="mr-2 h-4 w-4" />
105+
Organization Settings
106+
</Button>
107+
</Link>
108+
</div>
71109
<Card >
72-
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-6">
73-
<CardTitle className="text-3xl font-bold tracking-tight">Dashboard</CardTitle>
74-
<div className="flex space-x-4">
75-
<Link href={`/org/${organizationId}/projects/create`}>
76-
<Button variant="default" size="sm">
77-
<Plus className="mr-2 h-4 w-4" />
78-
Create Project
79-
</Button>
80-
</Link>
81-
</div>
82-
</CardHeader>
83-
<CardContent>
84-
<div className="flex items-center justify-between mb-6">
85-
<h2 className="text-2xl font-semibold tracking-tight">Recent Projects</h2>
110+
<CardHeader>
111+
<div className="flex items-center justify-between">
112+
<CardTitle>
113+
Recent Projects
114+
</CardTitle>
86115
<div className="flex items-center space-x-4">
87-
<Search className="w-[200px]" placeholder="Search projects" />
116+
<div className="flex items-center space-x-4">
117+
<Search placeholder="Search projects" />
118+
{/* <MultiSelect
119+
options={userTeams.map(team => ({ label: team.name, value: team.id }))}
120+
placeholder="Filter by teams"
121+
/> */}
122+
</div>
88123
<Button variant="secondary" size="sm" asChild>
89124
<Link href={`/org/${organizationId}/projects`}>
90125
<Layers className="mr-2 h-4 w-4" />
91126
View all projects
92127
</Link>
93128
</Button>
129+
<Separator orientation="vertical" className="h-6" />
130+
<div className="flex space-x-4">
131+
<Link href={`/org/${organizationId}/projects/create`}>
132+
<Button variant="default" size="sm">
133+
<Plus className="mr-2 h-4 w-4" />
134+
Create Project
135+
</Button>
136+
</Link>
137+
</div>
94138
</div>
95139
</div>
140+
</CardHeader>
141+
<CardContent>
142+
96143
<Suspense fallback={<ProjectsLoadingFallback quantity={3} />}>
97144
<Projects
98145
organizationId={organizationId}
99146
filters={validatedSearchParams}
147+
userId={userId}
148+
userRole={userRole}
100149
/>
101150
{validatedSearchParams.query && (
102151
<p className="mt-4 text-sm text-muted-foreground">
@@ -143,7 +192,6 @@ async function Dashboard({ params, searchParams }: DashboardProps) {
143192
export async function generateMetadata({ params }: DashboardProps): Promise<Metadata> {
144193
const { organizationId } = organizationParamSchema.parse(params);
145194
const title = await getOrganizationTitle(organizationId);
146-
console.log('Organization title', title);
147195

148196
return {
149197
title: `Dashboard | ${title}`,

src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/projects/ProjectsWithPagination.tsx

+27-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,32 @@
11
import { Pagination } from "@/components/Pagination";
2-
import { getAllProjectsInOrganization, getProjects, getProjectsTotalCount } from "@/data/user/projects";
2+
import { getLoggedInUserOrganizationRole } from "@/data/user/organizations";
3+
import { getAllProjectsInOrganization, getProjects, getProjectsCountForUser, getProjectsForUser, getProjectsTotalCount } from "@/data/user/projects";
4+
import { serverGetLoggedInUser } from "@/utils/server/serverGetLoggedInUser";
35
import { projectsfilterSchema } from "@/utils/zod-schemas/params";
46
import { OrganizationProjectsTable } from "./OrganizationProjectsTable";
57

8+
export async function UserProjectsWithPagination({
9+
organizationId,
10+
searchParams,
11+
}: { organizationId: string; searchParams: unknown }) {
12+
const filters = projectsfilterSchema.parse(searchParams);
13+
const [{ id: userId }, userRole] = await Promise.all([
14+
serverGetLoggedInUser(),
15+
getLoggedInUserOrganizationRole(organizationId)
16+
]);
17+
const [projects, totalPages] = await Promise.all([
18+
getProjectsForUser({ ...filters, organizationId, userRole, userId }),
19+
getProjectsCountForUser({ ...filters, organizationId, userId }),
20+
]);
21+
22+
return (
23+
<>
24+
<OrganizationProjectsTable projects={projects} />
25+
<Pagination totalPages={totalPages} />
26+
</>
27+
);
28+
}
29+
630
export async function AllProjectsTableWithPagination({
731
organizationId,
832
searchParams,
@@ -20,6 +44,8 @@ export async function AllProjectsTableWithPagination({
2044
</>
2145
);
2246
}
47+
48+
2349
export async function ProjectsTableWithPagination({
2450
organizationId,
2551
teamId,
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,25 @@
11
'use client';
2-
import { T } from '@/components/ui/Typography';
2+
33
import { Button } from '@/components/ui/button';
4+
import { Card, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
45
import {
56
Dialog,
67
DialogContent,
78
DialogDescription,
9+
DialogFooter,
810
DialogHeader,
911
DialogTitle,
1012
DialogTrigger,
1113
} from '@/components/ui/dialog';
14+
import { FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
1215
import { Input } from '@/components/ui/input';
1316
import { deleteOrganization } from '@/data/user/organizations';
1417
import { useSAToastMutation } from '@/hooks/useSAToastMutation';
1518
import { zodResolver } from '@hookform/resolvers/zod';
19+
import { motion } from 'framer-motion';
1620
import { useRouter } from 'next/navigation';
1721
import { useState } from 'react';
18-
import { useForm } from 'react-hook-form';
22+
import { FormProvider, useForm } from 'react-hook-form';
1923
import { toast } from 'sonner';
2024
import { z } from 'zod';
2125

@@ -30,6 +34,23 @@ export const DeleteOrganization = ({
3034
}: DeleteOrganizationProps) => {
3135
const [open, setOpen] = useState(false);
3236
const router = useRouter();
37+
38+
const formSchema = z.object({
39+
organizationTitle: z
40+
.string()
41+
.refine(
42+
(v) => v === `delete ${organizationTitle}`,
43+
`Must match "delete ${organizationTitle}"`,
44+
),
45+
});
46+
47+
const form = useForm<z.infer<typeof formSchema>>({
48+
resolver: zodResolver(formSchema),
49+
defaultValues: {
50+
organizationTitle: '',
51+
},
52+
});
53+
3354
const { mutate, isLoading } = useSAToastMutation(
3455
async () => deleteOrganization(organizationId),
3556
{
@@ -53,78 +74,70 @@ export const DeleteOrganization = ({
5374
},
5475
);
5576

56-
type inputs = {
57-
organizationTitle: string;
58-
};
59-
60-
const formSchema = z.object({
61-
organizationTitle: z
62-
.string()
63-
.refine(
64-
(v) => v === `delete ${organizationTitle}`,
65-
`Must match "delete ${organizationTitle}"`,
66-
),
67-
});
68-
69-
const {
70-
register,
71-
handleSubmit,
72-
formState: { errors, isValid },
73-
} = useForm<inputs>({
74-
resolver: zodResolver(formSchema),
75-
});
76-
77-
const onSubmit = () => {
77+
const onSubmit = (values: z.infer<typeof formSchema>) => {
7878
mutate();
79-
toast.success('Organization deleted');
80-
setOpen(false);
8179
};
8280

8381
return (
84-
<div className="space-y-4">
85-
<T.H3>Danger Zone</T.H3>
86-
<div>
87-
<T.P>Delete your organization</T.P>
88-
<T.Subtle>
89-
Once you delete an organization, there is no going back. Please be
90-
certain.
91-
</T.Subtle>
92-
</div>
93-
94-
<Dialog open={open} onOpenChange={setOpen}>
95-
<DialogTrigger asChild>
96-
<Button variant={'destructive'}>Delete Organization</Button>
97-
</DialogTrigger>
98-
99-
<DialogContent>
100-
<DialogHeader>
101-
<DialogTitle>Delete Organization</DialogTitle>
102-
<DialogDescription>
103-
Type <strong> "delete {organizationTitle}" </strong>to confirm.
104-
</DialogDescription>
105-
</DialogHeader>
106-
<form
107-
className="flex flex-col gap-4"
108-
onSubmit={handleSubmit(onSubmit)}
109-
>
110-
<Input type="text" {...register('organizationTitle')} />
111-
{errors.organizationTitle && (
112-
<p className="text-red-400 text-sm font-bold">
113-
{errors.organizationTitle.message}
114-
</p>
115-
)}
116-
117-
<Button
118-
disabled={isLoading || !isValid}
119-
type="submit"
120-
variant="destructive"
121-
className="w-fit self-end"
122-
>
123-
{isLoading ? 'Deleting...' : 'Delete'} Organization
124-
</Button>
125-
</form>
126-
</DialogContent>
127-
</Dialog>
128-
</div>
82+
<motion.div
83+
initial={{ opacity: 0, y: 20 }}
84+
animate={{ opacity: 1, y: 0 }}
85+
transition={{ duration: 0.3 }}
86+
>
87+
<Card className="w-full max-w-4xl border-destructive/50 bg-destructive/5">
88+
<CardHeader>
89+
<CardTitle>
90+
Danger Zone
91+
</CardTitle>
92+
<CardDescription>
93+
Once you delete an organization, there is no going back. This action will permanently remove all associated data and cannot be undone.
94+
</CardDescription>
95+
</CardHeader>
96+
<CardFooter className="flex justify-start">
97+
<Dialog open={open} onOpenChange={setOpen}>
98+
<DialogTrigger asChild>
99+
<Button variant="destructive">Delete Organization</Button>
100+
</DialogTrigger>
101+
<DialogContent>
102+
<DialogHeader>
103+
<DialogTitle>Delete Organization</DialogTitle>
104+
<DialogDescription>
105+
Type <strong>"delete {organizationTitle}"</strong> to confirm.
106+
</DialogDescription>
107+
</DialogHeader>
108+
<FormProvider {...form}>
109+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
110+
<FormField
111+
control={form.control}
112+
name="organizationTitle"
113+
render={({ field }) => (
114+
<FormItem>
115+
<FormLabel>Confirmation</FormLabel>
116+
<FormControl>
117+
<Input {...field} />
118+
</FormControl>
119+
<FormMessage />
120+
</FormItem>
121+
)}
122+
/>
123+
<DialogFooter>
124+
<Button type="button" variant="outline" onClick={() => setOpen(false)}>
125+
Cancel
126+
</Button>
127+
<Button
128+
type="submit"
129+
variant="destructive"
130+
disabled={isLoading || !form.formState.isValid}
131+
>
132+
{isLoading ? 'Deleting...' : 'Delete'} Organization
133+
</Button>
134+
</DialogFooter>
135+
</form>
136+
</FormProvider>
137+
</DialogContent>
138+
</Dialog>
139+
</CardFooter>
140+
</Card>
141+
</motion.div>
129142
);
130-
};
143+
};

0 commit comments

Comments
 (0)