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:
@@ -46,7 +46,9 @@ export function SiteNavigation() {
|
|||||||
<div className={'hidden items-center lg:flex'}>
|
<div className={'hidden items-center lg:flex'}>
|
||||||
<NavigationMenu>
|
<NavigationMenu>
|
||||||
<NavigationMenuList
|
<NavigationMenuList
|
||||||
className={'space-x-1.5 rounded-full border px-1 py-2'}
|
className={
|
||||||
|
'space-x-1.5 rounded-full px-1 py-2 shadow-sm dark:shadow-primary/20'
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{NavItems}
|
{NavItems}
|
||||||
</NavigationMenuList>
|
</NavigationMenuList>
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { Heading } from '@kit/ui/heading';
|
|
||||||
import { cn } from '@kit/ui/utils';
|
import { cn } from '@kit/ui/utils';
|
||||||
|
|
||||||
export function SitePageHeader(props: {
|
export function SitePageHeader(props: {
|
||||||
@@ -8,9 +7,11 @@ export function SitePageHeader(props: {
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn('flex flex-col items-center space-y-2.5', props.className)}
|
className={cn('flex flex-col items-center space-y-4', props.className)}
|
||||||
>
|
>
|
||||||
<Heading level={1}>{props.title}</Heading>
|
<h1 className={'text-center text-3xl font-semibold xl:text-4xl'}>
|
||||||
|
{props.title}
|
||||||
|
</h1>
|
||||||
|
|
||||||
<h2 className={'text-center text-xl text-muted-foreground xl:text-2xl'}>
|
<h2 className={'text-center text-xl text-muted-foreground xl:text-2xl'}>
|
||||||
<span className={'font-normal'}>{props.subtitle}</span>
|
<span className={'font-normal'}>{props.subtitle}</span>
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ async function BlogPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'container mx-auto'}>
|
<div className={'container mx-auto'}>
|
||||||
<div className={'flex flex-col space-y-16'}>
|
<div className={'flex flex-col space-y-12 xl:space-y-24'}>
|
||||||
<SitePageHeader
|
<SitePageHeader
|
||||||
title={t('marketing:blog')}
|
title={t('marketing:blog')}
|
||||||
subtitle={t('marketing:blogSubtitle')}
|
subtitle={t('marketing:blogSubtitle')}
|
||||||
|
|||||||
@@ -26,18 +26,16 @@ async function DocsPage() {
|
|||||||
const cards = docs.filter((item) => !item.parentId);
|
const cards = docs.filter((item) => !item.parentId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'flex flex-1 flex-col'}>
|
<PageBody>
|
||||||
<PageBody>
|
<div className={'flex flex-col items-center space-y-12 xl:space-y-24'}>
|
||||||
<div className={'flex flex-col items-center space-y-16'}>
|
<SitePageHeader
|
||||||
<SitePageHeader
|
title={t('marketing:documentation')}
|
||||||
title={t('marketing:documentation')}
|
subtitle={t('marketing:documentationSubtitle')}
|
||||||
subtitle={t('marketing:documentationSubtitle')}
|
/>
|
||||||
/>
|
|
||||||
|
|
||||||
<DocsCards cards={cards} />
|
<DocsCards cards={cards} />
|
||||||
</div>
|
</div>
|
||||||
</PageBody>
|
</PageBody>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ async function FAQPage() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div className={'container mx-auto'}>
|
<div className={'container mx-auto'}>
|
||||||
<div className={'flex flex-col space-y-16'}>
|
<div className={'flex flex-col space-y-12 xl:space-y-24'}>
|
||||||
<SitePageHeader
|
<SitePageHeader
|
||||||
title={t('marketing:faq')}
|
title={t('marketing:faq')}
|
||||||
subtitle={t('marketing:faqSubtitle')}
|
subtitle={t('marketing:faqSubtitle')}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ async function SiteLayout(props: React.PropsWithChildren) {
|
|||||||
const user = await getUser();
|
const user = await getUser();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'flex flex-col space-y-8'}>
|
<div className={'flex flex-col space-y-6 xl:space-y-10 2xl:space-y-12'}>
|
||||||
<SiteHeader user={user} />
|
<SiteHeader user={user} />
|
||||||
|
|
||||||
{props.children}
|
{props.children}
|
||||||
|
|||||||
@@ -13,57 +13,50 @@ import { withI18n } from '~/lib/i18n/with-i18n';
|
|||||||
|
|
||||||
function Home() {
|
function Home() {
|
||||||
return (
|
return (
|
||||||
<div className={'flex flex-col space-y-16'}>
|
<div className={'flex flex-col space-y-24'}>
|
||||||
<div className={'container mx-auto'}>
|
<div className={'container mx-auto flex flex-col space-y-24'}>
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
'mt-4 flex flex-col items-center md:flex-row xl:mt-12' +
|
'flex flex-col items-center md:flex-row' +
|
||||||
' mx-auto flex-1 justify-center animate-in fade-in ' +
|
' mx-auto flex-1 justify-center animate-in fade-in ' +
|
||||||
' duration-1000 slide-in-from-top-12'
|
' duration-1000 slide-in-from-top-12'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className={'flex w-full flex-1 flex-col items-center space-y-8'}>
|
<div
|
||||||
|
className={'flex w-full flex-1 flex-col items-center space-y-12'}
|
||||||
|
>
|
||||||
<Pill>
|
<Pill>
|
||||||
<span>The leading SaaS Starter Kit for ambitious developers</span>
|
<span>The leading SaaS Starter Kit for ambitious developers</span>
|
||||||
</Pill>
|
</Pill>
|
||||||
|
|
||||||
<HeroTitle>
|
<div className={'flex flex-col items-center space-y-8'}>
|
||||||
<span>The SaaS Starter Kit</span>
|
<HeroTitle>
|
||||||
|
<span>The SaaS Starter Kit</span>
|
||||||
|
|
||||||
<span>straight from the future</span>
|
<span>without compromises</span>
|
||||||
</HeroTitle>
|
</HeroTitle>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Heading
|
<Heading
|
||||||
level={3}
|
level={3}
|
||||||
className={'text-center font-medium text-muted-foreground'}
|
className={'text-center font-medium text-muted-foreground'}
|
||||||
>
|
>
|
||||||
<span>Here you can write a short description of your SaaS</span>
|
<span>Build and launch a SaaS in days, not months</span>
|
||||||
</Heading>
|
</Heading>
|
||||||
|
|
||||||
<Heading
|
<Heading
|
||||||
level={3}
|
level={3}
|
||||||
className={'text-center font-medium text-muted-foreground'}
|
className={'text-center font-medium text-muted-foreground'}
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
This subheading is usually laid out on multiple lines
|
<span>Focus on your business, not on the tech</span>
|
||||||
</span>
|
</span>
|
||||||
</Heading>
|
</Heading>
|
||||||
|
</div>
|
||||||
<Heading
|
|
||||||
level={3}
|
|
||||||
className={'text-center font-medium text-muted-foreground'}
|
|
||||||
>
|
|
||||||
<span>Impress your customers, straight to the point.</span>
|
|
||||||
</Heading>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={'flex flex-col items-center space-y-4'}>
|
<div>
|
||||||
<MainCallToActionButton />
|
<MainCallToActionButton />
|
||||||
|
|
||||||
<span className={'text-xs text-muted-foreground'}>
|
|
||||||
Free plan. No credit card required.
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -222,8 +215,7 @@ function HeroTitle({ children }: React.PropsWithChildren) {
|
|||||||
return (
|
return (
|
||||||
<h1
|
<h1
|
||||||
className={
|
className={
|
||||||
'text-center font-sans text-4xl md:text-5xl' +
|
'flex flex-col text-center text-5xl font-bold xl:text-6xl 2xl:text-7xl'
|
||||||
' flex flex-col font-bold xl:text-7xl 2xl:text-[5rem]'
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
@@ -233,7 +225,7 @@ function HeroTitle({ children }: React.PropsWithChildren) {
|
|||||||
|
|
||||||
function Pill(props: React.PropsWithChildren) {
|
function Pill(props: React.PropsWithChildren) {
|
||||||
return (
|
return (
|
||||||
<h2 className={'rounded-full px-4 py-2 text-sm shadow'}>
|
<h2 className={'rounded-full border-primary px-4 py-2 text-sm shadow'}>
|
||||||
{props.children}
|
{props.children}
|
||||||
</h2>
|
</h2>
|
||||||
);
|
);
|
||||||
@@ -266,14 +258,14 @@ function RightFeatureContainer(props: React.PropsWithChildren) {
|
|||||||
|
|
||||||
function MainCallToActionButton() {
|
function MainCallToActionButton() {
|
||||||
return (
|
return (
|
||||||
<Button className={'rounded-full'}>
|
<Button className={'h-12 rounded-full px-5 text-[1em]'}>
|
||||||
<Link href={'/auth/sign-up'}>
|
<Link href={'/auth/sign-up'}>
|
||||||
<span className={'flex items-center space-x-0.5'}>
|
<span className={'flex items-center space-x-0.5'}>
|
||||||
<span>Get Started</span>
|
<span>Get Started</span>
|
||||||
|
|
||||||
<ChevronRight
|
<ChevronRight
|
||||||
className={
|
className={
|
||||||
'h-4 animate-in fade-in slide-in-from-left-8' +
|
'h-5 animate-in fade-in slide-in-from-left-8' +
|
||||||
' delay-1000 duration-1000 zoom-in fill-mode-both'
|
' delay-1000 duration-1000 zoom-in fill-mode-both'
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ async function PricingPage() {
|
|||||||
const { t } = await createI18nServerInstance();
|
const { t } = await createI18nServerInstance();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'container mx-auto mt-8 flex flex-col space-y-12'}>
|
<div className={'container mx-auto mt-8 flex flex-col space-y-24'}>
|
||||||
<SitePageHeader
|
<SitePageHeader
|
||||||
title={t('marketing:pricing')}
|
title={t('marketing:pricing')}
|
||||||
subtitle={t('marketing:pricingSubtitle')}
|
subtitle={t('marketing:pricingSubtitle')}
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
import { AdminGuard } from '@kit/admin/components/admin-guard';
|
|
||||||
|
|
||||||
function AccountPage() {
|
|
||||||
return <div></div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AdminGuard(AccountPage);
|
|
||||||
45
apps/web/app/admin/accounts/[id]/page.tsx
Normal file
45
apps/web/app/admin/accounts/[id]/page.tsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { cache } from 'react';
|
||||||
|
|
||||||
|
import { AdminAccountPage } from '@kit/admin/components/admin-account-page';
|
||||||
|
import { AdminGuard } from '@kit/admin/components/admin-guard';
|
||||||
|
import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client';
|
||||||
|
|
||||||
|
interface Params {
|
||||||
|
params: {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const generateMetadata = async ({ params }: Params) => {
|
||||||
|
const account = await loadAccount(params.id);
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: `Admin | ${account.name}`,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
async function AccountPage({ params }: Params) {
|
||||||
|
const account = await loadAccount(params.id);
|
||||||
|
|
||||||
|
return <AdminAccountPage account={account} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AdminGuard(AccountPage);
|
||||||
|
|
||||||
|
const loadAccount = cache(async (id: string) => {
|
||||||
|
const client = getSupabaseServerComponentClient({
|
||||||
|
admin: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data, error } = await client
|
||||||
|
.from('accounts')
|
||||||
|
.select('*, memberships: accounts_memberships (*)')
|
||||||
|
.eq('id', id)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
});
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ServerDataLoader } from '@makerkit/data-loader-supabase-nextjs';
|
import { ServerDataLoader } from '@makerkit/data-loader-supabase-nextjs';
|
||||||
|
|
||||||
import { AccountsTable } from '@kit/admin/components/accounts-table';
|
import { AdminAccountsTable } from '@kit/admin/components/admin-accounts-table';
|
||||||
import { AdminGuard } from '@kit/admin/components/admin-guard';
|
import { AdminGuard } from '@kit/admin/components/admin-guard';
|
||||||
import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client';
|
import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client';
|
||||||
import { PageBody, PageHeader } from '@kit/ui/page';
|
import { PageBody, PageHeader } from '@kit/ui/page';
|
||||||
@@ -8,8 +8,13 @@ import { PageBody, PageHeader } from '@kit/ui/page';
|
|||||||
interface SearchParams {
|
interface SearchParams {
|
||||||
page?: string;
|
page?: string;
|
||||||
account_type?: 'all' | 'team' | 'personal';
|
account_type?: 'all' | 'team' | 'personal';
|
||||||
|
query?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
title: `Accounts`,
|
||||||
|
};
|
||||||
|
|
||||||
function AccountsPage({ searchParams }: { searchParams: SearchParams }) {
|
function AccountsPage({ searchParams }: { searchParams: SearchParams }) {
|
||||||
const client = getSupabaseServerComponentClient({
|
const client = getSupabaseServerComponentClient({
|
||||||
admin: true,
|
admin: true,
|
||||||
@@ -34,7 +39,7 @@ function AccountsPage({ searchParams }: { searchParams: SearchParams }) {
|
|||||||
>
|
>
|
||||||
{({ data, page, pageSize, pageCount }) => {
|
{({ data, page, pageSize, pageCount }) => {
|
||||||
return (
|
return (
|
||||||
<AccountsTable
|
<AdminAccountsTable
|
||||||
page={page}
|
page={page}
|
||||||
pageSize={pageSize}
|
pageSize={pageSize}
|
||||||
pageCount={pageCount}
|
pageCount={pageCount}
|
||||||
@@ -54,7 +59,8 @@ function AccountsPage({ searchParams }: { searchParams: SearchParams }) {
|
|||||||
function getFilters(params: SearchParams) {
|
function getFilters(params: SearchParams) {
|
||||||
const filters: {
|
const filters: {
|
||||||
[key: string]: {
|
[key: string]: {
|
||||||
eq: boolean;
|
eq?: boolean | string;
|
||||||
|
like?: string;
|
||||||
};
|
};
|
||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
@@ -64,6 +70,12 @@ function getFilters(params: SearchParams) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (params.query) {
|
||||||
|
filters.name = {
|
||||||
|
like: `%${params.query}%`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return filters;
|
return filters;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ import { Page } from '@kit/ui/page';
|
|||||||
|
|
||||||
import { AdminSidebar } from '~/admin/_components/admin-sidebar';
|
import { AdminSidebar } from '~/admin/_components/admin-sidebar';
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
title: `Admin`,
|
||||||
|
};
|
||||||
|
|
||||||
export default function AdminLayout(props: React.PropsWithChildren) {
|
export default function AdminLayout(props: React.PropsWithChildren) {
|
||||||
return <Page sidebar={<AdminSidebar />}>{props.children}</Page>;
|
return <Page sidebar={<AdminSidebar />}>{props.children}</Page>;
|
||||||
}
|
}
|
||||||
|
|||||||
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,
|
DropdownMenuTrigger,
|
||||||
} from '@kit/ui/dropdown-menu';
|
} from '@kit/ui/dropdown-menu';
|
||||||
import { DataTable } from '@kit/ui/enhanced-data-table';
|
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 {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
|
SelectLabel,
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
} from '@kit/ui/select';
|
} from '@kit/ui/select';
|
||||||
|
|
||||||
type Account = Database['public']['Tables']['accounts']['Row'];
|
type Account = Database['public']['Tables']['accounts']['Row'];
|
||||||
|
|
||||||
const FiltersSchema = z.object({
|
const FiltersSchema = z.object({
|
||||||
type: z.enum(['all', 'team', 'personal']),
|
type: z.enum(['all', 'team', 'personal']),
|
||||||
|
query: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export function AccountsTable(
|
export function AdminAccountsTable(
|
||||||
props: React.PropsWithChildren<{
|
props: React.PropsWithChildren<{
|
||||||
data: Account[];
|
data: Account[];
|
||||||
pageCount: number;
|
pageCount: number;
|
||||||
@@ -66,6 +73,7 @@ function AccountsTableFilters(props: {
|
|||||||
resolver: zodResolver(FiltersSchema),
|
resolver: zodResolver(FiltersSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
type: props.filters?.type ?? 'all',
|
type: props.filters?.type ?? 'all',
|
||||||
|
query: '',
|
||||||
},
|
},
|
||||||
mode: 'onChange',
|
mode: 'onChange',
|
||||||
reValidateMode: 'onChange',
|
reValidateMode: 'onChange',
|
||||||
@@ -74,9 +82,10 @@ function AccountsTableFilters(props: {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const pathName = usePathname();
|
const pathName = usePathname();
|
||||||
|
|
||||||
const onSubmit = ({ type }: z.infer<typeof FiltersSchema>) => {
|
const onSubmit = ({ type, query }: z.infer<typeof FiltersSchema>) => {
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
account_type: type,
|
account_type: type,
|
||||||
|
query: query ?? '',
|
||||||
});
|
});
|
||||||
|
|
||||||
const url = `${pathName}?${params.toString()}`;
|
const url = `${pathName}?${params.toString()}`;
|
||||||
@@ -85,35 +94,59 @@ function AccountsTableFilters(props: {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'flex space-x-4'}>
|
<div className={'flex justify-end space-x-4'}>
|
||||||
<form onSubmit={form.handleSubmit((data) => onSubmit(data))}>
|
<Form {...form}>
|
||||||
<Select
|
<form
|
||||||
value={form.watch('type')}
|
className={'flex space-x-4'}
|
||||||
onValueChange={(value) => {
|
onSubmit={form.handleSubmit((data) => onSubmit(data))}
|
||||||
form.setValue(
|
|
||||||
'type',
|
|
||||||
value as z.infer<typeof FiltersSchema>['type'],
|
|
||||||
{
|
|
||||||
shouldValidate: true,
|
|
||||||
shouldDirty: true,
|
|
||||||
shouldTouch: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return onSubmit(form.getValues());
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<Select
|
||||||
<span>Account Type</span>
|
value={form.watch('type')}
|
||||||
</SelectTrigger>
|
onValueChange={(value) => {
|
||||||
|
form.setValue(
|
||||||
|
'type',
|
||||||
|
value as z.infer<typeof FiltersSchema>['type'],
|
||||||
|
{
|
||||||
|
shouldValidate: true,
|
||||||
|
shouldDirty: true,
|
||||||
|
shouldTouch: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
<SelectContent>
|
return onSubmit(form.getValues());
|
||||||
<SelectItem value={'all'}>All</SelectItem>
|
}}
|
||||||
<SelectItem value={'team'}>Team</SelectItem>
|
>
|
||||||
<SelectItem value={'personal'}>Personal</SelectItem>
|
<SelectTrigger>
|
||||||
</SelectContent>
|
<SelectValue placeholder={'Account Type'} />
|
||||||
</Select>
|
</SelectTrigger>
|
||||||
</form>
|
|
||||||
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -123,7 +156,16 @@ function getColumns(): ColumnDef<Account>[] {
|
|||||||
{
|
{
|
||||||
id: 'name',
|
id: 'name',
|
||||||
header: 'Name',
|
header: 'Name',
|
||||||
accessorKey: 'name',
|
cell: ({ row }) => {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
className={'hover:underline'}
|
||||||
|
href={`/admin/accounts/${row.original.id}`}
|
||||||
|
>
|
||||||
|
{row.original.name}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'email',
|
id: 'email',
|
||||||
@@ -151,6 +193,8 @@ function getColumns(): ColumnDef<Account>[] {
|
|||||||
id: 'actions',
|
id: 'actions',
|
||||||
header: '',
|
header: '',
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
|
const isPersonalAccount = row.original.is_personal_account;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
@@ -164,10 +208,18 @@ function getColumns(): ColumnDef<Account>[] {
|
|||||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||||
|
|
||||||
<DropdownMenuItem>
|
<DropdownMenuItem>
|
||||||
<Link href={`/accounts/${row.original.id}`}>View</Link>
|
<Link href={`/admin/accounts/${row.original.id}`}>View</Link>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
||||||
<DropdownMenuItem>Delete</DropdownMenuItem>
|
<If condition={isPersonalAccount}>
|
||||||
|
<DropdownMenuItem className={'text-orange-800'}>
|
||||||
|
Ban
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</If>
|
||||||
|
|
||||||
|
<DropdownMenuItem className={'text-destructive'}>
|
||||||
|
Delete
|
||||||
|
</DropdownMenuItem>
|
||||||
</DropdownMenuGroup>
|
</DropdownMenuGroup>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
@@ -70,7 +70,7 @@ export async function AdminDashboard() {
|
|||||||
<CardTitle>Trials</CardTitle>
|
<CardTitle>Trials</CardTitle>
|
||||||
|
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Th number of trial subscriptions currently active.
|
The number of trial subscriptions currently active.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</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',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -269,7 +269,6 @@ create table if not exists public.accounts(
|
|||||||
id uuid unique not null default extensions.uuid_generate_v4(),
|
id uuid unique not null default extensions.uuid_generate_v4(),
|
||||||
primary_owner_user_id uuid references auth.users on delete
|
primary_owner_user_id uuid references auth.users on delete
|
||||||
cascade not null default auth.uid(),
|
cascade not null default auth.uid(),
|
||||||
-- Auth ID in Supabase Auth
|
|
||||||
name varchar(255) not null,
|
name varchar(255) not null,
|
||||||
slug text unique,
|
slug text unique,
|
||||||
email varchar(320) unique,
|
email varchar(320) unique,
|
||||||
|
|||||||
Reference in New Issue
Block a user