refactoring and more work with clerk

This commit is contained in:
Carl-Gerhard Lindesvärd
2024-02-07 23:28:55 +01:00
parent a9cbff2306
commit 86d2d0750f
61 changed files with 703 additions and 727 deletions

View File

@@ -0,0 +1,37 @@
'use client';
import { StickyBelowHeader } from '@/app/(app)/[organizationId]/[projectId]/layout-sticky-below-header';
import { columns } from '@/components/clients/table';
import { DataTable } from '@/components/DataTable';
import { Button } from '@/components/ui/button';
import { useAppParams } from '@/hooks/useAppParams';
import { pushModal } from '@/modals';
import type { getClientsByOrganizationId } from '@/server/services/clients.service';
import { PlusIcon } from 'lucide-react';
interface ListClientsProps {
clients: Awaited<ReturnType<typeof getClientsByOrganizationId>>;
}
export default function ListClients({ clients }: ListClientsProps) {
const organizationId = useAppParams().organizationId;
return (
<>
<StickyBelowHeader>
<div className="p-4 flex items-center justify-between">
<div />
<Button
icon={PlusIcon}
onClick={() => pushModal('AddClient', { organizationId })}
>
<span className="max-sm:hidden">Create client</span>
<span className="sm:hidden">Client</span>
</Button>
</div>
</StickyBelowHeader>
<div className="p-4">
<DataTable data={clients} columns={columns} />
</div>
</>
);
}

View File

@@ -0,0 +1,22 @@
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
import { getExists } from '@/server/pageExists';
import { getClientsByOrganizationId } from '@/server/services/clients.service';
import ListClients from './list-clients';
interface PageProps {
params: {
organizationId: string;
};
}
export default async function Page({ params: { organizationId } }: PageProps) {
await getExists(organizationId);
const clients = await getClientsByOrganizationId(organizationId);
return (
<PageLayout title="Clients" organizationSlug={organizationId}>
<ListClients clients={clients} />
</PageLayout>
);
}

View File

@@ -0,0 +1,66 @@
'use client';
import { api, handleError } from '@/app/_trpc/client';
import { InputWithLabel } from '@/components/forms/InputWithLabel';
import { Button } from '@/components/ui/button';
import { toast } from '@/components/ui/use-toast';
import { Widget, WidgetBody, WidgetHead } from '@/components/Widget';
import type { getOrganizationBySlug } from '@/server/services/organization.service';
import { useRouter } from 'next/navigation';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
const validator = z.object({
id: z.string().min(2),
name: z.string().min(2),
});
type IForm = z.infer<typeof validator>;
interface EditOrganizationProps {
organization: Awaited<ReturnType<typeof getOrganizationBySlug>>;
}
export default function EditOrganization({
organization,
}: EditOrganizationProps) {
const router = useRouter();
const { register, handleSubmit, formState, reset } = useForm<IForm>({
defaultValues: organization,
});
const mutation = api.organization.update.useMutation({
onSuccess(res) {
toast({
title: 'Organization updated',
description: 'Your organization has been updated.',
});
reset(res);
router.refresh();
},
onError: handleError,
});
return (
<form
onSubmit={handleSubmit((values) => {
mutation.mutate(values);
})}
>
<Widget>
<WidgetHead className="flex items-center justify-between">
<span className="title">Org. details</span>
<Button size="sm" type="submit" disabled={!formState.isDirty}>
Save
</Button>
</WidgetHead>
<WidgetBody>
<InputWithLabel
label="Name"
{...register('name')}
defaultValue={organization.name}
/>
</WidgetBody>
</Widget>
</form>
);
}

View File

@@ -0,0 +1,60 @@
import { api } from '@/app/_trpc/client';
import { InputWithLabel } from '@/components/forms/InputWithLabel';
import { Button } from '@/components/ui/button';
import { toast } from '@/components/ui/use-toast';
import { useAppParams } from '@/hooks/useAppParams';
import { zInviteUser } from '@/utils/validation';
import { zodResolver } from '@hookform/resolvers/zod';
import { SendIcon } from 'lucide-react';
import { useRouter } from 'next/navigation';
import { useForm } from 'react-hook-form';
import type { z } from 'zod';
type IForm = z.infer<typeof zInviteUser>;
export function InviteUser() {
const router = useRouter();
const { organizationId: organizationSlug } = useAppParams();
const { register, handleSubmit, formState, reset } = useForm<IForm>({
resolver: zodResolver(zInviteUser),
defaultValues: {
organizationSlug,
email: '',
role: 'org:member',
},
});
const mutation = api.organization.inviteUser.useMutation({
onSuccess() {
toast({
title: 'User invited!',
description: 'The user has been invited to the organization.',
});
reset();
router.refresh();
},
});
return (
<form
onSubmit={handleSubmit((values) => mutation.mutate(values))}
className="flex items-end gap-4"
>
<InputWithLabel
className="w-full max-w-sm"
label="Email"
placeholder="Who do you want to invite?"
{...register('email')}
/>
<Button
icon={SendIcon}
type="submit"
disabled={!formState.isDirty}
loading={mutation.isLoading}
>
Invite user
</Button>
</form>
);
}

View File

@@ -0,0 +1,64 @@
'use client';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { Widget, WidgetBody, WidgetHead } from '@/components/Widget';
import type { IServiceInvites } from '@/server/services/organization.service';
import { InviteUser } from './invite-user';
interface InvitedUsersProps {
invites: IServiceInvites;
}
export default function InvitedUsers({ invites }: InvitedUsersProps) {
return (
<Widget>
<WidgetHead className="flex items-center justify-between">
<span className="title">Invites</span>
</WidgetHead>
<WidgetBody>
<InviteUser />
<div className="font-medium mt-8 mb-2">Invited users</div>
<Table className="mini">
<TableHeader>
<TableRow>
<TableHead>Email</TableHead>
<TableHead>Role</TableHead>
<TableHead>Status</TableHead>
<TableHead>Created</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{invites.map((item) => {
return (
<TableRow key={item.id}>
<TableCell className="font-medium">{item.email}</TableCell>
<TableCell>{item.role}</TableCell>
<TableCell>{item.status}</TableCell>
<TableCell>
{new Date(item.createdAt).toLocaleString()}
</TableCell>
</TableRow>
);
})}
{invites.length === 0 && (
<TableRow>
<TableCell colSpan={2} className="italic">
No invites
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</WidgetBody>
</Widget>
);
}

View File

@@ -0,0 +1,35 @@
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
import {
getInvites,
getOrganizationBySlug,
} from '@/server/services/organization.service';
import { clerkClient } from '@clerk/nextjs';
import { notFound } from 'next/navigation';
import EditOrganization from './edit-organization';
import InvitedUsers from './invited-users';
interface PageProps {
params: {
organizationId: string;
};
}
export default async function Page({ params: { organizationId } }: PageProps) {
const organization = await getOrganizationBySlug(organizationId);
if (!organization) {
return notFound();
}
const invites = await getInvites(organization.id);
return (
<PageLayout title={organization.name} organizationSlug={organizationId}>
<div className="p-4 grid grid-cols-1 gap-4">
<EditOrganization organization={organization} />
<InvitedUsers invites={invites} />
</div>
</PageLayout>
);
}

View File

@@ -0,0 +1 @@
export { default } from './organization/page';

View File

@@ -0,0 +1,85 @@
'use client';
import { api, handleError } from '@/app/_trpc/client';
import { InputWithLabel } from '@/components/forms/InputWithLabel';
import { Button } from '@/components/ui/button';
import { toast } from '@/components/ui/use-toast';
import { Widget, WidgetBody, WidgetHead } from '@/components/Widget';
import type { getUserById } from '@/server/services/user.service';
import { zodResolver } from '@hookform/resolvers/zod';
import { useRouter } from 'next/navigation';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
const validator = z.object({
firstName: z.string().min(2),
lastName: z.string().min(2),
email: z.string().email(),
});
type IForm = z.infer<typeof validator>;
interface EditProfileProps {
profile: Awaited<ReturnType<typeof getUserById>>;
}
export default function EditProfile({ profile }: EditProfileProps) {
const router = useRouter();
const { register, handleSubmit, reset, formState } = useForm<IForm>({
resolver: zodResolver(validator),
defaultValues: {
firstName: profile.firstName ?? '',
lastName: profile.lastName ?? '',
email: profile.email ?? '',
},
});
const mutation = api.user.update.useMutation({
onSuccess(res) {
toast({
title: 'Profile updated',
description: 'Your profile has been updated.',
});
reset(res);
router.refresh();
},
onError: handleError,
});
return (
<form
onSubmit={handleSubmit((values) => {
mutation.mutate(values);
})}
>
<Widget>
<WidgetHead className="flex items-center justify-between">
<span className="title">Your profile</span>
<Button size="sm" type="submit" disabled={!formState.isDirty}>
Save
</Button>
</WidgetHead>
<WidgetBody className="flex flex-col gap-4">
<InputWithLabel
label="First name"
placeholder="Your first name"
defaultValue={profile.firstName ?? ''}
{...register('firstName')}
/>
<InputWithLabel
label="Last name"
placeholder="Your last name"
defaultValue={profile.lastName ?? ''}
{...register('lastName')}
/>
<InputWithLabel
disabled
label="Email"
placeholder="Your email"
defaultValue={profile.email ?? ''}
{...register('email')}
/>
</WidgetBody>
</Widget>
</form>
);
}

View File

@@ -0,0 +1,20 @@
'use client';
import { Widget, WidgetBody, WidgetHead } from '@/components/Widget';
import { SignOutButton } from '@clerk/nextjs';
export function Logout() {
return (
<Widget className="border-destructive">
<WidgetHead className="border-destructive">
<span className="title text-destructive">Sad part</span>
</WidgetHead>
<WidgetBody>
<p className="mb-4">
Sometime&apos;s you need to go. See you next time
</p>
<SignOutButton />
</WidgetBody>
</Widget>
);
}

View File

@@ -0,0 +1,27 @@
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
import { getExists } from '@/server/pageExists';
import { getUserById } from '@/server/services/user.service';
import { auth } from '@clerk/nextjs';
import EditProfile from './edit-profile';
import { Logout } from './logout';
interface PageProps {
params: {
organizationId: string;
};
}
export default async function Page({ params: { organizationId } }: PageProps) {
const { userId } = auth();
await getExists(organizationId);
const profile = await getUserById(userId!);
return (
<PageLayout title={profile.lastName} organizationSlug={organizationId}>
<div className="p-4 flex flex-col gap-4">
<EditProfile profile={profile} />
<Logout />
</div>
</PageLayout>
);
}

View File

@@ -0,0 +1,40 @@
'use client';
import { StickyBelowHeader } from '@/app/(app)/[organizationId]/[projectId]/layout-sticky-below-header';
import { DataTable } from '@/components/DataTable';
import { columns } from '@/components/projects/table';
import { Button } from '@/components/ui/button';
import { useAppParams } from '@/hooks/useAppParams';
import { pushModal } from '@/modals';
import type { getProjectsByOrganizationId } from '@/server/services/project.service';
import { PlusIcon } from 'lucide-react';
interface ListProjectsProps {
projects: Awaited<ReturnType<typeof getProjectsByOrganizationId>>;
}
export default function ListProjects({ projects }: ListProjectsProps) {
const organizationId = useAppParams().organizationId;
return (
<>
<StickyBelowHeader>
<div className="p-4 flex items-center justify-between">
<div />
<Button
icon={PlusIcon}
onClick={() =>
pushModal('AddProject', {
organizationId,
})
}
>
<span className="max-sm:hidden">Create project</span>
<span className="sm:hidden">Project</span>
</Button>
</div>
</StickyBelowHeader>
<div className="p-4">
<DataTable data={projects} columns={columns} />
</div>
</>
);
}

View File

@@ -0,0 +1,22 @@
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
import { getExists } from '@/server/pageExists';
import { getProjectsByOrganizationSlug } from '@/server/services/project.service';
import ListProjects from './list-projects';
interface PageProps {
params: {
organizationId: string;
};
}
export default async function Page({ params: { organizationId } }: PageProps) {
await getExists(organizationId);
const projects = await getProjectsByOrganizationSlug(organizationId);
return (
<PageLayout title="Projects" organizationSlug={organizationId}>
<ListProjects projects={projects} />
</PageLayout>
);
}