diff --git a/apps/web/app/admin/page.tsx b/apps/web/app/admin/page.tsx
index 146377d9e..859f822ad 100644
--- a/apps/web/app/admin/page.tsx
+++ b/apps/web/app/admin/page.tsx
@@ -1,11 +1,14 @@
+import { AdminDashboard } from '@kit/admin/components/admin-dashboard';
import { PageBody, PageHeader } from '@kit/ui/page';
export default function AdminPage() {
return (
<>
-
+
-
+
+
+
>
);
}
diff --git a/apps/web/package.json b/apps/web/package.json
index 06178fdb1..6056a4a9f 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -33,6 +33,8 @@
"@kit/supabase": "workspace:^",
"@kit/team-accounts": "workspace:^",
"@kit/ui": "workspace:^",
+ "@makerkit/data-loader-supabase-core": "0.0.5",
+ "@makerkit/data-loader-supabase-nextjs": "^0.0.7",
"@marsidev/react-turnstile": "^0.5.4",
"@radix-ui/react-icons": "^1.3.0",
"@supabase/ssr": "^0.1.0",
diff --git a/packages/billing/gateway/package.json b/packages/billing/gateway/package.json
index fd72b2710..3ca1ae7a7 100644
--- a/packages/billing/gateway/package.json
+++ b/packages/billing/gateway/package.json
@@ -14,6 +14,7 @@
"./components": "./src/components/index.ts"
},
"peerDependencies": {
+ "@hookform/resolvers": "3.3.4",
"@kit/billing": "0.1.0",
"@kit/shared": "^0.1.0",
"@kit/stripe": "0.1.0",
@@ -24,6 +25,7 @@
"zod": "^3.22.4"
},
"devDependencies": {
+ "@hookform/resolvers": "^3.3.4",
"@kit/billing": "workspace:^",
"@kit/eslint-config": "workspace:*",
"@kit/lemon-squeezy": "workspace:^",
diff --git a/packages/features/admin/package.json b/packages/features/admin/package.json
index d6507ca64..42f3b5a1d 100644
--- a/packages/features/admin/package.json
+++ b/packages/features/admin/package.json
@@ -10,7 +10,9 @@
},
"prettier": "@kit/prettier-config",
"peerDependencies": {
- "@kit/ui": "0.1.0"
+ "@kit/ui": "0.1.0",
+ "@makerkit/data-loader-supabase-core": "0.0.5",
+ "@makerkit/data-loader-supabase-nextjs": "^0.0.7"
},
"devDependencies": {
"@kit/eslint-config": "workspace:*",
@@ -19,6 +21,8 @@
"@kit/tailwind-config": "workspace:*",
"@kit/tsconfig": "workspace:*",
"@kit/ui": "workspace:^",
+ "@makerkit/data-loader-supabase-core": "0.0.5",
+ "@makerkit/data-loader-supabase-nextjs": "^0.0.7",
"@supabase/supabase-js": "^2.42.0",
"lucide-react": "^0.363.0"
},
diff --git a/packages/features/admin/src/components/admin-dashboard.tsx b/packages/features/admin/src/components/admin-dashboard.tsx
index 594d27463..1a312727c 100644
--- a/packages/features/admin/src/components/admin-dashboard.tsx
+++ b/packages/features/admin/src/components/admin-dashboard.tsx
@@ -1,17 +1,16 @@
-import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from '@kit/ui/card';
-interface Data {
- usersCount: number;
- organizationsCount: number;
- activeSubscriptions: number;
- trialSubscriptions: number;
-}
+import { loadAdminDashboard } from '../lib/server/loaders/admin-dashboard.loader';
+
+export async function AdminDashboard() {
+ const data = await loadAdminDashboard();
-export function AdminDashboard({
- data,
-}: React.PropsWithChildren<{
- data: Data;
-}>) {
return (
Users
+
+
+ The number of personal accounts that have been created.
+
- {data.usersCount}
+ {data.accounts}
- Organizations
+ Team Accounts
+
+
+ The number of team accounts that have been created.
+
- {data.organizationsCount}
+ {data.teamAccounts}
@@ -46,11 +53,14 @@ export function AdminDashboard({
Paying Customers
+
+ The number of paying customers with active subscriptions.
+
- {data.activeSubscriptions}
+ {data.subscriptions}
@@ -58,11 +68,15 @@ export function AdminDashboard({
Trials
+
+
+ Th number of trial subscriptions currently active.
+
- {data.trialSubscriptions}
+ {data.trials}
diff --git a/packages/features/admin/src/components/admin-header.tsx b/packages/features/admin/src/components/admin-header.tsx
deleted file mode 100644
index 1bc62a9b9..000000000
--- a/packages/features/admin/src/components/admin-header.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-import Link from 'next/link';
-
-import { ArrowLeft } from 'lucide-react';
-
-import { Button } from '@kit/ui/button';
-import { PageHeader } from '@kit/ui/page';
-
-export function AdminHeader({
- children,
- paths,
-}: React.PropsWithChildren<{
- paths: {
- appHome: string;
- };
-}>) {
- return (
-
-
-
-
-
- );
-}
diff --git a/packages/features/admin/src/components/admin-sidebar.tsx b/packages/features/admin/src/components/admin-sidebar.tsx
deleted file mode 100644
index 73e166b05..000000000
--- a/packages/features/admin/src/components/admin-sidebar.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-import { Home, User, Users } from 'lucide-react';
-
-import { Sidebar, SidebarContent, SidebarItem } from '@kit/ui/sidebar';
-
-export function AdminSidebar(props: { Logo: React.ReactNode }) {
- return (
-
- {props.Logo}
-
-
- }>
- Admin
-
-
- }>
- Users
-
-
- }
- >
- Organizations
-
-
-
- );
-}
diff --git a/packages/features/admin/src/index.ts b/packages/features/admin/src/index.ts
index 427d99962..a944278f0 100644
--- a/packages/features/admin/src/index.ts
+++ b/packages/features/admin/src/index.ts
@@ -1 +1 @@
-export * from './lib/is-super-admin';
+export * from './lib/server/is-super-admin';
diff --git a/packages/features/admin/src/lib/is-super-admin.ts b/packages/features/admin/src/lib/server/is-super-admin.ts
similarity index 100%
rename from packages/features/admin/src/lib/is-super-admin.ts
rename to packages/features/admin/src/lib/server/is-super-admin.ts
diff --git a/packages/features/admin/src/lib/server/loaders/admin-dashboard.loader.ts b/packages/features/admin/src/lib/server/loaders/admin-dashboard.loader.ts
new file mode 100644
index 000000000..30634d3b7
--- /dev/null
+++ b/packages/features/admin/src/lib/server/loaders/admin-dashboard.loader.ts
@@ -0,0 +1,51 @@
+import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client';
+
+export async function loadAdminDashboard(params?: {
+ count: 'exact' | 'estimated' | 'planned';
+}) {
+ const count = params?.count ?? 'estimated';
+ const client = getSupabaseServerComponentClient({ admin: true });
+
+ const selectParams = {
+ count,
+ head: true,
+ };
+
+ const subscriptionsPromise = client
+ .from('subscriptions')
+ .select('*', selectParams)
+ .eq('status', 'active')
+ .then((response) => response.count);
+
+ const trialsPromise = client
+ .from('subscriptions')
+ .select('*', selectParams)
+ .eq('status', 'trialing')
+ .then((response) => response.count);
+
+ const accountsPromise = client
+ .from('accounts')
+ .select('*', selectParams)
+ .eq('is_personal_account', true)
+ .then((response) => response.count);
+
+ const teamAccountsPromise = client
+ .from('accounts')
+ .select('*', selectParams)
+ .eq('is_personal_account', false)
+ .then((response) => response.count);
+
+ const [subscriptions, trials, accounts, teamAccounts] = await Promise.all([
+ subscriptionsPromise,
+ trialsPromise,
+ accountsPromise,
+ teamAccountsPromise,
+ ]);
+
+ return {
+ subscriptions,
+ trials,
+ accounts,
+ teamAccounts,
+ };
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 7c0354780..04e26c508 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -86,6 +86,12 @@ importers:
'@kit/ui':
specifier: workspace:^
version: link:../../packages/ui
+ '@makerkit/data-loader-supabase-core':
+ specifier: 0.0.5
+ version: 0.0.5(@supabase/postgrest-js@1.15.0)(@supabase/supabase-js@2.42.0)
+ '@makerkit/data-loader-supabase-nextjs':
+ specifier: ^0.0.7
+ version: 0.0.7(@supabase/postgrest-js@1.15.0)(@supabase/supabase-js@2.42.0)(next@14.2.0-canary.62)(react@18.2.0)(swr@2.2.5)
'@marsidev/react-turnstile':
specifier: ^0.5.4
version: 0.5.4(react-dom@18.2.0)(react@18.2.0)
@@ -231,6 +237,9 @@ importers:
packages/billing/gateway:
devDependencies:
+ '@hookform/resolvers':
+ specifier: ^3.3.4
+ version: 3.3.4(react-hook-form@7.51.2)
'@kit/billing':
specifier: workspace:^
version: link:../core
@@ -547,6 +556,12 @@ importers:
'@kit/ui':
specifier: workspace:^
version: link:../../ui
+ '@makerkit/data-loader-supabase-core':
+ specifier: 0.0.5
+ version: 0.0.5(@supabase/postgrest-js@1.15.0)(@supabase/supabase-js@2.42.0)
+ '@makerkit/data-loader-supabase-nextjs':
+ specifier: ^0.0.7
+ version: 0.0.7(@supabase/postgrest-js@1.15.0)(@supabase/supabase-js@2.42.0)(next@14.1.0)(react@18.2.0)(swr@2.2.5)
'@supabase/supabase-js':
specifier: ^2.42.0
version: 2.42.0
@@ -2192,6 +2207,50 @@ packages:
engines: {node: '>=18'}
dev: false
+ /@makerkit/data-loader-supabase-core@0.0.5(@supabase/postgrest-js@1.15.0)(@supabase/supabase-js@2.42.0):
+ resolution: {integrity: sha512-J68TcXACZpbBhFPwNX4AP8O37iPHpcqpT8PLTLeMvoCpm2HI2MK+KGV5uj2zJydHKClRi44KkNa0BUHJzg9myw==}
+ peerDependencies:
+ '@supabase/postgrest-js': '>1.0.0'
+ '@supabase/supabase-js': '>=2.0.0'
+ dependencies:
+ '@supabase/postgrest-js': 1.15.0
+ '@supabase/supabase-js': 2.42.0
+ ts-case-convert: 2.0.7
+
+ /@makerkit/data-loader-supabase-nextjs@0.0.7(@supabase/postgrest-js@1.15.0)(@supabase/supabase-js@2.42.0)(next@14.1.0)(react@18.2.0)(swr@2.2.5):
+ resolution: {integrity: sha512-iPi4dWkZnv3awtlJaQIVLo7nca8XEjSMTE1HHZg1PtBAYRyLbq6vJYzhez0XVu/OSncw5QyhTzCzB1dxzOHxhw==}
+ peerDependencies:
+ '@supabase/supabase-js': '>=2.0.0'
+ next: '>=13.4.0'
+ react: '>=18.0.0'
+ swr: '>=2.0.0'
+ dependencies:
+ '@makerkit/data-loader-supabase-core': 0.0.5(@supabase/postgrest-js@1.15.0)(@supabase/supabase-js@2.42.0)
+ '@supabase/supabase-js': 2.42.0
+ next: 14.1.0(@opentelemetry/api@1.8.0)(react-dom@18.2.0)(react@18.2.0)
+ react: 18.2.0
+ swr: 2.2.5(react@18.2.0)
+ transitivePeerDependencies:
+ - '@supabase/postgrest-js'
+ dev: true
+
+ /@makerkit/data-loader-supabase-nextjs@0.0.7(@supabase/postgrest-js@1.15.0)(@supabase/supabase-js@2.42.0)(next@14.2.0-canary.62)(react@18.2.0)(swr@2.2.5):
+ resolution: {integrity: sha512-iPi4dWkZnv3awtlJaQIVLo7nca8XEjSMTE1HHZg1PtBAYRyLbq6vJYzhez0XVu/OSncw5QyhTzCzB1dxzOHxhw==}
+ peerDependencies:
+ '@supabase/supabase-js': '>=2.0.0'
+ next: '>=13.4.0'
+ react: '>=18.0.0'
+ swr: '>=2.0.0'
+ dependencies:
+ '@makerkit/data-loader-supabase-core': 0.0.5(@supabase/postgrest-js@1.15.0)(@supabase/supabase-js@2.42.0)
+ '@supabase/supabase-js': 2.42.0
+ next: 14.2.0-canary.62(react-dom@18.2.0)(react@18.2.0)
+ react: 18.2.0
+ swr: 2.2.5(react@18.2.0)
+ transitivePeerDependencies:
+ - '@supabase/postgrest-js'
+ dev: false
+
/@manypkg/cli@0.21.3:
resolution: {integrity: sha512-ro6j5b+44dN2AfId23voWxdlOqUCSbCwUHrUwq0LpoN/oZy6zQFAHDwYHbw50j2nL9EgpwIA03ZjaBceuUcMrw==}
engines: {node: '>=14.18.0'}
@@ -2300,7 +2359,6 @@ packages:
/@next/env@14.1.0:
resolution: {integrity: sha512-Py8zIo+02ht82brwwhTg36iogzFqGLPXlRGKQw5s+qP/kMNc4MAyDeEwBKDijk6zTIbegEgu8Qy7C1LboslQAw==}
- dev: false
/@next/env@14.2.0-canary.62:
resolution: {integrity: sha512-K5lmKK/TalagQELw3W0hKDXmNGGXY3Zxw3yH27y9DOT7evhiJvK2Ywq5uN4lEjYHkeW+QbyC/OjUIcK3RUSHbQ==}
@@ -2327,7 +2385,6 @@ packages:
cpu: [arm64]
os: [darwin]
requiresBuild: true
- dev: false
optional: true
/@next/swc-darwin-arm64@14.2.0-canary.62:
@@ -2354,7 +2411,6 @@ packages:
cpu: [x64]
os: [darwin]
requiresBuild: true
- dev: false
optional: true
/@next/swc-darwin-x64@14.2.0-canary.62:
@@ -2381,7 +2437,6 @@ packages:
cpu: [arm64]
os: [linux]
requiresBuild: true
- dev: false
optional: true
/@next/swc-linux-arm64-gnu@14.2.0-canary.62:
@@ -2408,7 +2463,6 @@ packages:
cpu: [arm64]
os: [linux]
requiresBuild: true
- dev: false
optional: true
/@next/swc-linux-arm64-musl@14.2.0-canary.62:
@@ -2435,7 +2489,6 @@ packages:
cpu: [x64]
os: [linux]
requiresBuild: true
- dev: false
optional: true
/@next/swc-linux-x64-gnu@14.2.0-canary.62:
@@ -2462,7 +2515,6 @@ packages:
cpu: [x64]
os: [linux]
requiresBuild: true
- dev: false
optional: true
/@next/swc-linux-x64-musl@14.2.0-canary.62:
@@ -2489,7 +2541,6 @@ packages:
cpu: [arm64]
os: [win32]
requiresBuild: true
- dev: false
optional: true
/@next/swc-win32-arm64-msvc@14.2.0-canary.62:
@@ -2516,7 +2567,6 @@ packages:
cpu: [ia32]
os: [win32]
requiresBuild: true
- dev: false
optional: true
/@next/swc-win32-ia32-msvc@14.2.0-canary.62:
@@ -2543,7 +2593,6 @@ packages:
cpu: [x64]
os: [win32]
requiresBuild: true
- dev: false
optional: true
/@next/swc-win32-x64-msvc@14.2.0-canary.62:
@@ -2594,7 +2643,6 @@ packages:
/@opentelemetry/api@1.8.0:
resolution: {integrity: sha512-I/s6F7yKUDdtMsoBWXJe8Qz40Tui5vsuKCWJEWVL+5q9sSWRzzx6v2KeNsOBEwd94j0eWkpWCH4yB6rZg9Mf0w==}
engines: {node: '>=8.0.0'}
- dev: false
/@opentelemetry/context-async-hooks@1.21.0(@opentelemetry/api@1.8.0):
resolution: {integrity: sha512-t0iulGPiMjG/NrSjinPQoIf8ST/o9V0dGOJthfrFporJlNdlKIQPfC7lkrV+5s2dyBThfmSbJlp/4hO1eOcDXA==}
@@ -5013,7 +5061,6 @@ packages:
resolution: {integrity: sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==}
dependencies:
tslib: 2.6.2
- dev: false
/@swc/helpers@0.5.5:
resolution: {integrity: sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==}
@@ -6078,7 +6125,6 @@ packages:
engines: {node: '>=10.16.0'}
dependencies:
streamsearch: 1.1.0
- dev: false
/cacheable-lookup@7.0.0:
resolution: {integrity: sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==}
@@ -6278,7 +6324,6 @@ packages:
/client-only@0.0.1:
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
- dev: false
/clipanion@3.2.1(typanion@3.14.0):
resolution: {integrity: sha512-dYFdjLb7y1ajfxQopN05mylEpK9ZX0sO1/RfMXdfmwjlIsPkbh4p7A682x++zFPLDCo1x3p82dtljHf5cW2LKA==}
@@ -7938,7 +7983,6 @@ packages:
/graceful-fs@4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
- dev: false
/gradient-string@2.0.2:
resolution: {integrity: sha512-rEDCuqUQ4tbD78TpzsMtt5OIf0cBCSDWSJtUDaF6JsAh+k0v9r++NzxNEG87oDZx9ZwGhD8DaezR2L/yrw0Jdw==}
@@ -9749,7 +9793,6 @@ packages:
transitivePeerDependencies:
- '@babel/core'
- babel-plugin-macros
- dev: false
/next@14.2.0-canary.62(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-SsS+fpJ/anrtLgeCC76V9WOlreZanUYsuKsRMx+FDwOJ3ZnbZkohu3+RRLIQM1vtWcp707iV11+OlF/qgOldCA==}
@@ -10334,7 +10377,6 @@ packages:
nanoid: 3.3.7
picocolors: 1.0.0
source-map-js: 1.2.0
- dev: false
/postcss@8.4.33:
resolution: {integrity: sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==}
@@ -11433,7 +11475,6 @@ packages:
/streamsearch@1.1.0:
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
engines: {node: '>=10.0.0'}
- dev: false
/string-width@4.2.3:
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
@@ -11574,7 +11615,6 @@ packages:
dependencies:
client-only: 0.0.1
react: 18.2.0
- dev: false
/sucrase@3.35.0:
resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==}
@@ -11634,6 +11674,15 @@ packages:
upper-case: 1.1.3
dev: false
+ /swr@2.2.5(react@18.2.0):
+ resolution: {integrity: sha512-QtxqyclFeAsxEUeZIYmsaQ0UjimSq1RZ9Un7I68/0ClKK/U3LoyQunwkQfJZr2fc22DfIXLNDc2wFyTEikCUpg==}
+ peerDependencies:
+ react: ^16.11.0 || ^17.0.0 || ^18.0.0
+ dependencies:
+ client-only: 0.0.1
+ react: 18.2.0
+ use-sync-external-store: 1.2.0(react@18.2.0)
+
/tailwind-merge@2.2.0:
resolution: {integrity: sha512-SqqhhaL0T06SW59+JVNfAqKdqLs0497esifRrZ7jOaefP3o64fdFNDMrAQWZFMxTLJPiHVjRLUywT8uFz1xNWQ==}
dependencies:
@@ -11861,6 +11910,9 @@ packages:
typescript: 5.4.3
dev: false
+ /ts-case-convert@2.0.7:
+ resolution: {integrity: sha512-Kqj8wrkuduWsKUOUNRczrkdHCDt4ZNNd6HKjVw42EnMIGHQUABS4pqfy0acETVLwUTppc1fzo/yi11+uMTaqzw==}
+
/ts-interface-checker@0.1.13:
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
@@ -11914,7 +11966,6 @@ packages:
/tslib@2.6.2:
resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
- dev: false
/turbo-darwin-64@1.13.2:
resolution: {integrity: sha512-CCSuD8CfmtncpohCuIgq7eAzUas0IwSbHfI8/Q3vKObTdXyN8vAo01gwqXjDGpzG9bTEVedD0GmLbD23dR0MLA==}
@@ -12243,6 +12294,13 @@ packages:
tslib: 2.6.2
dev: false
+ /use-sync-external-store@1.2.0(react@18.2.0):
+ resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0
+ dependencies:
+ react: 18.2.0
+
/util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}