From 962aa542b0f139c5efa031afcbc9936ac73ce743 Mon Sep 17 00:00:00 2001
From: ErikaKK <491649804@qq.com>
Date: Sun, 1 Feb 2026 22:23:46 +0800
Subject: [PATCH 1/7] add export button for project and payment table
---
src/app/dashboard/admin/@tools/export.tsx | 30 +++++------------------
src/app/profile/[id]/profile.tsx | 2 +-
2 files changed, 7 insertions(+), 25 deletions(-)
diff --git a/src/app/dashboard/admin/@tools/export.tsx b/src/app/dashboard/admin/@tools/export.tsx
index 2fc42097f..b264d2143 100644
--- a/src/app/dashboard/admin/@tools/export.tsx
+++ b/src/app/dashboard/admin/@tools/export.tsx
@@ -25,35 +25,17 @@ export default function ExportButton({ data, label }: ExportButtonProps) {
return
}
- const headers = Object.keys(data[0] as Record)
- const escapeCSV = (value: unknown): string => {
+ const headers = Object.keys(data[0] as Record).join(",")
+ const escapeCSV = (value: unknown) => {
if (value == null) return ""
-
- let str: string
- if (typeof value === "object") {
- if (value instanceof Date) {
- str = value.toISOString()
- } else {
- // Stringify JSON objects/arrays
- str = JSON.stringify(value)
- }
- } else {
- str = String(value)
- }
-
- // Escape CSV: wrap in quotes if contains comma, quote, or newline
- // and escape internal quotes by doubling them
- if (/[",\n\r]/.test(str)) {
+ const str = String(value)
+ if (/[",\n]/.test(str)) {
return `"${str.replace(/"/g, '""')}"`
}
return str
}
-
- const rows = data
- .map((row) => headers.map((key) => escapeCSV((row as Record)[key])).join(","))
- .join("\n")
-
- const csv = headers.join(",") + "\n" + rows
+ const rows = data.map((row) => Object.values(row).map(escapeCSV).join(",")).join("\n")
+ const csv = headers + "\n" + rows
const blob = new Blob([csv], { type: "text/csv" })
const url = window.URL.createObjectURL(blob)
diff --git a/src/app/profile/[id]/profile.tsx b/src/app/profile/[id]/profile.tsx
index 84c77ca25..4053f0bc4 100644
--- a/src/app/profile/[id]/profile.tsx
+++ b/src/app/profile/[id]/profile.tsx
@@ -146,7 +146,7 @@ const ProfilePage = ({ id, currentUser }: ProfilePageProps) => {
user.membership_expiry && (
Your membership will expire on{" "}
-
+
{format(new Date(String(user.membership_expiry)), "dd MMMM yyyy")}
From 992c2d6971964672aaac493b801fc9b3b4cebad0 Mon Sep 17 00:00:00 2001
From: ErikaKK <491649804@qq.com>
Date: Wed, 4 Feb 2026 23:28:20 +0800
Subject: [PATCH 2/7] migration
---
.env.example | 8 +-
drizzle.config.ts | 4 +-
package.json | 2 +
pnpm-lock.yaml | 81 +++++++++++++++----
src/app/dashboard/admin/@tools/export.tsx | 4 +
src/app/dashboard/admin/@users/table.tsx | 4 +-
src/app/dashboard/admin/layout.tsx | 15 ++--
src/app/profile/[id]/profile.tsx | 2 +-
src/middleware.ts | 33 ++------
.../routers/admin/analytics/get-payments.ts | 2 +-
.../admin/analytics/get-users-per-day.ts | 4 +-
.../api/routers/admin/projects/get-all.ts | 2 +-
.../routers/admin/projects/get-projects.ts | 4 +-
src/server/api/routers/admin/users/get-all.ts | 2 +-
.../api/routers/admin/users/update-email.ts | 1 -
.../routers/projects/get-application-open.ts | 2 +-
.../api/routers/projects/get-projects.ts | 10 +--
src/server/api/routers/projects/get-public.ts | 2 +-
src/server/db/drizzle.ts | 9 +++
src/server/db/index.ts | 10 ++-
src/server/db/schema.ts | 12 +--
21 files changed, 130 insertions(+), 83 deletions(-)
create mode 100644 src/server/db/drizzle.ts
diff --git a/.env.example b/.env.example
index 1fefa2a95..72d084e1f 100644
--- a/.env.example
+++ b/.env.example
@@ -37,4 +37,10 @@ UPSTASH_REDIS_REST_TOKEN=upstash-redis-token
NEXT_PUBLIC_MAPBOX_API=mapbox-key
# Cron (openssl rand -hex 32)
-CRON_SECRET=some-random-secret
\ No newline at end of file
+CRON_SECRET=some-random-secret
+
+# Supabase
+SUPABASE_URL=
+SUPABASE_PUBLISHABLE_DEFAULT_KEY=
+SUPABASE_ANON_KEY=
+SUPABASE_SERVICE_ROLE_KEY=
\ No newline at end of file
diff --git a/drizzle.config.ts b/drizzle.config.ts
index 7a6c2ad59..1a0565012 100644
--- a/drizzle.config.ts
+++ b/drizzle.config.ts
@@ -1,7 +1,5 @@
import { defineConfig } from "drizzle-kit"
-import { getXataClient } from "~/server/db/xata"
-
export default defineConfig({
out: "./src/server/db/migrations",
schema: "./src/server/db/schema.ts",
@@ -9,6 +7,6 @@ export default defineConfig({
dialect: "postgresql",
tablesFilter: ["cfc-website_*"],
dbCredentials: {
- url: getXataClient().sql.connectionString,
+ url: process.env.SUPABASE_DB_URL!,
},
})
diff --git a/package.json b/package.json
index 18ccda0e3..ae5c3183d 100644
--- a/package.json
+++ b/package.json
@@ -71,6 +71,7 @@
"next": "14.2.26",
"next-themes": "0.3.0",
"pg": "^8.14.1",
+ "pg-native": "^3.5.2",
"react": "18.3.1",
"react-day-picker": "8.10.0",
"react-dom": "18.3.1",
@@ -96,6 +97,7 @@
"@types/eslint": "8.56.6",
"@types/mapbox-gl": "3.1.0",
"@types/node": "20.12.2",
+ "@types/pg": "^8.16.0",
"@types/react": "18.3.3",
"@types/react-dom": "18.2.23",
"@typescript-eslint/eslint-plugin": "7.4.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index cfd4796d3..5a4e40dd9 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -141,7 +141,7 @@ importers:
version: 0.30.1(typescript@5.4.3)
'@xata.io/drizzle':
specifier: 0.0.24
- version: 0.0.24(drizzle-orm@0.41.0(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(@xata.io/client@0.30.1(typescript@5.4.3))(gel@2.2.0)(pg@8.16.3))(typescript@5.4.3)
+ version: 0.0.24(drizzle-orm@0.41.0(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(@xata.io/client@0.30.1(typescript@5.4.3))(gel@2.2.0)(pg@8.16.3(pg-native@3.5.2)))(typescript@5.4.3)
class-variance-authority:
specifier: 0.7.0
version: 0.7.0
@@ -156,10 +156,10 @@ importers:
version: 3.6.0
drizzle-orm:
specifier: 0.41.0
- version: 0.41.0(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(@xata.io/client@0.30.1(typescript@5.4.3))(gel@2.2.0)(pg@8.16.3)
+ version: 0.41.0(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(@xata.io/client@0.30.1(typescript@5.4.3))(gel@2.2.0)(pg@8.16.3(pg-native@3.5.2))
drizzle-zod:
specifier: ^0.7.1
- version: 0.7.1(drizzle-orm@0.41.0(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(@xata.io/client@0.30.1(typescript@5.4.3))(gel@2.2.0)(pg@8.16.3))(zod@3.22.4)
+ version: 0.7.1(drizzle-orm@0.41.0(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(@xata.io/client@0.30.1(typescript@5.4.3))(gel@2.2.0)(pg@8.16.3(pg-native@3.5.2)))(zod@3.22.4)
framer-motion:
specifier: 11.0.24
version: 11.0.24(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -174,7 +174,10 @@ importers:
version: 0.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
pg:
specifier: ^8.14.1
- version: 8.16.3
+ version: 8.16.3(pg-native@3.5.2)
+ pg-native:
+ specifier: ^3.5.2
+ version: 3.5.2
react:
specifier: 18.3.1
version: 18.3.1
@@ -245,6 +248,9 @@ importers:
'@types/node':
specifier: 20.12.2
version: 20.12.2
+ '@types/pg':
+ specifier: ^8.16.0
+ version: 8.16.0
'@types/react':
specifier: 18.3.3
version: 18.3.3
@@ -2808,6 +2814,9 @@ packages:
'@types/pg-pool@2.0.6':
resolution: {integrity: sha512-TaAUE5rq2VQYxab5Ts7WZhKNmuN78Q6PiFonTDdpbx8a1H0M1vhy3rhiMjl+e2iHmogyMw7jZF4FrE6eJUy5HQ==}
+ '@types/pg@8.16.0':
+ resolution: {integrity: sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ==}
+
'@types/pg@8.6.1':
resolution: {integrity: sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w==}
@@ -3585,6 +3594,9 @@ packages:
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
engines: {node: '>=8'}
+ bindings@1.5.0:
+ resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==}
+
bn.js@5.2.2:
resolution: {integrity: sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==}
@@ -4515,6 +4527,9 @@ packages:
resolution: {integrity: sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==}
engines: {node: '>= 12'}
+ file-uri-to-path@1.0.0:
+ resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==}
+
fill-range@7.1.1:
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
engines: {node: '>=8'}
@@ -5179,6 +5194,9 @@ packages:
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
engines: {node: '>= 0.8.0'}
+ libpq@1.8.15:
+ resolution: {integrity: sha512-4lSWmly2Nsj3LaTxxtFmJWuP3Kx+0hYHEd+aNrcXEWT0nKWaPd9/QZPiMkkC680zeALFGHQdQWjBvnilL+vgWA==}
+
lighthouse-logger@1.4.2:
resolution: {integrity: sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g==}
@@ -5439,6 +5457,9 @@ packages:
mz@2.7.0:
resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
+ nan@2.22.2:
+ resolution: {integrity: sha512-DANghxFkS1plDdRsX0X9pm0Z6SJNN6gBdtXfanwoZ8hooC5gosGFSBGRYHUVPz1asKA/kMRqDRdHrluZ61SpBQ==}
+
nanoid@3.3.11:
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
@@ -5692,6 +5713,9 @@ packages:
resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==}
engines: {node: '>=4.0.0'}
+ pg-native@3.5.2:
+ resolution: {integrity: sha512-3oi+KVil86Vngo4H0IlhBaYSJWdcu8t2f1Y4TkQoQi5oZ9bNeYECGqW3oSGx69mjSZYHoC3h+3jYtqzRgndn5A==}
+
pg-pool@3.10.1:
resolution: {integrity: sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==}
peerDependencies:
@@ -9755,7 +9779,13 @@ snapshots:
'@types/pg-pool@2.0.6':
dependencies:
- '@types/pg': 8.6.1
+ '@types/pg': 8.16.0
+
+ '@types/pg@8.16.0':
+ dependencies:
+ '@types/node': 20.12.2
+ pg-protocol: 1.10.3
+ pg-types: 2.2.0
'@types/pg@8.6.1':
dependencies:
@@ -10481,10 +10511,10 @@ snapshots:
dependencies:
typescript: 5.4.3
- '@xata.io/drizzle@0.0.24(drizzle-orm@0.41.0(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(@xata.io/client@0.30.1(typescript@5.4.3))(gel@2.2.0)(pg@8.16.3))(typescript@5.4.3)':
+ '@xata.io/drizzle@0.0.24(drizzle-orm@0.41.0(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(@xata.io/client@0.30.1(typescript@5.4.3))(gel@2.2.0)(pg@8.16.3(pg-native@3.5.2)))(typescript@5.4.3)':
dependencies:
'@xata.io/client': 0.30.1(typescript@5.4.3)
- drizzle-orm: 0.41.0(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(@xata.io/client@0.30.1(typescript@5.4.3))(gel@2.2.0)(pg@8.16.3)
+ drizzle-orm: 0.41.0(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(@xata.io/client@0.30.1(typescript@5.4.3))(gel@2.2.0)(pg@8.16.3(pg-native@3.5.2))
transitivePeerDependencies:
- typescript
@@ -10799,6 +10829,10 @@ snapshots:
binary-extensions@2.3.0: {}
+ bindings@1.5.0:
+ dependencies:
+ file-uri-to-path: 1.0.0
+
bn.js@5.2.2: {}
borsh@0.7.0:
@@ -11287,17 +11321,17 @@ snapshots:
transitivePeerDependencies:
- supports-color
- drizzle-orm@0.41.0(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(@xata.io/client@0.30.1(typescript@5.4.3))(gel@2.2.0)(pg@8.16.3):
+ drizzle-orm@0.41.0(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(@xata.io/client@0.30.1(typescript@5.4.3))(gel@2.2.0)(pg@8.16.3(pg-native@3.5.2)):
optionalDependencies:
'@opentelemetry/api': 1.9.0
- '@types/pg': 8.6.1
+ '@types/pg': 8.16.0
'@xata.io/client': 0.30.1(typescript@5.4.3)
gel: 2.2.0
- pg: 8.16.3
+ pg: 8.16.3(pg-native@3.5.2)
- drizzle-zod@0.7.1(drizzle-orm@0.41.0(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(@xata.io/client@0.30.1(typescript@5.4.3))(gel@2.2.0)(pg@8.16.3))(zod@3.22.4):
+ drizzle-zod@0.7.1(drizzle-orm@0.41.0(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(@xata.io/client@0.30.1(typescript@5.4.3))(gel@2.2.0)(pg@8.16.3(pg-native@3.5.2)))(zod@3.22.4):
dependencies:
- drizzle-orm: 0.41.0(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(@xata.io/client@0.30.1(typescript@5.4.3))(gel@2.2.0)(pg@8.16.3)
+ drizzle-orm: 0.41.0(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(@xata.io/client@0.30.1(typescript@5.4.3))(gel@2.2.0)(pg@8.16.3(pg-native@3.5.2))
zod: 3.22.4
dunder-proto@1.0.1:
@@ -11788,6 +11822,8 @@ snapshots:
dependencies:
tslib: 2.8.1
+ file-uri-to-path@1.0.0: {}
+
fill-range@7.1.1:
dependencies:
to-regex-range: 5.0.1
@@ -12525,6 +12561,11 @@ snapshots:
prelude-ls: 1.2.1
type-check: 0.4.0
+ libpq@1.8.15:
+ dependencies:
+ bindings: 1.5.0
+ nan: 2.22.2
+
lighthouse-logger@1.4.2:
dependencies:
debug: 2.6.9
@@ -12918,6 +12959,8 @@ snapshots:
object-assign: 4.1.1
thenify-all: 1.6.0
+ nan@2.22.2: {}
+
nanoid@3.3.11: {}
napi-postinstall@0.3.4: {}
@@ -13176,9 +13219,14 @@ snapshots:
pg-int8@1.0.1: {}
- pg-pool@3.10.1(pg@8.16.3):
+ pg-native@3.5.2:
+ dependencies:
+ libpq: 1.8.15
+ pg-types: 2.2.0
+
+ pg-pool@3.10.1(pg@8.16.3(pg-native@3.5.2)):
dependencies:
- pg: 8.16.3
+ pg: 8.16.3(pg-native@3.5.2)
pg-protocol@1.10.3: {}
@@ -13190,15 +13238,16 @@ snapshots:
postgres-date: 1.0.7
postgres-interval: 1.2.0
- pg@8.16.3:
+ pg@8.16.3(pg-native@3.5.2):
dependencies:
pg-connection-string: 2.9.1
- pg-pool: 3.10.1(pg@8.16.3)
+ pg-pool: 3.10.1(pg@8.16.3(pg-native@3.5.2))
pg-protocol: 1.10.3
pg-types: 2.2.0
pgpass: 1.0.5
optionalDependencies:
pg-cloudflare: 1.2.7
+ pg-native: 3.5.2
pgpass@1.0.5:
dependencies:
diff --git a/src/app/dashboard/admin/@tools/export.tsx b/src/app/dashboard/admin/@tools/export.tsx
index b264d2143..2711b55be 100644
--- a/src/app/dashboard/admin/@tools/export.tsx
+++ b/src/app/dashboard/admin/@tools/export.tsx
@@ -28,6 +28,10 @@ export default function ExportButton({ data, label }: ExportButtonProps) {
const headers = Object.keys(data[0] as Record).join(",")
const escapeCSV = (value: unknown) => {
if (value == null) return ""
+ if (value instanceof Date) return value.toISOString()
+ if (typeof value === "string" && !isNaN(Date.parse(value))) {
+ return new Date(value).toISOString()
+ }
const str = String(value)
if (/[",\n]/.test(str)) {
return `"${str.replace(/"/g, '""')}"`
diff --git a/src/app/dashboard/admin/@users/table.tsx b/src/app/dashboard/admin/@users/table.tsx
index 5adb01847..c0844a6a2 100644
--- a/src/app/dashboard/admin/@users/table.tsx
+++ b/src/app/dashboard/admin/@users/table.tsx
@@ -66,7 +66,7 @@ import { api } from "~/trpc/react"
import ExportButton from "../@tools/export"
import AddUserForm from "./form"
-type DisplayColumn = Omit
+type DisplayColumn = Omit
export interface TableProps {
data: Array
@@ -167,7 +167,7 @@ const columns = (updateRole: ({ id, role }: UpdateUserRoleFunctionProps) => void
id: "Date joined",
header: "Date joined",
cell: (cell) => {cell.getValue()},
- accessorFn: (user) => format(user.createdAt, "Pp", { locale: enAU }),
+ accessorFn: (user) => format(user.created_at, "Pp", { locale: enAU }),
},
{
id: "Membership expiry",
diff --git a/src/app/dashboard/admin/layout.tsx b/src/app/dashboard/admin/layout.tsx
index b2de060aa..87c29705b 100644
--- a/src/app/dashboard/admin/layout.tsx
+++ b/src/app/dashboard/admin/layout.tsx
@@ -1,14 +1,10 @@
-"use client"
-
import * as React from "react"
-import type { ImperativePanelHandle } from "react-resizable-panels"
-import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "~/components/ui/resizable"
-import { Separator } from "~/components/ui/separator"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs"
+import NotFound from "~/app/not-found"
import type { PropsWithChildren } from "~/lib/types"
-import { cn } from "~/lib/utils"
+import { api } from "~/trpc/server"
interface AdminDashLayoutProps extends PropsWithChildren {
users: React.ReactNode
@@ -18,7 +14,12 @@ interface AdminDashLayoutProps extends PropsWithChildren {
tools: React.ReactNode
}
-const Layout = ({ children, ...props }: AdminDashLayoutProps) => {
+const Layout = async ({ children, ...props }: AdminDashLayoutProps) => {
+ const user = await api.users.getCurrent.query()
+ if (!["admin", "committee"].includes(user?.role ?? "")) {
+ return
+ }
+
const sidebarItems = [
{ text: "Users", icon: "group", component: props.users },
{ text: "Projects", icon: "devices", component: props.projects },
diff --git a/src/app/profile/[id]/profile.tsx b/src/app/profile/[id]/profile.tsx
index 4053f0bc4..84c77ca25 100644
--- a/src/app/profile/[id]/profile.tsx
+++ b/src/app/profile/[id]/profile.tsx
@@ -146,7 +146,7 @@ const ProfilePage = ({ id, currentUser }: ProfilePageProps) => {
user.membership_expiry && (
Your membership will expire on{" "}
-
+
{format(new Date(String(user.membership_expiry)), "dd MMMM yyyy")}
diff --git a/src/middleware.ts b/src/middleware.ts
index d5b7acdfb..d1f311d9d 100644
--- a/src/middleware.ts
+++ b/src/middleware.ts
@@ -1,36 +1,17 @@
-import { auth, clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server"
-import { eq } from "drizzle-orm"
+import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server"
-import { db } from "./server/db"
-import { User } from "./server/db/schema"
-
-const adminRoles = ["admin", "committee"]
-
-const isAdminPage = createRouteMatcher(["/dashboard/admin(.*)"])
const isProtectedPage = createRouteMatcher(["/dashboard(.*)", "/profile/settings(.*)"])
const isAuthPage = createRouteMatcher(["/join(.*)"])
export default clerkMiddleware(async (auth, req) => {
- const session = await auth()
- const clerkId = session.userId
-
- if (isAdminPage(req) && clerkId) {
- const user = await db.query.User.findFirst({
- where: eq(User.clerk_id, clerkId),
- })
-
- if (!adminRoles.includes(user?.role ?? "")) {
- // non-existent clerk role so we go to 404 page cleanly
- await auth.protect({
- role: "lmfaooo",
- })
- }
- }
-
+ // Only require authentication for protected pages
if (isProtectedPage(req)) {
await auth.protect()
}
+ // Redirect authenticated users away from /join
+ const session = await auth()
+ const clerkId = session.userId
if (isAuthPage(req) && clerkId) {
return Response.redirect(new URL("/dashboard", req.url))
}
@@ -39,11 +20,7 @@ export default clerkMiddleware(async (auth, req) => {
export const config = {
matcher: [
"/",
- // Skip Next.js internals and all static files, unless found in search params
"/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)",
- // // Always run for API routes
- // "/(api|trpc)(.*)",
- // Ignore trpc routes because they are handled by the server
"/api(.*)",
],
}
diff --git a/src/server/api/routers/admin/analytics/get-payments.ts b/src/server/api/routers/admin/analytics/get-payments.ts
index ee2ce741e..496481f77 100644
--- a/src/server/api/routers/admin/analytics/get-payments.ts
+++ b/src/server/api/routers/admin/analytics/get-payments.ts
@@ -5,7 +5,7 @@ import { Payment } from "~/server/db/schema"
export const getAllPayments = adminProcedure.query(async ({ ctx }) => {
const paymentList = await ctx.db.query.Payment.findMany({
- orderBy: [desc(Payment.createdAt), desc(Payment.id)],
+ orderBy: [desc(Payment.created_at), desc(Payment.id)],
})
return paymentList
diff --git a/src/server/api/routers/admin/analytics/get-users-per-day.ts b/src/server/api/routers/admin/analytics/get-users-per-day.ts
index b2fd49b3c..03e978aff 100644
--- a/src/server/api/routers/admin/analytics/get-users-per-day.ts
+++ b/src/server/api/routers/admin/analytics/get-users-per-day.ts
@@ -20,7 +20,7 @@ export const getUsersPerDay = adminProcedure.query(async ({ ctx }) => {
count: sql`count(*)`.mapWith(Number),
})
.from(User)
- .where(between(User.createdAt, twoMonths, new Date()))
+ .where(between(User.created_at, twoMonths, new Date()))
.groupBy(
sql`extract(day from created_at)`,
sql`extract(month from created_at)`,
@@ -39,7 +39,7 @@ export const getUsersPerDay = adminProcedure.query(async ({ ctx }) => {
count: sql`count(*)`.mapWith(Number),
})
.from(User)
- .where(and(isNotNull(User.role), between(User.createdAt, twoMonths, new Date())))
+ .where(and(isNotNull(User.role), between(User.created_at, twoMonths, new Date())))
.groupBy(
sql`extract(day from created_at)`,
sql`extract(month from created_at)`,
diff --git a/src/server/api/routers/admin/projects/get-all.ts b/src/server/api/routers/admin/projects/get-all.ts
index 6ccbd0478..94324cb01 100644
--- a/src/server/api/routers/admin/projects/get-all.ts
+++ b/src/server/api/routers/admin/projects/get-all.ts
@@ -5,7 +5,7 @@ import { Project } from "~/server/db/schema"
export const getAllProjects = adminProcedure.query(async ({ ctx }) => {
const projectList = await ctx.db.query.Project.findMany({
- orderBy: [desc(Project.createdAt), desc(Project.id)],
+ orderBy: [desc(Project.created_at), desc(Project.id)],
})
return projectList
diff --git a/src/server/api/routers/admin/projects/get-projects.ts b/src/server/api/routers/admin/projects/get-projects.ts
index ae2ea89c1..bc1739580 100644
--- a/src/server/api/routers/admin/projects/get-projects.ts
+++ b/src/server/api/routers/admin/projects/get-projects.ts
@@ -35,7 +35,7 @@ export const getProjects = adminProcedure
},
where: name ? eq(Project.name, name) : undefined,
- orderBy: [desc(Project.createdAt), desc(Project.id)],
+ orderBy: [desc(Project.created_at), desc(Project.id)],
})
return projectList
@@ -71,7 +71,7 @@ export const getPublicProjects = adminProcedure
},
where: eq(Project.is_public, is_public),
- orderBy: [desc(Project.createdAt), desc(Project.id)],
+ orderBy: [desc(Project.created_at), desc(Project.id)],
})
return projectList
diff --git a/src/server/api/routers/admin/users/get-all.ts b/src/server/api/routers/admin/users/get-all.ts
index f08e09616..7f9e3bece 100644
--- a/src/server/api/routers/admin/users/get-all.ts
+++ b/src/server/api/routers/admin/users/get-all.ts
@@ -5,7 +5,7 @@ import { User } from "~/server/db/schema"
export const getAll = adminProcedure.query(async ({ ctx }) => {
const userList = await ctx.db.query.User.findMany({
- orderBy: [desc(User.createdAt), desc(User.id)],
+ orderBy: [desc(User.created_at), desc(User.id)],
})
return userList
diff --git a/src/server/api/routers/admin/users/update-email.ts b/src/server/api/routers/admin/users/update-email.ts
index 3acf3cefa..c03151ad0 100644
--- a/src/server/api/routers/admin/users/update-email.ts
+++ b/src/server/api/routers/admin/users/update-email.ts
@@ -4,7 +4,6 @@ import { eq } from "drizzle-orm"
import { z } from "zod"
import { adminProcedure } from "~/server/api/trpc"
-import { db } from "~/server/db"
import { User } from "~/server/db/schema"
export const updateEmail = adminProcedure
diff --git a/src/server/api/routers/projects/get-application-open.ts b/src/server/api/routers/projects/get-application-open.ts
index 13786aa61..d11855904 100644
--- a/src/server/api/routers/projects/get-application-open.ts
+++ b/src/server/api/routers/projects/get-application-open.ts
@@ -7,7 +7,7 @@ import { Project } from "~/server/db/schema"
export const getApplicationOpen = protectedRatedProcedure(Ratelimit.fixedWindow(60, "30s")).query(async ({ ctx }) => {
const projectList = await ctx.db.query.Project.findMany({
where: (project, { eq }) => eq(project.is_application_open, true),
- orderBy: [desc(Project.createdAt), desc(Project.id)],
+ orderBy: [desc(Project.created_at), desc(Project.id)],
})
return projectList
diff --git a/src/server/api/routers/projects/get-projects.ts b/src/server/api/routers/projects/get-projects.ts
index 4687a6a22..06690923e 100644
--- a/src/server/api/routers/projects/get-projects.ts
+++ b/src/server/api/routers/projects/get-projects.ts
@@ -1,6 +1,6 @@
import { TRPCError } from "@trpc/server"
import { Ratelimit } from "@upstash/ratelimit"
-import { and, arrayContains, eq } from "drizzle-orm"
+import { and, arrayContains, eq, sql } from "drizzle-orm"
import { z } from "zod"
import { protectedRatedProcedure, publicRatedProcedure } from "~/server/api/trpc"
@@ -41,14 +41,14 @@ export const getProjectByName = publicRatedProcedure(Ratelimit.fixedWindow(60, "
export const getProjectByUser = protectedRatedProcedure(Ratelimit.fixedWindow(60, "30s"))
.input(z.object({ user: z.string(), isPublic: z.boolean().optional() }))
.query(async ({ input, ctx }) => {
- const conditions = []
+ let whereClause = sql`TRUE`
if (input.user) {
- conditions.push(arrayContains(Project.members, [input.user]))
+ whereClause = sql`${whereClause} AND ${Project.members} ILIKE '%${input.user}%'`
}
if (typeof input.isPublic === "boolean") {
- conditions.push(eq(Project.is_public, input.isPublic))
+ whereClause = sql`${whereClause} AND ${Project.is_public} = ${input.isPublic}`
}
const projectData = await ctx.db.query.Project.findMany({
@@ -70,7 +70,7 @@ export const getProjectByUser = protectedRatedProcedure(Ratelimit.fixedWindow(60
application_url: true,
is_public: true,
},
- where: conditions.length > 0 ? and(...conditions) : undefined,
+ where: whereClause,
})
return projectData
diff --git a/src/server/api/routers/projects/get-public.ts b/src/server/api/routers/projects/get-public.ts
index d1748e159..7175cc082 100644
--- a/src/server/api/routers/projects/get-public.ts
+++ b/src/server/api/routers/projects/get-public.ts
@@ -14,7 +14,7 @@ export const getPublic = publicRatedProcedure(Ratelimit.fixedWindow(60, "30s")).
},
where: (project, { eq }) => eq(project.is_public, true),
- orderBy: [desc(Project.createdAt), desc(Project.id)],
+ orderBy: [desc(Project.created_at), desc(Project.id)],
})
return projectList
diff --git a/src/server/db/drizzle.ts b/src/server/db/drizzle.ts
new file mode 100644
index 000000000..800c7ad14
--- /dev/null
+++ b/src/server/db/drizzle.ts
@@ -0,0 +1,9 @@
+// src/server/db/drizzle.ts
+import { drizzle } from "drizzle-orm/node-postgres"
+import { Pool } from "pg"
+
+const pool = new Pool({
+ connectionString: process.env.SUPABASE_DB_URL,
+})
+
+export const db = drizzle(pool)
diff --git a/src/server/db/index.ts b/src/server/db/index.ts
index 2a9dd6d97..d3646f385 100644
--- a/src/server/db/index.ts
+++ b/src/server/db/index.ts
@@ -1,8 +1,10 @@
-import { drizzle } from "drizzle-orm/xata-http"
+import { drizzle } from "drizzle-orm/node-postgres"
+import { Pool } from "pg"
import * as schema from "./schema"
-import { getXataClient } from "./xata"
-const xata = getXataClient()
+const pool = new Pool({
+ connectionString: process.env.SUPABASE_DB_URL,
+})
-export const db = drizzle(xata, { schema })
+export const db = drizzle(pool, { schema })
diff --git a/src/server/db/schema.ts b/src/server/db/schema.ts
index b4667f96d..73b096e40 100644
--- a/src/server/db/schema.ts
+++ b/src/server/db/schema.ts
@@ -37,10 +37,10 @@ export const User = pgTable(
role: roleEnum("role"),
square_customer_id: varchar("square_customer_id", { length: 32 }).unique().notNull(),
membership_expiry: timestamp("membership_expiry"),
- createdAt: timestamp("created_at")
+ created_at: timestamp("created_at")
.$default(() => new Date())
.notNull(),
- updatedAt: timestamp("updated_at").$onUpdate(() => new Date()),
+ updated_at: timestamp("updated_at").$onUpdate(() => new Date()),
// `reminder_pending` id a Flag used to send reminders for membership renewal bc Resend can only send 100 emails per day
reminder_pending: boolean("reminder_pending").default(false).notNull(),
},
@@ -62,10 +62,10 @@ export const Payment = pgTable(
label: varchar("label", { length: 256 }).notNull(),
event_id: varchar("event_id", { length: 32 }), // TODO: link when events are implemented
- createdAt: timestamp("created_at")
+ created_at: timestamp("created_at")
.$default(() => new Date())
.notNull(),
- updatedAt: timestamp("updated_at").$onUpdate(() => new Date()),
+ updated_at: timestamp("updated_at").$onUpdate(() => new Date()),
},
(payment) => [index("user_id_idx").on(payment.user_id), index("event_id_idx").on(payment.event_id)],
)
@@ -105,10 +105,10 @@ export const Project = pgTable(
is_application_open: boolean("is_application_open").default(false).notNull(), // whether the project is receiving applications or not
application_url: varchar("application_url", { length: 256 }), // link to the application form
is_public: boolean("is_public").default(false).notNull(), // means they are visible on projects page
- createdAt: timestamp("created_at")
+ created_at: timestamp("created_at")
.$default(() => new Date())
.notNull(),
- updatedAt: timestamp("updated_at").$onUpdate(() => new Date()),
+ updated_at: timestamp("updated_at").$onUpdate(() => new Date()),
},
(project) => [index("name_idx").on(project.name)],
)
From e79b6f335351662b12f129cf58869eb37d39e226 Mon Sep 17 00:00:00 2001
From: ErikaKK <491649804@qq.com>
Date: Fri, 6 Feb 2026 21:06:36 +0800
Subject: [PATCH 3/7] some fix and env var change
---
.env.example | 9 ++--
README.md | 17 +++++---
src/app/dashboard/(root)/page.tsx | 2 +-
src/app/dashboard/admin/@analytics/count.tsx | 7 ++--
src/app/dashboard/admin/@tools/tool-card.tsx | 42 +++++++++++++++++++
.../dashboard/admin/@tools/update-email.tsx | 32 +++-----------
src/app/profile/[id]/profile.tsx | 2 +-
src/components/payment/online/index.tsx | 4 +-
src/env.js | 6 +++
.../routers/admin/analytics/get-payments.ts | 2 +-
.../admin/analytics/get-project-count.ts | 13 ++++++
.../routers/admin/analytics/get-user-count.ts | 4 +-
.../api/routers/admin/analytics/index.ts | 2 +
.../routers/admin/projects/get-projects.ts | 2 +-
.../api/routers/projects/get-projects.ts | 3 +-
src/server/db/xata.ts | 33 ---------------
16 files changed, 97 insertions(+), 83 deletions(-)
create mode 100644 src/app/dashboard/admin/@tools/tool-card.tsx
create mode 100644 src/server/api/routers/admin/analytics/get-project-count.ts
delete mode 100644 src/server/db/xata.ts
diff --git a/.env.example b/.env.example
index 72d084e1f..c56055c86 100644
--- a/.env.example
+++ b/.env.example
@@ -39,8 +39,9 @@ NEXT_PUBLIC_MAPBOX_API=mapbox-key
# Cron (openssl rand -hex 32)
CRON_SECRET=some-random-secret
+# Resend
+RESEND_API_KEY=
+
# Supabase
-SUPABASE_URL=
-SUPABASE_PUBLISHABLE_DEFAULT_KEY=
-SUPABASE_ANON_KEY=
-SUPABASE_SERVICE_ROLE_KEY=
\ No newline at end of file
+SUPABASE_DB_URL=
+SUPABASE_ANON_KEY=
\ No newline at end of file
diff --git a/README.md b/README.md
index 36842d214..5c1d41fd3 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,8 @@ Bootstrapped using the create-t3-app using:
- [Drizzle ORM](https://orm.drizzle.team/docs/overview)
- [Clerk authentication](https://clerk.com/docs)
- [Vercel](https://vercel.com)
-- [Xata](https://app.xata.io)
+- [Supabase](https://supabase.com/)
+- Deprecated: [Xata lite](https://app.xata.io)
We use [pnpm](https://pnpm.io/) as the package manager for this project.
@@ -32,22 +33,26 @@ pnpm i -E
pnpm upgrade -L
# Database migration
-pnpm drizzle-kit pull
-pnpm drizzle-kit generate
-pnpm drizzle-kit migrate
+npx drizzle-kit generate
+npx drizzle-kit push
```
+### Notes
+
+- UTC time
+- When you make changes to the DB, please back up the data first.
+
## Planned Features
### Project Integration Enhancements
-- Profiles will soon showcase the projects that users have contributed to, highlighting their work and collaborations.
+- Profiles showcase the projects that users have contributed to, highlighting their work and collaborations.
- Applications to join our seasonal Summer and Winter projects will be directly accessible through the site for our members.
- Committee members and Administrators will gain the ability to upload project specifics directly to the platform.
### User Profile Enhancements
-- The platform will support multiple role assignments for users, offering tailored experiences based on their involvement and interests.
+- The platform supports multiple role assignments for users, offering tailored experiences based on their involvement and interests.
- Enhanced profile customization options will be introduced.
### Event Participation and Management
diff --git a/src/app/dashboard/(root)/page.tsx b/src/app/dashboard/(root)/page.tsx
index 358404c21..c3df3556e 100644
--- a/src/app/dashboard/(root)/page.tsx
+++ b/src/app/dashboard/(root)/page.tsx
@@ -13,7 +13,7 @@ export default async function Dashboard() {
Here you can see our upcoming projects and the projects you have participated in. You can apply for new
projects here if we link the application form.
-
diff --git a/src/app/dashboard/admin/@analytics/count.tsx b/src/app/dashboard/admin/@analytics/count.tsx
index 1c40161bf..3a9f7e002 100644
--- a/src/app/dashboard/admin/@analytics/count.tsx
+++ b/src/app/dashboard/admin/@analytics/count.tsx
@@ -1,5 +1,3 @@
-import Link from "next/link"
-
import { api } from "~/trpc/server"
interface CardProps {
@@ -25,13 +23,14 @@ const Card = (props: CardProps) => {
const Count = async () => {
const count = await api.admin.analytics.getUserCount.query()
+ const projectCount = await api.admin.analytics.getProjectCount.query()
return (
<>
-
-
+
+ {/* */}
>
)
}
diff --git a/src/app/dashboard/admin/@tools/tool-card.tsx b/src/app/dashboard/admin/@tools/tool-card.tsx
new file mode 100644
index 000000000..361bb762b
--- /dev/null
+++ b/src/app/dashboard/admin/@tools/tool-card.tsx
@@ -0,0 +1,42 @@
+import * as React from "react"
+
+import { Button } from "~/components/ui/button"
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "~/components/ui/dialog"
+
+type ToolCardProps = {
+ title: string
+ children: React.ReactNode
+}
+
+function ToolCard({ title, children }: ToolCardProps) {
+ return (
+
+
+
{title}
+
+
+
+
+
+ )
+}
+
+export default ToolCard
diff --git a/src/app/dashboard/admin/@tools/update-email.tsx b/src/app/dashboard/admin/@tools/update-email.tsx
index 02e91785b..e3d709e48 100644
--- a/src/app/dashboard/admin/@tools/update-email.tsx
+++ b/src/app/dashboard/admin/@tools/update-email.tsx
@@ -5,20 +5,14 @@ import { useForm } from "react-hook-form"
import { z } from "zod"
import { Button } from "~/components/ui/button"
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from "~/components/ui/dialog"
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "~/components/ui/form"
import { Input } from "~/components/ui/input"
import { toast } from "~/components/ui/use-toast"
import { api } from "~/trpc/react"
+import ToolCard from "./tool-card"
+
const formSchema = z.object({
userId: z.string(),
oldEmail: z.string().email(),
@@ -35,25 +29,9 @@ const defaultValues: FormSchema = {
const UpdateEmail = () => {
return (
-
-
-
Update Email
-
-
-
-
-
+
+
+
)
}
diff --git a/src/app/profile/[id]/profile.tsx b/src/app/profile/[id]/profile.tsx
index 84c77ca25..7ebe3b0b4 100644
--- a/src/app/profile/[id]/profile.tsx
+++ b/src/app/profile/[id]/profile.tsx
@@ -146,7 +146,7 @@ const ProfilePage = ({ id, currentUser }: ProfilePageProps) => {
user.membership_expiry && (
Your membership will expire on{" "}
-
+
{format(new Date(String(user.membership_expiry)), "dd MMMM yyyy")}
diff --git a/src/components/payment/online/index.tsx b/src/components/payment/online/index.tsx
index e5236644a..c372393ea 100644
--- a/src/components/payment/online/index.tsx
+++ b/src/components/payment/online/index.tsx
@@ -16,7 +16,7 @@ import { type RouterOutputs } from "~/trpc/shared"
const Card = dynamic(() => import("./card"), {
ssr: false,
- loading: () => ,
+ loading: () => ,
})
const GooglePay = dynamic(() => import("./google-pay"), {
ssr: false,
@@ -206,7 +206,7 @@ const OnlinePaymentForm = ({
) : (
-
+
)
}
diff --git a/src/env.js b/src/env.js
index 11507a5a1..fb77e45af 100644
--- a/src/env.js
+++ b/src/env.js
@@ -14,6 +14,9 @@ export const env = createEnv({
UPSTASH_REDIS_REST_URL: z.string().url(),
UPSTASH_REDIS_REST_TOKEN: z.string(),
CRON_SECRET: z.string(),
+ RESEND_API_KEY: z.string(),
+ SUPABASE_DB_URL: z.string().url(),
+ SUPABASE_ANON_KEY: z.string(),
},
/**
@@ -53,6 +56,9 @@ export const env = createEnv({
NEXT_PUBLIC_CLERK_SIGN_IN_URL: process.env.NEXT_PUBLIC_CLERK_SIGN_IN_URL,
NEXT_PUBLIC_CLERK_SIGN_UP_URL: process.env.NEXT_PUBLIC_CLERK_SIGN_UP_URL,
CRON_SECRET: process.env.CRON_SECRET,
+ RESEND_API_KEY: process.env.RESEND_API_KEY,
+ SUPABASE_DB_URL: process.env.SUPABASE_DB_URL,
+ SUPABASE_ANON_KEY: process.env.SUPABASE_ANON_KEY,
},
/**
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially
diff --git a/src/server/api/routers/admin/analytics/get-payments.ts b/src/server/api/routers/admin/analytics/get-payments.ts
index 496481f77..8a13cb45d 100644
--- a/src/server/api/routers/admin/analytics/get-payments.ts
+++ b/src/server/api/routers/admin/analytics/get-payments.ts
@@ -1,4 +1,4 @@
-import { desc } from "drizzle-orm"
+import { desc, sql } from "drizzle-orm"
import { adminProcedure } from "~/server/api/trpc"
import { Payment } from "~/server/db/schema"
diff --git a/src/server/api/routers/admin/analytics/get-project-count.ts b/src/server/api/routers/admin/analytics/get-project-count.ts
new file mode 100644
index 000000000..e6f9c1b80
--- /dev/null
+++ b/src/server/api/routers/admin/analytics/get-project-count.ts
@@ -0,0 +1,13 @@
+import { eq, sql } from "drizzle-orm"
+
+import { adminProcedure } from "~/server/api/trpc"
+import { Project } from "~/server/db/schema"
+
+export const getProjectCount = adminProcedure.query(async ({ ctx }) => {
+ const [result] = await ctx.db
+ .select({ count: sql`count(*)`.mapWith(Number) })
+ .from(Project)
+ .where(eq(Project.is_public, true))
+
+ return { publicProjects: result?.count ?? 0 }
+})
diff --git a/src/server/api/routers/admin/analytics/get-user-count.ts b/src/server/api/routers/admin/analytics/get-user-count.ts
index fc1ff31b3..7decc31af 100644
--- a/src/server/api/routers/admin/analytics/get-user-count.ts
+++ b/src/server/api/routers/admin/analytics/get-user-count.ts
@@ -1,4 +1,4 @@
-import { isNotNull, sql } from "drizzle-orm"
+import { eq, sql } from "drizzle-orm"
import { adminProcedure } from "~/server/api/trpc"
import { User } from "~/server/db/schema"
@@ -9,7 +9,7 @@ export const getUserCount = adminProcedure.query(async ({ ctx }) => {
ctx.db
.select({ count: sql`count(*)`.mapWith(Number) })
.from(User)
- .where(isNotNull(User.role)),
+ .where(eq(User.role, "member")),
])
return {
diff --git a/src/server/api/routers/admin/analytics/index.ts b/src/server/api/routers/admin/analytics/index.ts
index 75d2fd0f3..726604003 100644
--- a/src/server/api/routers/admin/analytics/index.ts
+++ b/src/server/api/routers/admin/analytics/index.ts
@@ -2,6 +2,7 @@ import { createTRPCRouter } from "~/server/api/trpc"
import { getGenderStatistics } from "./get-gender-statistics"
import { getAllPayments } from "./get-payments"
+import { getProjectCount } from "./get-project-count"
import { getUserCount } from "./get-user-count"
import { getUsersPerDay } from "./get-users-per-day"
@@ -10,4 +11,5 @@ export const analyticsAdminRouter = createTRPCRouter({
getUsersPerDay,
getGenderStatistics,
getAllPayments,
+ getProjectCount,
})
diff --git a/src/server/api/routers/admin/projects/get-projects.ts b/src/server/api/routers/admin/projects/get-projects.ts
index bc1739580..12138ba92 100644
--- a/src/server/api/routers/admin/projects/get-projects.ts
+++ b/src/server/api/routers/admin/projects/get-projects.ts
@@ -1,5 +1,5 @@
import { desc } from "drizzle-orm"
-import { and, eq } from "drizzle-orm"
+import { eq } from "drizzle-orm"
import { z } from "zod"
import { adminProcedure } from "~/server/api/trpc"
diff --git a/src/server/api/routers/projects/get-projects.ts b/src/server/api/routers/projects/get-projects.ts
index 06690923e..118cf5d56 100644
--- a/src/server/api/routers/projects/get-projects.ts
+++ b/src/server/api/routers/projects/get-projects.ts
@@ -44,7 +44,8 @@ export const getProjectByUser = protectedRatedProcedure(Ratelimit.fixedWindow(60
let whereClause = sql`TRUE`
if (input.user) {
- whereClause = sql`${whereClause} AND ${Project.members} ILIKE '%${input.user}%'`
+ const searchPattern = `%${input.user}%`
+ whereClause = sql`${whereClause} AND ${Project.members} ILIKE ${searchPattern}`
}
if (typeof input.isPublic === "boolean") {
diff --git a/src/server/db/xata.ts b/src/server/db/xata.ts
deleted file mode 100644
index 50f317f61..000000000
--- a/src/server/db/xata.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import { buildClient } from "@xata.io/client"
-import type { BaseClientOptions, SchemaInference } from "@xata.io/client"
-
-import { env } from "~/env"
-
-const tables = [] as const
-
-export type SchemaTables = typeof tables
-export type InferredTypes = SchemaInference
-
-// eslint-disable-next-line
-export type DatabaseSchema = {}
-
-const DatabaseClient = buildClient()
-
-const defaultOptions = {
- databaseURL: env.XATA_DATABASE_URL,
-}
-
-export class XataClient extends DatabaseClient {
- constructor(options?: BaseClientOptions) {
- super({ ...defaultOptions, ...options }, tables)
- }
-}
-
-let instance: XataClient | undefined = undefined
-
-export const getXataClient = () => {
- if (instance) return instance
-
- instance = new XataClient()
- return instance
-}
From e178a00593e57b83e2914324d8c21eb5cfcbcae0 Mon Sep 17 00:00:00 2001
From: ErikaKK <491649804@qq.com>
Date: Fri, 6 Feb 2026 23:20:39 +0800
Subject: [PATCH 4/7] fix db type issue
---
src/app/dashboard/admin/@tools/export.tsx | 32 +++++++++++++------
src/app/dashboard/admin/@tools/page.tsx | 3 --
.../dashboard/admin/@tools/update-email.tsx | 4 ++-
.../api/routers/admin/users/update-email.ts | 17 ++++++++--
.../api/routers/projects/get-projects.ts | 9 +++---
src/server/api/routers/users/update-email.ts | 5 +++
src/server/db/drizzle.ts | 9 ------
7 files changed, 50 insertions(+), 29 deletions(-)
delete mode 100644 src/server/db/drizzle.ts
diff --git a/src/app/dashboard/admin/@tools/export.tsx b/src/app/dashboard/admin/@tools/export.tsx
index 2711b55be..2fc42097f 100644
--- a/src/app/dashboard/admin/@tools/export.tsx
+++ b/src/app/dashboard/admin/@tools/export.tsx
@@ -25,21 +25,35 @@ export default function ExportButton({ data, label }: ExportButtonProps) {
return
}
- const headers = Object.keys(data[0] as Record).join(",")
- const escapeCSV = (value: unknown) => {
+ const headers = Object.keys(data[0] as Record)
+ const escapeCSV = (value: unknown): string => {
if (value == null) return ""
- if (value instanceof Date) return value.toISOString()
- if (typeof value === "string" && !isNaN(Date.parse(value))) {
- return new Date(value).toISOString()
+
+ let str: string
+ if (typeof value === "object") {
+ if (value instanceof Date) {
+ str = value.toISOString()
+ } else {
+ // Stringify JSON objects/arrays
+ str = JSON.stringify(value)
+ }
+ } else {
+ str = String(value)
}
- const str = String(value)
- if (/[",\n]/.test(str)) {
+
+ // Escape CSV: wrap in quotes if contains comma, quote, or newline
+ // and escape internal quotes by doubling them
+ if (/[",\n\r]/.test(str)) {
return `"${str.replace(/"/g, '""')}"`
}
return str
}
- const rows = data.map((row) => Object.values(row).map(escapeCSV).join(",")).join("\n")
- const csv = headers + "\n" + rows
+
+ const rows = data
+ .map((row) => headers.map((key) => escapeCSV((row as Record)[key])).join(","))
+ .join("\n")
+
+ const csv = headers.join(",") + "\n" + rows
const blob = new Blob([csv], { type: "text/csv" })
const url = window.URL.createObjectURL(blob)
diff --git a/src/app/dashboard/admin/@tools/page.tsx b/src/app/dashboard/admin/@tools/page.tsx
index 38fc1e1ac..9b839e930 100644
--- a/src/app/dashboard/admin/@tools/page.tsx
+++ b/src/app/dashboard/admin/@tools/page.tsx
@@ -12,9 +12,6 @@ export default async function AdminUserTable() {
Tools
-
-
-
diff --git a/src/app/dashboard/admin/@tools/update-email.tsx b/src/app/dashboard/admin/@tools/update-email.tsx
index e3d709e48..5f6366f08 100644
--- a/src/app/dashboard/admin/@tools/update-email.tsx
+++ b/src/app/dashboard/admin/@tools/update-email.tsx
@@ -27,9 +27,11 @@ const defaultValues: FormSchema = {
newEmail: "",
}
+// deprecated, only for admin to update user email
+// users should use the updateEmail procedure in users router to update their own email
const UpdateEmail = () => {
return (
-
+
)
diff --git a/src/server/api/routers/admin/users/update-email.ts b/src/server/api/routers/admin/users/update-email.ts
index c03151ad0..8b18111f1 100644
--- a/src/server/api/routers/admin/users/update-email.ts
+++ b/src/server/api/routers/admin/users/update-email.ts
@@ -1,11 +1,13 @@
import { clerkClient } from "@clerk/nextjs/server"
import { TRPCError } from "@trpc/server"
-import { eq } from "drizzle-orm"
+import { eq, sql } from "drizzle-orm"
import { z } from "zod"
import { adminProcedure } from "~/server/api/trpc"
-import { User } from "~/server/db/schema"
+import { Project, User } from "~/server/db/schema"
+// deprecated, only for admin to update user email
+// users should use the updateEmail procedure in users router to update their own email
export const updateEmail = adminProcedure
.input(z.object({ userId: z.string(), oldEmail: z.string().email(), newEmail: z.string().email() }))
.mutation(async ({ ctx, input }) => {
@@ -13,18 +15,23 @@ export const updateEmail = adminProcedure
const user_data = await ctx.db.query.User.findFirst({
where: eq(User.email, input.oldEmail),
})
+
if (!user_data) {
throw new TRPCError({ code: "NOT_FOUND", message: `User with email: ${input.oldEmail} does not exist` })
}
+
const user_email_data = await ctx.db.query.User.findFirst({
where: eq(User.email, input.newEmail),
})
+
if (user_email_data) {
throw new TRPCError({ code: "FORBIDDEN", message: `User with email: ${input.newEmail} already exist` })
}
+
const user = await ctx.db.query.User.findFirst({
where: eq(User.id, input.userId),
})
+
if (!user) {
throw new TRPCError({ code: "NOT_FOUND", message: `User with id: ${input.userId} does not exist` })
}
@@ -46,6 +53,12 @@ export const updateEmail = adminProcedure
console.error(err)
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Failed to update email" })
}
+ await ctx.db
+ .update(Project)
+ .set({
+ members: sql`array_replace(${Project.members}, ${input.oldEmail}, ${input.newEmail})`,
+ })
+ .where(sql`${input.oldEmail} = ANY(${Project.members})`)
return user
})
diff --git a/src/server/api/routers/projects/get-projects.ts b/src/server/api/routers/projects/get-projects.ts
index 118cf5d56..6bc49ba81 100644
--- a/src/server/api/routers/projects/get-projects.ts
+++ b/src/server/api/routers/projects/get-projects.ts
@@ -41,15 +41,14 @@ export const getProjectByName = publicRatedProcedure(Ratelimit.fixedWindow(60, "
export const getProjectByUser = protectedRatedProcedure(Ratelimit.fixedWindow(60, "30s"))
.input(z.object({ user: z.string(), isPublic: z.boolean().optional() }))
.query(async ({ input, ctx }) => {
- let whereClause = sql`TRUE`
+ const conditions = []
if (input.user) {
- const searchPattern = `%${input.user}%`
- whereClause = sql`${whereClause} AND ${Project.members} ILIKE ${searchPattern}`
+ conditions.push(arrayContains(Project.members, [input.user]))
}
if (typeof input.isPublic === "boolean") {
- whereClause = sql`${whereClause} AND ${Project.is_public} = ${input.isPublic}`
+ conditions.push(eq(Project.is_public, input.isPublic))
}
const projectData = await ctx.db.query.Project.findMany({
@@ -71,7 +70,7 @@ export const getProjectByUser = protectedRatedProcedure(Ratelimit.fixedWindow(60
application_url: true,
is_public: true,
},
- where: whereClause,
+ where: conditions.length > 0 ? and(...conditions) : undefined,
})
return projectData
diff --git a/src/server/api/routers/users/update-email.ts b/src/server/api/routers/users/update-email.ts
index 18a7c84ef..98d7f6e92 100644
--- a/src/server/api/routers/users/update-email.ts
+++ b/src/server/api/routers/users/update-email.ts
@@ -20,18 +20,23 @@ export const updateEmail = protectedRatedProcedure(Ratelimit.fixedWindow(4, "30s
const user_data = await ctx.db.query.User.findFirst({
where: eq(User.email, input.oldEmail),
})
+
if (!user_data) {
throw new TRPCError({ code: "NOT_FOUND", message: `User with email: ${input.oldEmail} does not exist` })
}
+
const user_email_data = await ctx.db.query.User.findFirst({
where: eq(User.email, input.newEmail),
})
+
if (user_email_data) {
throw new TRPCError({ code: "FORBIDDEN", message: `User with email: ${input.newEmail} already exist` })
}
+
const user = await ctx.db.query.User.findFirst({
where: eq(User.id, input.userId),
})
+
if (!user) {
throw new TRPCError({ code: "NOT_FOUND", message: `User with id: ${input.userId} does not exist` })
}
diff --git a/src/server/db/drizzle.ts b/src/server/db/drizzle.ts
deleted file mode 100644
index 800c7ad14..000000000
--- a/src/server/db/drizzle.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-// src/server/db/drizzle.ts
-import { drizzle } from "drizzle-orm/node-postgres"
-import { Pool } from "pg"
-
-const pool = new Pool({
- connectionString: process.env.SUPABASE_DB_URL,
-})
-
-export const db = drizzle(pool)
From c05ced849d330a3d87d17758ade9ca31db06df7d Mon Sep 17 00:00:00 2001
From: ErikaKK <491649804@qq.com>
Date: Fri, 6 Feb 2026 23:29:08 +0800
Subject: [PATCH 5/7] small edits
---
src/middleware.ts | 4 ++++
src/server/api/routers/projects/get-projects.ts | 2 +-
2 files changed, 5 insertions(+), 1 deletion(-)
diff --git a/src/middleware.ts b/src/middleware.ts
index d1f311d9d..9a12d65e4 100644
--- a/src/middleware.ts
+++ b/src/middleware.ts
@@ -20,7 +20,11 @@ export default clerkMiddleware(async (auth, req) => {
export const config = {
matcher: [
"/",
+ // Skip Next.js internals and all static files, unless found in search params
"/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)",
+ // // Always run for API routes
+ // "/(api|trpc)(.*)",
+ // Ignore trpc routes because they are handled by the server
"/api(.*)",
],
}
diff --git a/src/server/api/routers/projects/get-projects.ts b/src/server/api/routers/projects/get-projects.ts
index 6bc49ba81..4687a6a22 100644
--- a/src/server/api/routers/projects/get-projects.ts
+++ b/src/server/api/routers/projects/get-projects.ts
@@ -1,6 +1,6 @@
import { TRPCError } from "@trpc/server"
import { Ratelimit } from "@upstash/ratelimit"
-import { and, arrayContains, eq, sql } from "drizzle-orm"
+import { and, arrayContains, eq } from "drizzle-orm"
import { z } from "zod"
import { protectedRatedProcedure, publicRatedProcedure } from "~/server/api/trpc"
From b53474b02975125f6e559065d9bb83fa21ccf20b Mon Sep 17 00:00:00 2001
From: ErikaKK <491649804@qq.com>
Date: Fri, 6 Feb 2026 23:43:37 +0800
Subject: [PATCH 6/7] fix project member preview api permission
---
src/app/projects/(default)/db-project.tsx | 2 +-
src/server/api/routers/admin/users/index.ts | 2 --
.../api/routers/{admin => }/users/get-name-by-email.ts | 5 +++--
src/server/api/routers/users/index.ts | 2 ++
4 files changed, 6 insertions(+), 5 deletions(-)
rename src/server/api/routers/{admin => }/users/get-name-by-email.ts (67%)
diff --git a/src/app/projects/(default)/db-project.tsx b/src/app/projects/(default)/db-project.tsx
index 8a19e4994..4303aa12e 100644
--- a/src/app/projects/(default)/db-project.tsx
+++ b/src/app/projects/(default)/db-project.tsx
@@ -53,7 +53,7 @@ export const Impact = ({ impact, ...props }: { impact?: string[]; className?: st
)
export default function DBProject({ data }: DBProjectProps) {
- const { data: users } = api.admin.users.getNamesByEmails.useQuery({ emails: data.members ? data.members : [] })
+ const { data: users } = api.users.getNamesByEmails.useQuery({ emails: data.members ? data.members : [] })
let icon = "devices" // default "devices"
if (data.type === "Mobile application") {
diff --git a/src/server/api/routers/admin/users/index.ts b/src/server/api/routers/admin/users/index.ts
index cd3f7b885..1a83a3eef 100644
--- a/src/server/api/routers/admin/users/index.ts
+++ b/src/server/api/routers/admin/users/index.ts
@@ -2,7 +2,6 @@ import { createTRPCRouter } from "~/server/api/trpc"
import { createManual } from "./create-manual"
import { getAll } from "./get-all"
-import { getNamesByEmails } from "./get-name-by-email"
import { updateEmail } from "./update-email"
import { updateRole } from "./update-role"
@@ -11,5 +10,4 @@ export const usersAdminRouter = createTRPCRouter({
getAll,
updateRole,
updateEmail,
- getNamesByEmails,
})
diff --git a/src/server/api/routers/admin/users/get-name-by-email.ts b/src/server/api/routers/users/get-name-by-email.ts
similarity index 67%
rename from src/server/api/routers/admin/users/get-name-by-email.ts
rename to src/server/api/routers/users/get-name-by-email.ts
index b9a469f55..a4bc4032d 100644
--- a/src/server/api/routers/admin/users/get-name-by-email.ts
+++ b/src/server/api/routers/users/get-name-by-email.ts
@@ -1,10 +1,11 @@
+import { Ratelimit } from "@upstash/ratelimit"
import { inArray } from "drizzle-orm"
import { z } from "zod"
-import { adminProcedure } from "~/server/api/trpc"
+import { publicRatedProcedure } from "~/server/api/trpc"
import { User } from "~/server/db/schema"
-export const getNamesByEmails = adminProcedure
+export const getNamesByEmails = publicRatedProcedure(Ratelimit.fixedWindow(4, "30s"))
.input(z.object({ emails: z.array(z.string().email()) }))
.query(async ({ ctx, input }) => {
const users = await ctx.db.query.User.findMany({
diff --git a/src/server/api/routers/users/index.ts b/src/server/api/routers/users/index.ts
index c52fb16ff..1bc9527be 100644
--- a/src/server/api/routers/users/index.ts
+++ b/src/server/api/routers/users/index.ts
@@ -3,6 +3,7 @@ import { createTRPCRouter } from "~/server/api/trpc"
import { create } from "./create"
import { get } from "./get"
import { getCurrent } from "./get-current"
+import { getNamesByEmails } from "./get-name-by-email"
import { update } from "./update"
import { updateEmail } from "./update-email"
@@ -12,4 +13,5 @@ export const usersRouter = createTRPCRouter({
get,
update,
updateEmail,
+ getNamesByEmails,
})
From 2c8251d3019bcb9f4b61454a644675a52deafdab Mon Sep 17 00:00:00 2001
From: ErikaKK <491649804@qq.com>
Date: Fri, 6 Feb 2026 23:53:28 +0800
Subject: [PATCH 7/7] edit docs
---
README.md | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index 5c1d41fd3..fec8f684b 100644
--- a/README.md
+++ b/README.md
@@ -33,8 +33,9 @@ pnpm i -E
pnpm upgrade -L
# Database migration
-npx drizzle-kit generate
-npx drizzle-kit push
+pnpm drizzle-kit pull
+pnpm drizzle-kit generate
+pnpm drizzle-kit push
```
### Notes