Skip to content

Commit

Permalink
fixes / ui changes and added functions for teams
Browse files Browse the repository at this point in the history
  • Loading branch information
psiddharthdesign committed Aug 2, 2024
1 parent 814cefc commit 5cb8491
Show file tree
Hide file tree
Showing 26 changed files with 918 additions and 436 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,19 @@ import { Search } from "@/components/Search";
import { TeamsCardList } from "@/components/Teams/TeamsCardList";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { getOrganizationTitle } from "@/data/user/organizations";
import { getAllProjectsInOrganization } from "@/data/user/projects";
import { Separator } from "@/components/ui/separator";
import { T } from "@/components/ui/Typography";
import { getLoggedInUserOrganizationRole, getOrganizationTitle } from "@/data/user/organizations";
import { getAllProjectsInOrganization, getProjectsForUser } from "@/data/user/projects";
import { getSlimTeamById, getTeams } from "@/data/user/teams";
import { Tables } from "@/lib/database.types";
import { Enum } from "@/types";
import { serverGetLoggedInUser } from "@/utils/server/serverGetLoggedInUser";
import {
organizationParamSchema,
projectsfilterSchema
} from "@/utils/zod-schemas/params";
import { Layers, Plus } from "lucide-react";
import { Layers, Plus, Settings } from "lucide-react";
import type { Metadata } from 'next';
import Link from "next/link";
import { Suspense } from "react";
Expand All @@ -23,24 +28,46 @@ import TeamsLoadingFallback from "./TeamsLoadingFallback";
async function Projects({
organizationId,
filters,
userId,
userRole,
}: {
organizationId: string;
filters: z.infer<typeof projectsfilterSchema>;
userId: string;
userRole: Enum<'organization_member_role'>;
}) {
const projects = await getAllProjectsInOrganization({
organizationId,
...filters,
});
let projects: Tables<'projects'>[];

if (userRole === 'admin') {
projects = await getAllProjectsInOrganization({
organizationId,
...filters,
});
} else {
projects = await getProjectsForUser({
userId,
userRole,
organizationId,
...filters,
});
}

const projectWithTeamNames = await Promise.all(projects.map(async (project) => {
if (project.team_id) {
const team = await getSlimTeamById(project.team_id);
const projectWithTeamName = { ...project, teamName: team.name };
return projectWithTeamName;
try {
const team = await getSlimTeamById(project.team_id);
return { ...project, teamName: team?.name || 'Unknown Team' };
} catch (error) {
console.error(`Error fetching team for project ${project.id}:`, error);
return { ...project };
}
}
return project;
}));

return <ProjectsCardList projects={projectWithTeamNames} />;
}

async function Teams({
organizationId,
filters,
Expand All @@ -65,38 +92,60 @@ export type DashboardProps = {
async function Dashboard({ params, searchParams }: DashboardProps) {
const { organizationId } = organizationParamSchema.parse(params);
const validatedSearchParams = projectsfilterSchema.parse(searchParams);
const { id: userId } = await serverGetLoggedInUser();
const userRole = await getLoggedInUserOrganizationRole(organizationId);

return (
<DashboardClientWrapper>
<div className="flex justify-between gap-4 w-full">
<T.H2>Dashboard</T.H2>
<Link href={`/org/${organizationId}/settings`}>
<Button className="w-fit" variant="outline">
<Settings className="mr-2 h-4 w-4" />
Organization Settings
</Button>
</Link>
</div>
<Card >
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-6">
<CardTitle className="text-3xl font-bold tracking-tight">Dashboard</CardTitle>
<div className="flex space-x-4">
<Link href={`/org/${organizationId}/projects/create`}>
<Button variant="default" size="sm">
<Plus className="mr-2 h-4 w-4" />
Create Project
</Button>
</Link>
</div>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between mb-6">
<h2 className="text-2xl font-semibold tracking-tight">Recent Projects</h2>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle>
Recent Projects
</CardTitle>
<div className="flex items-center space-x-4">
<Search className="w-[200px]" placeholder="Search projects" />
<div className="flex items-center space-x-4">
<Search placeholder="Search projects" />
{/* <MultiSelect
options={userTeams.map(team => ({ label: team.name, value: team.id }))}
placeholder="Filter by teams"
/> */}
</div>
<Button variant="secondary" size="sm" asChild>
<Link href={`/org/${organizationId}/projects`}>
<Layers className="mr-2 h-4 w-4" />
View all projects
</Link>
</Button>
<Separator orientation="vertical" className="h-6" />
<div className="flex space-x-4">
<Link href={`/org/${organizationId}/projects/create`}>
<Button variant="default" size="sm">
<Plus className="mr-2 h-4 w-4" />
Create Project
</Button>
</Link>
</div>
</div>
</div>
</CardHeader>
<CardContent>

<Suspense fallback={<ProjectsLoadingFallback quantity={3} />}>
<Projects
organizationId={organizationId}
filters={validatedSearchParams}
userId={userId}
userRole={userRole}
/>
{validatedSearchParams.query && (
<p className="mt-4 text-sm text-muted-foreground">
Expand Down Expand Up @@ -143,7 +192,6 @@ async function Dashboard({ params, searchParams }: DashboardProps) {
export async function generateMetadata({ params }: DashboardProps): Promise<Metadata> {
const { organizationId } = organizationParamSchema.parse(params);
const title = await getOrganizationTitle(organizationId);
console.log('Organization title', title);

return {
title: `Dashboard | ${title}`,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,32 @@
import { Pagination } from "@/components/Pagination";
import { getAllProjectsInOrganization, getProjects, getProjectsTotalCount } from "@/data/user/projects";
import { getLoggedInUserOrganizationRole } from "@/data/user/organizations";
import { getAllProjectsInOrganization, getProjects, getProjectsCountForUser, getProjectsForUser, getProjectsTotalCount } from "@/data/user/projects";
import { serverGetLoggedInUser } from "@/utils/server/serverGetLoggedInUser";
import { projectsfilterSchema } from "@/utils/zod-schemas/params";
import { OrganizationProjectsTable } from "./OrganizationProjectsTable";

export async function UserProjectsWithPagination({
organizationId,
searchParams,
}: { organizationId: string; searchParams: unknown }) {
const filters = projectsfilterSchema.parse(searchParams);
const [{ id: userId }, userRole] = await Promise.all([
serverGetLoggedInUser(),
getLoggedInUserOrganizationRole(organizationId)
]);
const [projects, totalPages] = await Promise.all([
getProjectsForUser({ ...filters, organizationId, userRole, userId }),
getProjectsCountForUser({ ...filters, organizationId, userId }),
]);

return (
<>
<OrganizationProjectsTable projects={projects} />
<Pagination totalPages={totalPages} />
</>
);
}

export async function AllProjectsTableWithPagination({
organizationId,
searchParams,
Expand All @@ -20,6 +44,8 @@ export async function AllProjectsTableWithPagination({
</>
);
}


export async function ProjectsTableWithPagination({
organizationId,
teamId,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
'use client';
import { T } from '@/components/ui/Typography';

import { Button } from '@/components/ui/button';
import { Card, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog';
import { FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { deleteOrganization } from '@/data/user/organizations';
import { useSAToastMutation } from '@/hooks/useSAToastMutation';
import { zodResolver } from '@hookform/resolvers/zod';
import { motion } from 'framer-motion';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'sonner';
import { z } from 'zod';

Expand All @@ -30,6 +34,23 @@ export const DeleteOrganization = ({
}: DeleteOrganizationProps) => {
const [open, setOpen] = useState(false);
const router = useRouter();

const formSchema = z.object({
organizationTitle: z
.string()
.refine(
(v) => v === `delete ${organizationTitle}`,
`Must match "delete ${organizationTitle}"`,
),
});

const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
organizationTitle: '',
},
});

const { mutate, isLoading } = useSAToastMutation(
async () => deleteOrganization(organizationId),
{
Expand All @@ -53,78 +74,70 @@ export const DeleteOrganization = ({
},
);

type inputs = {
organizationTitle: string;
};

const formSchema = z.object({
organizationTitle: z
.string()
.refine(
(v) => v === `delete ${organizationTitle}`,
`Must match "delete ${organizationTitle}"`,
),
});

const {
register,
handleSubmit,
formState: { errors, isValid },
} = useForm<inputs>({
resolver: zodResolver(formSchema),
});

const onSubmit = () => {
const onSubmit = (values: z.infer<typeof formSchema>) => {
mutate();
toast.success('Organization deleted');
setOpen(false);
};

return (
<div className="space-y-4">
<T.H3>Danger Zone</T.H3>
<div>
<T.P>Delete your organization</T.P>
<T.Subtle>
Once you delete an organization, there is no going back. Please be
certain.
</T.Subtle>
</div>

<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button variant={'destructive'}>Delete Organization</Button>
</DialogTrigger>

<DialogContent>
<DialogHeader>
<DialogTitle>Delete Organization</DialogTitle>
<DialogDescription>
Type <strong> "delete {organizationTitle}" </strong>to confirm.
</DialogDescription>
</DialogHeader>
<form
className="flex flex-col gap-4"
onSubmit={handleSubmit(onSubmit)}
>
<Input type="text" {...register('organizationTitle')} />
{errors.organizationTitle && (
<p className="text-red-400 text-sm font-bold">
{errors.organizationTitle.message}
</p>
)}

<Button
disabled={isLoading || !isValid}
type="submit"
variant="destructive"
className="w-fit self-end"
>
{isLoading ? 'Deleting...' : 'Delete'} Organization
</Button>
</form>
</DialogContent>
</Dialog>
</div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
>
<Card className="w-full max-w-4xl border-destructive/50 bg-destructive/5">
<CardHeader>
<CardTitle>
Danger Zone
</CardTitle>
<CardDescription>
Once you delete an organization, there is no going back. This action will permanently remove all associated data and cannot be undone.
</CardDescription>
</CardHeader>
<CardFooter className="flex justify-start">
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button variant="destructive">Delete Organization</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Delete Organization</DialogTitle>
<DialogDescription>
Type <strong>"delete {organizationTitle}"</strong> to confirm.
</DialogDescription>
</DialogHeader>
<FormProvider {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="organizationTitle"
render={({ field }) => (
<FormItem>
<FormLabel>Confirmation</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => setOpen(false)}>
Cancel
</Button>
<Button
type="submit"
variant="destructive"
disabled={isLoading || !form.formState.isValid}
>
{isLoading ? 'Deleting...' : 'Delete'} Organization
</Button>
</DialogFooter>
</form>
</FormProvider>
</DialogContent>
</Dialog>
</CardFooter>
</Card>
</motion.div>
);
};
};
Loading

0 comments on commit 5cb8491

Please sign in to comment.