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:
104
packages/features/admin/src/components/admin-account-page.tsx
Normal file
104
packages/features/admin/src/components/admin-account-page.tsx
Normal 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;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
];
|
||||
}
|
||||
Reference in New Issue
Block a user