Update admin and marketing layouts, add new admin components

Refined both admin and marketing layouts for a clearer design. Newly added components for the admin page include admin-account-page, admin-members-table and admin-memberships-table. Also included in this update are route renaming, minor text edits and corrections in the code.
This commit is contained in:
giancarlo
2024-04-08 20:00:52 +08:00
parent 767e2f21b5
commit 13308194ec
18 changed files with 426 additions and 103 deletions

View File

@@ -0,0 +1,104 @@
import { Database } from '@kit/supabase/database';
import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client';
import { Heading } from '@kit/ui/heading';
import { PageBody, PageHeader } from '@kit/ui/page';
import { AdminMembersTable } from './admin-members-table';
import { AdminMembershipsTable } from './admin-memberships-table';
type Db = Database['public']['Tables'];
type Account = Db['accounts']['Row'];
type Membership = Db['accounts_memberships']['Row'];
export function AdminAccountPage(props: {
account: Account & { memberships: Membership[] };
}) {
if (props.account.is_personal_account) {
return <PersonalAccountPage account={props.account} />;
}
return <TeamAccountPage account={props.account} />;
}
async function PersonalAccountPage(props: { account: Account }) {
const memberships = await getMemberships(props.account.id);
return (
<>
<PageHeader
title={props.account.name}
description={`Manage ${props.account.name}'s account details and settings.`}
/>
<PageBody>
<div className={'divider-divider-x flex flex-col space-y-4'}>
<Heading level={4}>Memberships</Heading>
<div>
<AdminMembershipsTable memberships={memberships} />
</div>
</div>
</PageBody>
</>
);
}
async function TeamAccountPage(props: {
account: Account & { memberships: Membership[] };
}) {
const members = await getMembers(props.account.slug ?? '');
return (
<>
<PageHeader
title={props.account.name}
description={`Manage ${props.account.name}'s account details and settings.`}
/>
<PageBody>
<AdminMembersTable members={members} />
</PageBody>
</>
);
}
async function getMemberships(userId: string) {
const client = getSupabaseServerComponentClient({
admin: true,
});
const memberships = await client
.from('accounts_memberships')
.select<
string,
Membership & {
account: {
id: string;
name: string;
};
}
>('*, account: account_id !inner (id, name)')
.eq('user_id', userId);
if (memberships.error) {
throw memberships.error;
}
return memberships.data;
}
async function getMembers(accountSlug: string) {
const client = getSupabaseServerComponentClient({
admin: true,
});
const members = await client.rpc('get_account_members', {
account_slug: accountSlug,
});
if (members.error) {
throw members.error;
}
return members.data;
}

View File

@@ -20,20 +20,27 @@ import {
DropdownMenuTrigger,
} from '@kit/ui/dropdown-menu';
import { DataTable } from '@kit/ui/enhanced-data-table';
import { Form, FormControl, FormField, FormItem } from '@kit/ui/form';
import { If } from '@kit/ui/if';
import { Input } from '@kit/ui/input';
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from '@kit/ui/select';
type Account = Database['public']['Tables']['accounts']['Row'];
const FiltersSchema = z.object({
type: z.enum(['all', 'team', 'personal']),
query: z.string().optional(),
});
export function AccountsTable(
export function AdminAccountsTable(
props: React.PropsWithChildren<{
data: Account[];
pageCount: number;
@@ -66,6 +73,7 @@ function AccountsTableFilters(props: {
resolver: zodResolver(FiltersSchema),
defaultValues: {
type: props.filters?.type ?? 'all',
query: '',
},
mode: 'onChange',
reValidateMode: 'onChange',
@@ -74,9 +82,10 @@ function AccountsTableFilters(props: {
const router = useRouter();
const pathName = usePathname();
const onSubmit = ({ type }: z.infer<typeof FiltersSchema>) => {
const onSubmit = ({ type, query }: z.infer<typeof FiltersSchema>) => {
const params = new URLSearchParams({
account_type: type,
query: query ?? '',
});
const url = `${pathName}?${params.toString()}`;
@@ -85,35 +94,59 @@ function AccountsTableFilters(props: {
};
return (
<div className={'flex space-x-4'}>
<form onSubmit={form.handleSubmit((data) => onSubmit(data))}>
<Select
value={form.watch('type')}
onValueChange={(value) => {
form.setValue(
'type',
value as z.infer<typeof FiltersSchema>['type'],
{
shouldValidate: true,
shouldDirty: true,
shouldTouch: true,
},
);
return onSubmit(form.getValues());
}}
<div className={'flex justify-end space-x-4'}>
<Form {...form}>
<form
className={'flex space-x-4'}
onSubmit={form.handleSubmit((data) => onSubmit(data))}
>
<SelectTrigger>
<span>Account Type</span>
</SelectTrigger>
<Select
value={form.watch('type')}
onValueChange={(value) => {
form.setValue(
'type',
value as z.infer<typeof FiltersSchema>['type'],
{
shouldValidate: true,
shouldDirty: true,
shouldTouch: true,
},
);
<SelectContent>
<SelectItem value={'all'}>All</SelectItem>
<SelectItem value={'team'}>Team</SelectItem>
<SelectItem value={'personal'}>Personal</SelectItem>
</SelectContent>
</Select>
</form>
return onSubmit(form.getValues());
}}
>
<SelectTrigger>
<SelectValue placeholder={'Account Type'} />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Account Type</SelectLabel>
<SelectItem value={'all'}>All accounts</SelectItem>
<SelectItem value={'team'}>Team</SelectItem>
<SelectItem value={'personal'}>Personal</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<FormField
name={'query'}
render={({ field }) => (
<FormItem>
<FormControl className={'min-w-72'}>
<Input
className={'w-full'}
placeholder={`Search account...`}
{...field}
/>
</FormControl>
</FormItem>
)}
/>
</form>
</Form>
</div>
);
}
@@ -123,7 +156,16 @@ function getColumns(): ColumnDef<Account>[] {
{
id: 'name',
header: 'Name',
accessorKey: 'name',
cell: ({ row }) => {
return (
<Link
className={'hover:underline'}
href={`/admin/accounts/${row.original.id}`}
>
{row.original.name}
</Link>
);
},
},
{
id: 'email',
@@ -151,6 +193,8 @@ function getColumns(): ColumnDef<Account>[] {
id: 'actions',
header: '',
cell: ({ row }) => {
const isPersonalAccount = row.original.is_personal_account;
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
@@ -164,10 +208,18 @@ function getColumns(): ColumnDef<Account>[] {
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem>
<Link href={`/accounts/${row.original.id}`}>View</Link>
<Link href={`/admin/accounts/${row.original.id}`}>View</Link>
</DropdownMenuItem>
<DropdownMenuItem>Delete</DropdownMenuItem>
<If condition={isPersonalAccount}>
<DropdownMenuItem className={'text-orange-800'}>
Ban
</DropdownMenuItem>
</If>
<DropdownMenuItem className={'text-destructive'}>
Delete
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>

View File

@@ -70,7 +70,7 @@ export async function AdminDashboard() {
<CardTitle>Trials</CardTitle>
<CardDescription>
Th number of trial subscriptions currently active.
The number of trial subscriptions currently active.
</CardDescription>
</CardHeader>

View File

@@ -0,0 +1,67 @@
'use client';
import Link from 'next/link';
import { ColumnDef } from '@tanstack/react-table';
import { Database } from '@kit/supabase/database';
import { DataTable } from '@kit/ui/enhanced-data-table';
import { ProfileAvatar } from '@kit/ui/profile-avatar';
type Memberships =
Database['public']['Functions']['get_account_members']['Returns'][number];
export function AdminMembersTable(props: { members: Memberships[] }) {
return <DataTable data={props.members} columns={getColumns()} />;
}
function getColumns(): ColumnDef<Memberships>[] {
return [
{
header: 'User ID',
accessorKey: 'user_id',
},
{
header: 'Name',
cell: ({ row }) => {
const name = row.original.name ?? row.original.email;
return (
<div className={'flex items-center space-x-2'}>
<div>
<ProfileAvatar
pictureUrl={row.original.picture_url}
displayName={name}
/>
</div>
<Link
className={'hover:underline'}
href={`/admin/accounts/${row.original.id}`}
>
<span>{name}</span>
</Link>
</div>
);
},
},
{
header: 'Email',
accessorKey: 'email',
},
{
header: 'Role',
cell: ({ row }) => {
return row.original.role;
},
},
{
header: 'Created At',
accessorKey: 'created_at',
},
{
header: 'Updated At',
accessorKey: 'updated_at',
},
];
}

View File

@@ -0,0 +1,54 @@
'use client';
import Link from 'next/link';
import { ColumnDef } from '@tanstack/react-table';
import { Database } from '@kit/supabase/database';
import { DataTable } from '@kit/ui/enhanced-data-table';
type Membership =
Database['public']['Tables']['accounts_memberships']['Row'] & {
account: {
id: string;
name: string;
};
};
export function AdminMembershipsTable(props: { memberships: Membership[] }) {
return <DataTable data={props.memberships} columns={getColumns()} />;
}
function getColumns(): ColumnDef<Membership>[] {
return [
{
header: 'User ID',
accessorKey: 'user_id',
},
{
header: 'Team',
cell: ({ row }) => {
return (
<Link
className={'hover:underline'}
href={`/admin/accounts/${row.original.account_id}`}
>
{row.original.account.name}
</Link>
);
},
},
{
header: 'Role',
accessorKey: 'account_role',
},
{
header: 'Created At',
accessorKey: 'created_at',
},
{
header: 'Updated At',
accessorKey: 'updated_at',
},
];
}