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==}