diff --git a/apps/admin/.env.local.example b/apps/admin/.env.local.example index d129b0ce8..b988ccb31 100644 --- a/apps/admin/.env.local.example +++ b/apps/admin/.env.local.example @@ -1,20 +1,3 @@ -REDDIT_CLIENT_ID=your_client_id -REDDIT_CLIENT_SECRET=your_client_secret -REDDIT_USER_AGENT=your_user_agent - -GOOGLE_GENERATIVE_AI_API_KEY=your_api_key - -DATABASE_URL=postgresql://postgres:[YOUR-PASSWORD]@db.[SUPABASE_REFERENCE_ID].supabase.co:6543/postgres?pgbouncer=true&connection_limit=30&pool_timeout=600 -DIRECT_URL=postgresql://postgres:[YOUR-PASSWORD]@db.[SUPABASE_REFERENCE_ID].supabase.com:5432/postgres - -NEXTAUTH_SECRET=secret_key -NEXTAUTH_URL=http://localhost:3000 - -AUTH_GOOGLE_ID=google_id -AUTH_GOOGLE_SECRET=google_secret - -PASSWORD_KEY=some_random_value - -GOOGLE_CHAT_WEBHOOK_URL=google_chat_webhook_url - -CRON_SECRET=some_random_value +AXIOM_ORG_ID= +AXIOM_TOKEN= +DATABASE_URL= diff --git a/apps/admin/next.config.mjs b/apps/admin/next.config.mjs index 005aef8b4..8041a435f 100644 --- a/apps/admin/next.config.mjs +++ b/apps/admin/next.config.mjs @@ -1,22 +1,10 @@ // @ts-check -import { PrismaPlugin } from '@prisma/nextjs-monorepo-workaround-plugin'; /** * @type {import('next').NextConfig} **/ const nextConfig = { - pageExtensions: ['js', 'jsx', 'ts', 'tsx', 'md', 'mdx'], reactStrictMode: true, - experimental: { - optimizePackageImports: ['@mantine/core', '@mantine/hooks'], - }, - webpack: (config, { isServer }) => { - if (isServer) { - config.plugins = [...config.plugins, new PrismaPlugin()]; - } - - return config; - }, }; export default nextConfig; diff --git a/apps/admin/package.json b/apps/admin/package.json index c2f2888a6..32f6a8e33 100644 --- a/apps/admin/package.json +++ b/apps/admin/package.json @@ -12,8 +12,12 @@ "tsc": "tsc" }, "dependencies": { + "@axiomhq/js": "1.3.1", "clsx": "^2.1.1", + "date-fns": "2.30.0", + "lodash-es": "^4.17.21", "next": "14.2.15", + "pg": "^8.16.3", "react": "^18.2.0", "react-dom": "^18.2.0" }, @@ -25,6 +29,7 @@ "@tailwindcss/container-queries": "^0.1.1", "@tailwindcss/forms": "^0.5.10", "@tailwindcss/typography": "^0.5.16", + "@types/pg": "^8.15.4", "@types/react": "18.0.28", "@types/react-dom": "18.0.11", "eslint": "^9.26.0", diff --git a/apps/admin/src/app/api/conversions/route.ts b/apps/admin/src/app/api/conversions/route.ts new file mode 100644 index 000000000..d51a1d2f5 --- /dev/null +++ b/apps/admin/src/app/api/conversions/route.ts @@ -0,0 +1,95 @@ +import { Axiom } from '@axiomhq/js'; +import { startOfDay, subDays } from 'date-fns'; +import { Client as PgClient } from 'pg'; + +const daysBefore = 30; + +const pgClient = new PgClient({ + connectionString: process.env.DATABASE_URL, + ssl: { rejectUnauthorized: false }, +}); + +const aplQuery = ` +events +| extend shifted_time = _time + 8h +| summarize + CheckoutSuccess = countif(['event.name'] == 'checkout.success' and ['event.payload.namespace'] == 'interviews'), + CheckoutInitiate = dcountif(['user.fingerprint'], ['event.name'] == 'checkout.attempt' and ['event.payload.namespace'] == 'interviews'), + NumVisits = dcountif( + ['user.fingerprint'], + (['event.name'] == 'pageview' + and ['request.pathname'] !startswith '/projects' + )), + NumFirstVisits = dcountif( + ['user.fingerprint'], + (['event.name'] == 'pageview' + and ['request.pathname'] !startswith '/projects' + and todatetime(['user.firstVisit']) > (_time - 24h) + )), + CheckoutSuccessSameDay = dcountif(['user.email'], ['event.name'] == 'checkout.success' and ['event.payload.namespace'] == 'interviews' and todatetime(['user.firstVisit']) > (_time - 24h)), + CheckoutInitiateSameDay = dcountif(['user.email'], ['event.name'] == 'checkout.attempt' and ['event.payload.namespace'] == 'interviews' and todatetime(['user.firstVisit']) > (_time - 24h)), + SignUps = dcountif( + ['user.fingerprint'], + (['event.name'] == 'auth.sign_up' + )) + by ['time'] = startofday(shifted_time) +| extend CheckoutInitiateRate = round(100.0 * CheckoutInitiate / NumVisits, 2) +| extend CheckoutSuccessRate = round(100.0 * CheckoutSuccess / NumVisits, 2) +| extend CheckoutInitiateSameDayRate = round(100.0 * CheckoutInitiateSameDay / NumFirstVisits, 2) +| extend CheckoutSuccessSameDayRate = round(100.0 * CheckoutSuccessSameDay / NumFirstVisits, 2) +| order by _time desc +`; + +const pgQuerySignUps = `SELECT + date_trunc('day', created_at AT TIME ZONE 'Asia/Singapore') AS date, + count(*) AS "signUps" +FROM + auth.users +WHERE + created_at >= NOW() - INTERVAL '${daysBefore} days' +GROUP BY + date +ORDER BY date DESC;`; + +const pgQueryEmailSignUps = `SELECT + date_trunc('day', created_at AT TIME ZONE 'Asia/Singapore') AS date, + count(*) AS "emailSignUps", + count(email_confirmed_at) AS "confirmedEmailSignUps" +FROM + auth.users +WHERE + confirmation_sent_at IS NOT NULL AND + created_at >= NOW() - INTERVAL '${daysBefore} days' +GROUP BY + date +ORDER BY + date DESC;`; + +export async function GET(_request: Request) { + const axiom = new Axiom({ + orgId: process.env.AXIOM_ORG_ID!, + token: process.env.AXIOM_TOKEN!, + }); + + try { + await pgClient.connect(); + } catch (error) { + // Ignore + } + + const [axiomRes, pgResSignUps, pgResEmailSignUps] = await Promise.all([ + axiom.query(aplQuery, { + startTime: startOfDay(subDays(new Date(), daysBefore)).toISOString(), + }), + pgClient.query(pgQuerySignUps), + pgClient.query(pgQueryEmailSignUps), + ]); + + pgClient.end(); + + return Response.json({ + conversions: axiomRes.matches, + emailSignUps: pgResEmailSignUps.rows, + signUps: pgResSignUps.rows, + }); +} diff --git a/apps/admin/src/app/layout.tsx b/apps/admin/src/app/layout.tsx index 8398f97b6..7d49cd9fd 100644 --- a/apps/admin/src/app/layout.tsx +++ b/apps/admin/src/app/layout.tsx @@ -3,8 +3,6 @@ import type { Metadata } from 'next'; import '~/styles/globals.css'; -import RootLayout from '../components/RootLayout'; - type Props = Readonly<{ children: React.ReactNode; }>; @@ -17,9 +15,7 @@ export const metadata: Metadata = { export default function Layout({ children }: Props) { return ( - - {children} - + {children} ); } diff --git a/apps/admin/src/app/page.tsx b/apps/admin/src/app/page.tsx index 74d2692ea..2e8528a78 100644 --- a/apps/admin/src/app/page.tsx +++ b/apps/admin/src/app/page.tsx @@ -1,7 +1,5 @@ +import ConversionsPage from '~/components/ConversionsPage'; + export default function Page() { - return ( -
-

Admin Dashboard

-
- ); + return ; } diff --git a/apps/admin/src/components/ConversionsPage.tsx b/apps/admin/src/components/ConversionsPage.tsx new file mode 100644 index 000000000..5f0de2ffa --- /dev/null +++ b/apps/admin/src/components/ConversionsPage.tsx @@ -0,0 +1,40 @@ +'use client'; + +import { useEffect, useState } from 'react'; + +import type { DayData, EmailSignupData, SignupData } from './ConversionsTable'; +import ConversionsTable from './ConversionsTable'; + +export default function ConversionsPage() { + const [data, setData] = useState<{ + conversions: ReadonlyArray; + emailSignUps: ReadonlyArray; + signUps: ReadonlyArray; + } | null>(null); + + useEffect(() => { + // This is where you would fetch conversion data or any other necessary data + // For example, you could use fetch or axios to get data from an API endpoint + fetch('/api/conversions') + .then((response) => response.json()) + .then((data_) => { + setData(data_); + }) + .catch((error) => { + console.error('Error fetching conversion data:', error); + }); + }, []); + + return ( +
+

Critical data

+ {data && ( + + )} +
+ ); +} diff --git a/apps/admin/src/components/ConversionsTable.tsx b/apps/admin/src/components/ConversionsTable.tsx new file mode 100644 index 000000000..fe03f30d0 --- /dev/null +++ b/apps/admin/src/components/ConversionsTable.tsx @@ -0,0 +1,305 @@ +import clsx from 'clsx'; +import { addDays } from 'date-fns'; + +/* eslint-disable react/no-array-index-key */ + +const thClassname = + 'sticky top-0 z-10 px-2 py-2 text-left text-sm font-semibold text-gray-100 bg-gray-900 align-top'; +const tdClassname = 'px-2 py-2 text-sm text-gray-900'; + +export type SignupData = Readonly<{ + date: string; + signUps: string; +}>; + +export type EmailSignupData = Readonly<{ + confirmedEmailSignUps: string; + date: string; + emailSignUps: string; +}>; + +export type DayData = Readonly<{ + _rowId: string; + _time: string; + data: Readonly<{ + CheckoutInitiate: number; + CheckoutInitiateRate: string; + CheckoutInitiateSameDay: number; + CheckoutInitiateSameDayRate: string; + CheckoutSuccess: number; + CheckoutSuccessRate: string; + CheckoutSuccessSameDay: number; + CheckoutSuccessSameDayRate: string; + NumFirstVisits: number; + NumVisits: number; + time: string; + }>; +}>; + +const startOfWeekDay = 1; // Monday + +const dateFormat = { + day: '2-digit', + month: 'short', +} as const; + +// A helper to parse percent values safely +function parsePercent(value: string) { + const num = parseFloat(value); + + return isNaN(num) ? 0 : num; +} + +export default function ConversionsTable({ + conversions, + emailSignUps, + signUps, +}: { + conversions: ReadonlyArray; + emailSignUps: ReadonlyArray; + signUps: ReadonlyArray; +}) { + const conversionsRows = conversions + .slice() + .sort( + (a, b) => + new Date(a.data.time).getTime() - new Date(b.data.time).getTime(), + ); + + const signUpByDate = signUps.reduce((acc, signup, index) => { + const date = new Date(signup.date).toLocaleDateString('en-SG'); + const emailSignUpDay = emailSignUps[index]; + + return { + ...acc, + [date]: { + confirmedEmailSignUps: Number(emailSignUpDay.confirmedEmailSignUps), + emailSignUps: Number(emailSignUpDay.emailSignUps), + signUps: Number(signup.signUps), + }, + }; + }, {}) as Record< + string, + { confirmedEmailSignUps: number; emailSignUps: number; signUps: number } + >; + + const firstStartOfWeekIndex = conversionsRows.findIndex((row) => { + const date = new Date(row.data.time); + + return date.getDay() === startOfWeekDay; + }); + + // Precompute numeric values per column for percentile calculation + const numericColumnValues: Record> = { + CheckoutInitiate: conversionsRows.map((r) => r.data.CheckoutInitiate), + CheckoutInitiateRate: conversionsRows.map((r) => + parsePercent(r.data.CheckoutInitiateRate), + ), + CheckoutInitiateSameDayRate: conversionsRows.map((r) => + parsePercent(r.data.CheckoutInitiateSameDayRate), + ), + CheckoutSuccess: conversionsRows.map((r) => r.data.CheckoutSuccess), + CheckoutSuccessRate: conversionsRows.map((r) => + parsePercent(r.data.CheckoutSuccessRate), + ), + CheckoutSuccessSameDay: conversionsRows.map( + (r) => r.data.CheckoutSuccessSameDay, + ), + CheckoutSuccessSameDayRate: conversionsRows.map((r) => + parsePercent(r.data.CheckoutSuccessSameDayRate), + ), + NumFirstVisits: conversionsRows.map((r) => r.data.NumFirstVisits), + NumVisits: conversionsRows.map((r) => r.data.NumVisits), + }; + + const columns: Array<{ + getValue: ( + row: DayData, + signUp: + | { + confirmedEmailSignUps: number; + emailSignUps: number; + signUps: number; + } + | undefined, + ) => React.ReactNode; + header: React.ReactNode; + key: string | null; // Null for non-numeric columns + }> = [ + { + getValue: (row) => + new Date(row.data.time).toLocaleDateString('en-US', dateFormat), + header: 'Date', + key: null, + }, + { + getValue: (row) => row.data.NumVisits, + header: 'Non-purchasers', + key: 'NumVisits', + }, + { + getValue: (row) => row.data.NumFirstVisits, + header: 'First visits', + key: 'NumFirstVisits', + }, + { + getValue: (row) => row.data.CheckoutInitiate, + header: 'Checkout initiates', + key: 'CheckoutInitiate', + }, + { + getValue: (row) => `${row.data.CheckoutInitiateRate}%`, + header: <>Checkout initiate rate, + key: 'CheckoutInitiateRate', + }, + { + getValue: (row) => `${row.data.CheckoutInitiateSameDayRate}%`, + header: ( + <> + Checkout initiate rate +
+ (same day) + + ), + key: 'CheckoutInitiateSameDayRate', + }, + { + getValue: (row) => row.data.CheckoutSuccess, + header: 'New payments', + key: 'CheckoutSuccess', + }, + { + getValue: (row) => `${row.data.CheckoutSuccessRate}%`, + header: 'Conversion rate', + key: 'CheckoutSuccessRate', + }, + { + getValue: (row) => row.data.CheckoutSuccessSameDay, + header: ( + <> + New payments +
+ (same day) + + ), + key: 'CheckoutSuccessSameDay', + }, + { + getValue: (row) => `${row.data.CheckoutSuccessSameDayRate}%`, + header: ( + <> + Conversion rate +
+ (same day) + + ), + key: 'CheckoutSuccessSameDayRate', + }, + { + getValue: (_row, signUp) => signUp?.signUps ?? 0, + header: 'Signups', + key: null, + }, + { + getValue: (_row, signUp) => + signUp && signUp.signUps > 0 + ? `${((signUp.confirmedEmailSignUps / signUp.emailSignUps) * 100).toFixed(2)}%` + : '-', + header: 'Email verification rate', + key: null, + }, + ]; + + // Helper to compute percentile rank + function getPercentile(value: number, allValues: Array): number { + const sorted = [...allValues].sort((a, b) => a - b); + const index = sorted.findIndex((v) => v >= value); + const rank = index === -1 ? sorted.length : index + 1; + + return (rank / sorted.length) * 100; + } + + // Helper to get background color + function getBackground(percentile: number): string { + const bucket = Math.floor(percentile / 10) * 10; + + if (bucket <= 5) return 'bg-red-500/50'; + if (bucket <= 15) return 'bg-red-400/50'; + if (bucket <= 25) return 'bg-red-300/50'; + if (bucket <= 35) return 'bg-red-200/50'; + if (bucket <= 45) return 'bg-red-100/50'; + if (bucket <= 55) return 'bg-white'; + if (bucket <= 65) return 'bg-green-100/50'; + if (bucket <= 75) return 'bg-green-200/50'; + if (bucket <= 85) return 'bg-green-300/50'; + if (bucket <= 95) return 'bg-green-400/50'; + + return 'bg-green-500/50'; + } + + return ( + + + + + {columns.map((col, i) => ( + + ))} + + + + {conversionsRows.map((row, index) => { + const date = new Date(row.data.time); + const isStartOfWeek = date.getDay() === startOfWeekDay; + const signUp = signUpByDate[date.toLocaleDateString('en-SG')]; + + return ( + + {index === 0 && !isStartOfWeek && ( + + )} + {columns.map((col, index_) => { + let bgClass = ''; + + if (col.key && numericColumnValues[col.key]) { + const rawValue = col.key.endsWith('Rate') + ? // @ts-expect-error: vibe coded + parsePercent(row.data[col.key]) + : // @ts-expect-error: vibe coded + row.data[col.key]; + const percentile = getPercentile( + rawValue, + numericColumnValues[col.key], + ); + + bgClass = getBackground(percentile); + } + + return ( + + ); + })} + + ); + })} + +
+ Week + + {col.header} +
+ )} + {isStartOfWeek && ( + + {date.toLocaleDateString('en-US', dateFormat)} to{' '} + {addDays(date, 7).toLocaleDateString('en-US', dateFormat)} + + {col.getValue(row, signUp)} +
+ ); +} diff --git a/apps/admin/src/components/RootLayout.tsx b/apps/admin/src/components/RootLayout.tsx deleted file mode 100644 index af1a5b897..000000000 --- a/apps/admin/src/components/RootLayout.tsx +++ /dev/null @@ -1,9 +0,0 @@ -'use client'; - -type Props = Readonly<{ - children: React.ReactNode; -}>; - -export default function RootLayout({ children }: Props) { - return <>{children}; -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 16c33f306..ea649de14 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,12 +20,24 @@ importers: apps/admin: dependencies: + '@axiomhq/js': + specifier: 1.3.1 + version: 1.3.1 clsx: specifier: ^2.1.1 version: 2.1.1 + date-fns: + specifier: 2.30.0 + version: 2.30.0 + lodash-es: + specifier: ^4.17.21 + version: 4.17.21 next: specifier: 14.2.15 version: 14.2.15(@babel/core@7.27.4)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + pg: + specifier: ^8.16.3 + version: 8.16.3 react: specifier: ^18.2.0 version: 18.3.1 @@ -54,6 +66,9 @@ importers: '@tailwindcss/typography': specifier: ^0.5.16 version: 0.5.16(tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.12.11)(@types/node@22.15.0)(typescript@5.8.3))) + '@types/pg': + specifier: ^8.15.4 + version: 8.15.4 '@types/react': specifier: 18.0.28 version: 18.0.28 @@ -271,7 +286,7 @@ importers: version: 10.0.0 autoprefixer: specifier: ^10.4.21 - version: 10.4.21(postcss@8.5.3) + version: 10.4.21(postcss@8.5.6) dotenv-cli: specifier: ^7.3.0 version: 7.4.4 @@ -280,13 +295,13 @@ importers: version: 9.26.0(jiti@1.21.7) postcss: specifier: ^8.4.32 - version: 8.5.3 + version: 8.5.6 postcss-preset-mantine: specifier: ^1.11.1 - version: 1.17.0(postcss@8.5.3) + version: 1.17.0(postcss@8.5.6) postcss-simple-vars: specifier: ^7.0.1 - version: 7.0.1(postcss@8.5.3) + version: 7.0.1(postcss@8.5.6) prettier: specifier: 3.5.3 version: 3.5.3 @@ -725,13 +740,13 @@ importers: version: 1.18.8 '@vitejs/plugin-react': specifier: ^4.4.1 - version: 4.5.2(vite@6.3.5(@types/node@22.15.0)(jiti@1.21.7)(sugarss@4.0.1(postcss@8.5.3))(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0)) + version: 4.5.2(vite@6.3.5(@types/node@22.15.0)(jiti@1.21.7)(sugarss@4.0.1(postcss@8.5.6))(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0)) '@vitest/ui': specifier: ^3.1.2 version: 3.1.3(vitest@3.1.3) autoprefixer: specifier: ^10.4.13 - version: 10.4.21(postcss@8.5.3) + version: 10.4.21(postcss@8.5.6) chalk: specifier: ^5.2.0 version: 5.4.1 @@ -776,7 +791,7 @@ importers: version: 3.1.55(@next/env@14.2.15)(next@14.2.15(@babel/core@7.27.4)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) postcss: specifier: ^8.4.21 - version: 8.5.3 + version: 8.5.6 prettier-plugin-tailwindcss: specifier: ^0.5.9 version: 0.5.14(prettier-plugin-svelte@3.2.5(prettier@3.3.2)(svelte@5.30.1))(prettier@3.3.2) @@ -806,10 +821,10 @@ importers: version: 5.8.3 vite-tsconfig-paths: specifier: ^5.1.4 - version: 5.1.4(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.0)(jiti@1.21.7)(sugarss@4.0.1(postcss@8.5.3))(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0)) + version: 5.1.4(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.0)(jiti@1.21.7)(sugarss@4.0.1(postcss@8.5.6))(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0)) vitest: specifier: ^3.0.2 - version: 3.1.3(@types/debug@4.1.12)(@types/node@22.15.0)(@vitest/ui@3.1.3)(jiti@1.21.7)(jsdom@26.1.0)(sugarss@4.0.1(postcss@8.5.3))(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) + version: 3.1.3(@types/debug@4.1.12)(@types/node@22.15.0)(@vitest/ui@3.1.3)(jiti@1.21.7)(jsdom@26.1.0)(sugarss@4.0.1(postcss@8.5.6))(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) packages/eslint-config: devDependencies: @@ -1201,6 +1216,10 @@ packages: resolution: {integrity: sha512-4xf0b/KFVFmNvjxK4lHHuwioQ6TPr+PZLqEdMl/vcot1RqFwDnjaOEOp0/VPhyTgdE0di8bl/hOsu33bHMM27w==} engines: {node: '>=16'} + '@axiomhq/js@1.3.1': + resolution: {integrity: sha512-Ytf5V3wKz8FKNiqJxnqZmUhjgJ7TItKUoyHVNE/H2V9dN1ozD6NNnsueenOjKdA48cm2sGRyP432nworst18aA==} + engines: {node: '>=16'} + '@babel/code-frame@7.27.1': resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} @@ -4474,6 +4493,9 @@ packages: '@types/parse5@6.0.3': resolution: {integrity: sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==} + '@types/pg@8.15.4': + resolution: {integrity: sha512-I6UNVBAoYbvuWkkU3oosC8yxqH21f4/Jc4DK71JLG3dT2mdlGe1z+ep/LQGXaKaOgcvUrsQoPRqfgtMcvZiJhg==} + '@types/phoenix@1.6.6': resolution: {integrity: sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==} @@ -8240,6 +8262,40 @@ packages: periscopic@3.1.0: resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==} + pg-cloudflare@1.2.7: + resolution: {integrity: sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==} + + pg-connection-string@2.9.1: + resolution: {integrity: sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==} + + pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + pg-pool@3.10.1: + resolution: {integrity: sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==} + peerDependencies: + pg: '>=8.0' + + pg-protocol@1.10.3: + resolution: {integrity: sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==} + + pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + + pg@8.16.3: + resolution: {integrity: sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==} + engines: {node: '>= 16.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + + pgpass@1.0.5: + resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -8554,14 +8610,26 @@ packages: resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} engines: {node: ^10 || ^12 || >=14} - postcss@8.5.3: - resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==} - engines: {node: ^10 || ^12 || >=14} - postcss@8.5.6: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} + postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + postgres-bytea@1.0.0: + resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==} + engines: {node: '>=0.10.0'} + + postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + preact-render-to-string@5.2.6: resolution: {integrity: sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==} peerDependencies: @@ -9446,6 +9514,10 @@ packages: spawn-command@0.0.2: resolution: {integrity: sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==} + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} @@ -10226,6 +10298,10 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + uuid@11.1.0: + resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} + hasBin: true + uuid@3.4.0: resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==} deprecated: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details. @@ -10528,6 +10604,10 @@ packages: xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -11145,6 +11225,11 @@ snapshots: fetch-retry: 6.0.0 uuid: 8.3.2 + '@axiomhq/js@1.3.1': + dependencies: + fetch-retry: 6.0.0 + uuid: 11.1.0 + '@babel/code-frame@7.27.1': dependencies: '@babel/helper-validator-identifier': 7.27.1 @@ -14871,6 +14956,12 @@ snapshots: '@types/parse5@6.0.3': {} + '@types/pg@8.15.4': + dependencies: + '@types/node': 22.15.0 + pg-protocol: 1.10.3 + pg-types: 2.2.0 + '@types/phoenix@1.6.6': {} '@types/picomatch@3.0.2': {} @@ -15031,7 +15122,7 @@ snapshots: '@use-gesture/core': 10.3.1 react: 18.3.1 - '@vitejs/plugin-react@4.5.2(vite@6.3.5(@types/node@22.15.0)(jiti@1.21.7)(sugarss@4.0.1(postcss@8.5.3))(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0))': + '@vitejs/plugin-react@4.5.2(vite@6.3.5(@types/node@22.15.0)(jiti@1.21.7)(sugarss@4.0.1(postcss@8.5.6))(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0))': dependencies: '@babel/core': 7.27.4 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.27.4) @@ -15039,7 +15130,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.11 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 6.3.5(@types/node@22.15.0)(jiti@1.21.7)(sugarss@4.0.1(postcss@8.5.3))(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.5(@types/node@22.15.0)(jiti@1.21.7)(sugarss@4.0.1(postcss@8.5.6))(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) transitivePeerDependencies: - supports-color @@ -15050,14 +15141,6 @@ snapshots: chai: 5.2.0 tinyrainbow: 2.0.0 - '@vitest/mocker@3.1.3(vite@6.3.5(@types/node@22.15.0)(jiti@1.21.7)(sugarss@4.0.1(postcss@8.5.3))(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0))': - dependencies: - '@vitest/spy': 3.1.3 - estree-walker: 3.0.3 - magic-string: 0.30.17 - optionalDependencies: - vite: 6.3.5(@types/node@22.15.0)(jiti@1.21.7)(sugarss@4.0.1(postcss@8.5.3))(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) - '@vitest/mocker@3.1.3(vite@6.3.5(@types/node@22.15.0)(jiti@1.21.7)(sugarss@4.0.1(postcss@8.5.6))(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0))': dependencies: '@vitest/spy': 3.1.3 @@ -15094,7 +15177,7 @@ snapshots: sirv: 3.0.1 tinyglobby: 0.2.13 tinyrainbow: 2.0.0 - vitest: 3.1.3(@types/debug@4.1.12)(@types/node@22.15.0)(@vitest/ui@3.1.3)(jiti@1.21.7)(jsdom@26.1.0)(sugarss@4.0.1(postcss@8.5.3))(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) + vitest: 3.1.3(@types/debug@4.1.12)(@types/node@22.15.0)(@vitest/ui@3.1.3)(jiti@1.21.7)(jsdom@26.1.0)(sugarss@4.0.1(postcss@8.5.6))(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) '@vitest/utils@3.1.3': dependencies: @@ -15410,14 +15493,14 @@ snapshots: attr-accept@2.2.5: {} - autoprefixer@10.4.21(postcss@8.5.3): + autoprefixer@10.4.21(postcss@8.5.6): dependencies: browserslist: 4.24.5 caniuse-lite: 1.0.30001718 fraction.js: 4.3.7 normalize-range: 0.1.2 picocolors: 1.1.1 - postcss: 8.5.3 + postcss: 8.5.6 postcss-value-parser: 4.2.0 available-typed-arrays@1.0.7: @@ -19843,6 +19926,41 @@ snapshots: estree-walker: 3.0.3 is-reference: 3.0.3 + pg-cloudflare@1.2.7: + optional: true + + pg-connection-string@2.9.1: {} + + pg-int8@1.0.1: {} + + pg-pool@3.10.1(pg@8.16.3): + dependencies: + pg: 8.16.3 + + pg-protocol@1.10.3: {} + + pg-types@2.2.0: + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.0 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + + pg@8.16.3: + dependencies: + pg-connection-string: 2.9.1 + pg-pool: 3.10.1(pg@8.16.3) + pg-protocol: 1.10.3 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.2.7 + + pgpass@1.0.5: + dependencies: + split2: 4.2.0 + picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -19912,11 +20030,6 @@ snapshots: read-cache: 1.0.0 resolve: 1.22.10 - postcss-js@4.0.1(postcss@8.5.3): - dependencies: - camelcase-css: 2.0.1 - postcss: 8.5.3 - postcss-js@4.0.1(postcss@8.5.6): dependencies: camelcase-css: 2.0.1 @@ -19978,13 +20091,13 @@ snapshots: postcss: 8.5.6 postcss-selector-parser: 7.1.0 - postcss-mixins@9.0.4(postcss@8.5.3): + postcss-mixins@9.0.4(postcss@8.5.6): dependencies: fast-glob: 3.3.3 - postcss: 8.5.3 - postcss-js: 4.0.1(postcss@8.5.3) - postcss-simple-vars: 7.0.1(postcss@8.5.3) - sugarss: 4.0.1(postcss@8.5.3) + postcss: 8.5.6 + postcss-js: 4.0.1(postcss@8.5.6) + postcss-simple-vars: 7.0.1(postcss@8.5.6) + sugarss: 4.0.1(postcss@8.5.6) postcss-modules-extract-imports@3.1.0(postcss@8.5.6): dependencies: @@ -20019,11 +20132,6 @@ snapshots: postcss-modules-values: 4.0.0(postcss@8.5.6) string-hash: 1.1.3 - postcss-nested@6.2.0(postcss@8.5.3): - dependencies: - postcss: 8.5.3 - postcss-selector-parser: 6.1.2 - postcss-nested@6.2.0(postcss@8.5.6): dependencies: postcss: 8.5.6 @@ -20080,11 +20188,11 @@ snapshots: postcss: 8.5.6 postcss-value-parser: 4.2.0 - postcss-preset-mantine@1.17.0(postcss@8.5.3): + postcss-preset-mantine@1.17.0(postcss@8.5.6): dependencies: - postcss: 8.5.3 - postcss-mixins: 9.0.4(postcss@8.5.3) - postcss-nested: 6.2.0(postcss@8.5.3) + postcss: 8.5.6 + postcss-mixins: 9.0.4(postcss@8.5.6) + postcss-nested: 6.2.0(postcss@8.5.6) postcss-reduce-initial@7.0.3(postcss@8.5.6): dependencies: @@ -20112,9 +20220,9 @@ snapshots: cssesc: 3.0.0 util-deprecate: 1.0.2 - postcss-simple-vars@7.0.1(postcss@8.5.3): + postcss-simple-vars@7.0.1(postcss@8.5.6): dependencies: - postcss: 8.5.3 + postcss: 8.5.6 postcss-svgo@7.0.2(postcss@8.5.6): dependencies: @@ -20135,18 +20243,22 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - postcss@8.5.3: - dependencies: - nanoid: 3.3.11 - picocolors: 1.1.1 - source-map-js: 1.2.1 - postcss@8.5.6: dependencies: nanoid: 3.3.11 picocolors: 1.1.1 source-map-js: 1.2.1 + postgres-array@2.0.0: {} + + postgres-bytea@1.0.0: {} + + postgres-date@1.0.7: {} + + postgres-interval@1.2.0: + dependencies: + xtend: 4.0.2 + preact-render-to-string@5.2.6(preact@10.26.6): dependencies: preact: 10.26.6 @@ -21138,6 +21250,8 @@ snapshots: spawn-command@0.0.2: {} + split2@4.2.0: {} + sprintf-js@1.0.3: {} sprintf-js@1.1.3: {} @@ -21341,14 +21455,9 @@ snapshots: pirates: 4.0.7 ts-interface-checker: 0.1.13 - sugarss@4.0.1(postcss@8.5.3): - dependencies: - postcss: 8.5.3 - sugarss@4.0.1(postcss@8.5.6): dependencies: postcss: 8.5.6 - optional: true supabase@1.226.4: dependencies: @@ -22019,6 +22128,8 @@ snapshots: util-deprecate@1.0.2: {} + uuid@11.1.0: {} + uuid@3.4.0: {} uuid@8.3.2: {} @@ -22076,27 +22187,6 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.2 - vite-node@3.1.3(@types/node@22.15.0)(jiti@1.21.7)(sugarss@4.0.1(postcss@8.5.3))(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0): - dependencies: - cac: 6.7.14 - debug: 4.4.1 - es-module-lexer: 1.7.0 - pathe: 2.0.3 - vite: 6.3.5(@types/node@22.15.0)(jiti@1.21.7)(sugarss@4.0.1(postcss@8.5.3))(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) - transitivePeerDependencies: - - '@types/node' - - jiti - - less - - lightningcss - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - tsx - - yaml - vite-node@3.1.3(@types/node@22.15.0)(jiti@1.21.7)(sugarss@4.0.1(postcss@8.5.6))(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0): dependencies: cac: 6.7.14 @@ -22118,34 +22208,17 @@ snapshots: - tsx - yaml - vite-tsconfig-paths@5.1.4(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.0)(jiti@1.21.7)(sugarss@4.0.1(postcss@8.5.3))(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0)): + vite-tsconfig-paths@5.1.4(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.0)(jiti@1.21.7)(sugarss@4.0.1(postcss@8.5.6))(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0)): dependencies: debug: 4.4.1 globrex: 0.1.2 tsconfck: 3.1.6(typescript@5.8.3) optionalDependencies: - vite: 6.3.5(@types/node@22.15.0)(jiti@1.21.7)(sugarss@4.0.1(postcss@8.5.3))(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.5(@types/node@22.15.0)(jiti@1.21.7)(sugarss@4.0.1(postcss@8.5.6))(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) transitivePeerDependencies: - supports-color - typescript - vite@6.3.5(@types/node@22.15.0)(jiti@1.21.7)(sugarss@4.0.1(postcss@8.5.3))(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0): - dependencies: - esbuild: 0.25.4 - fdir: 6.4.4(picomatch@4.0.2) - picomatch: 4.0.2 - postcss: 8.5.6 - rollup: 4.40.2 - tinyglobby: 0.2.13 - optionalDependencies: - '@types/node': 22.15.0 - fsevents: 2.3.3 - jiti: 1.21.7 - sugarss: 4.0.1(postcss@8.5.3) - terser: 5.39.2 - tsx: 4.19.4 - yaml: 2.8.0 - vite@6.3.5(@types/node@22.15.0)(jiti@1.21.7)(sugarss@4.0.1(postcss@8.5.6))(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0): dependencies: esbuild: 0.25.4 @@ -22163,48 +22236,6 @@ snapshots: tsx: 4.19.4 yaml: 2.8.0 - vitest@3.1.3(@types/debug@4.1.12)(@types/node@22.15.0)(@vitest/ui@3.1.3)(jiti@1.21.7)(jsdom@26.1.0)(sugarss@4.0.1(postcss@8.5.3))(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0): - dependencies: - '@vitest/expect': 3.1.3 - '@vitest/mocker': 3.1.3(vite@6.3.5(@types/node@22.15.0)(jiti@1.21.7)(sugarss@4.0.1(postcss@8.5.3))(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0)) - '@vitest/pretty-format': 3.1.3 - '@vitest/runner': 3.1.3 - '@vitest/snapshot': 3.1.3 - '@vitest/spy': 3.1.3 - '@vitest/utils': 3.1.3 - chai: 5.2.0 - debug: 4.4.1 - expect-type: 1.2.1 - magic-string: 0.30.17 - pathe: 2.0.3 - std-env: 3.9.0 - tinybench: 2.9.0 - tinyexec: 0.3.2 - tinyglobby: 0.2.13 - tinypool: 1.0.2 - tinyrainbow: 2.0.0 - vite: 6.3.5(@types/node@22.15.0)(jiti@1.21.7)(sugarss@4.0.1(postcss@8.5.3))(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) - vite-node: 3.1.3(@types/node@22.15.0)(jiti@1.21.7)(sugarss@4.0.1(postcss@8.5.3))(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) - why-is-node-running: 2.3.0 - optionalDependencies: - '@types/debug': 4.1.12 - '@types/node': 22.15.0 - '@vitest/ui': 3.1.3(vitest@3.1.3) - jsdom: 26.1.0 - transitivePeerDependencies: - - jiti - - less - - lightningcss - - msw - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - tsx - - yaml - vitest@3.1.3(@types/debug@4.1.12)(@types/node@22.15.0)(@vitest/ui@3.1.3)(jiti@1.21.7)(jsdom@26.1.0)(sugarss@4.0.1(postcss@8.5.6))(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0): dependencies: '@vitest/expect': 3.1.3 @@ -22447,6 +22478,8 @@ snapshots: xmlchars@2.2.0: {} + xtend@4.0.2: {} + y18n@5.0.8: {} yallist@3.1.1: {}