Skip to content

Commit f0766df

Browse files
authored
Db backup button (#1668)
* add export button for project and payment table * improve the export for jsonb type * edit color
1 parent eb64742 commit f0766df

9 files changed

Lines changed: 95 additions & 22 deletions

File tree

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,14 @@ import { useEffect, useState } from "react"
55
import { Alert, AlertDescription, AlertTitle } from "~/components/ui/alert"
66
import { Button } from "~/components/ui/button"
77

8-
import { TableProps } from "./table"
8+
import { type Payment, type Project, type User } from "~/server/db/types"
99

10-
export default function ExportButton({ data }: TableProps) {
10+
export interface ExportButtonProps {
11+
data: Array<Project | Payment | User>
12+
label: string
13+
}
14+
15+
export default function ExportButton({ data, label }: ExportButtonProps) {
1116
const [open, setOpen] = useState(false)
1217
useEffect(() => {
1318
if (!open) return
@@ -17,19 +22,44 @@ export default function ExportButton({ data }: TableProps) {
1722
const downloadCSV = () => {
1823
if (!data || data.length === 0) {
1924
setOpen(true)
20-
console.log("open")
2125
return
2226
}
2327

24-
const headers = Object.keys(data[0] as Record<string, unknown>).join(",")
25-
const rows = data.map((row) => Object.values(row).join(",")).join("\n")
26-
const csv = headers + "\n" + rows
28+
const headers = Object.keys(data[0] as Record<string, unknown>)
29+
const escapeCSV = (value: unknown): string => {
30+
if (value == null) return ""
31+
32+
let str: string
33+
if (typeof value === "object") {
34+
if (value instanceof Date) {
35+
str = value.toISOString()
36+
} else {
37+
// Stringify JSON objects/arrays
38+
str = JSON.stringify(value)
39+
}
40+
} else {
41+
str = String(value)
42+
}
43+
44+
// Escape CSV: wrap in quotes if contains comma, quote, or newline
45+
// and escape internal quotes by doubling them
46+
if (/[",\n\r]/.test(str)) {
47+
return `"${str.replace(/"/g, '""')}"`
48+
}
49+
return str
50+
}
51+
52+
const rows = data
53+
.map((row) => headers.map((key) => escapeCSV((row as Record<string, unknown>)[key])).join(","))
54+
.join("\n")
55+
56+
const csv = headers.join(",") + "\n" + rows
2757
const blob = new Blob([csv], { type: "text/csv" })
2858
const url = window.URL.createObjectURL(blob)
2959

3060
const a = document.createElement("a")
3161
a.setAttribute("href", url)
32-
a.setAttribute("download", "output.csv")
62+
a.setAttribute("download", `${label}.csv`)
3363
a.click()
3464
window.URL.revokeObjectURL(url)
3565
setOpen(true)
@@ -38,7 +68,7 @@ export default function ExportButton({ data }: TableProps) {
3868
return (
3969
<div className="relative inline-block">
4070
<Button onClick={downloadCSV} variant={"outline"}>
41-
Export CSV
71+
Export {label}
4272
</Button>
4373
{open && (!data || data.length === 0) && (
4474
<Alert variant="destructive" className="fixed top-30 left-1/2 -translate-x-1/2 z-50 max-w-sm">
@@ -49,7 +79,7 @@ export default function ExportButton({ data }: TableProps) {
4979
{open && data && data.length > 0 && (
5080
<Alert variant="default" className="fixed top-30 left-1/2 -translate-x-1/2 z-50 max-w-sm">
5181
<AlertTitle>Export Successful</AlertTitle>
52-
<AlertDescription>Your data has been exported as `output.csv`.</AlertDescription>
82+
<AlertDescription>Your data has been exported as {`${label}.csv`}.</AlertDescription>
5383
</Alert>
5484
)}
5585
</div>

src/app/dashboard/admin/@tools/page.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
1+
import { api } from "~/trpc/server"
2+
3+
import ExportButton from "./export"
14
import UpdateEmail from "./update-email"
25

36
export default async function AdminUserTable() {
7+
const projects = await api.admin.projects.getAllProjects.query()
8+
const payments = await api.admin.analytics.getAllPayments.query()
9+
410
return (
511
<>
612
<div className="flex h-[50px] items-center p-1">
@@ -9,6 +15,10 @@ export default async function AdminUserTable() {
915
<div className="grid grid-cols-1 md:grid-cols-5 gap-4">
1016
<UpdateEmail />
1117
</div>
18+
<div className=" flex flex-col space-y-4 my-4">
19+
<ExportButton data={projects} label="projects" />
20+
<ExportButton data={payments} label="payments" />
21+
</div>
1222
</>
1323
)
1424
}

src/app/dashboard/admin/@users/table.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,13 +63,13 @@ import { cn } from "~/lib/utils"
6363
import { type User } from "~/server/db/types"
6464
import { api } from "~/trpc/react"
6565

66-
import ExportButton from "./export"
66+
import ExportButton from "../@tools/export"
6767
import AddUserForm from "./form"
6868

69-
type UserProps = Omit<User, "subscribe" | "square_customer_id" | "updatedAt" | "reminder_pending">
69+
type DisplayColumn = Omit<User, "subscribe" | "square_customer_id" | "updatedAt" | "reminder_pending">
7070

7171
export interface TableProps {
72-
data: Array<UserProps>
72+
data: Array<User>
7373
}
7474
interface UserTableProps extends TableProps {
7575
refetch: () => void
@@ -92,7 +92,7 @@ const sortIcon = (sortOrder: string | boolean) => {
9292
}
9393
}
9494

95-
const columns = (updateRole: ({ id, role }: UpdateUserRoleFunctionProps) => void): ColumnDef<UserProps>[] => [
95+
const columns = (updateRole: ({ id, role }: UpdateUserRoleFunctionProps) => void): ColumnDef<DisplayColumn>[] => [
9696
{
9797
id: "Select",
9898
enableSorting: false,
@@ -341,7 +341,7 @@ const UserTable = ({ data, isRefetching, ...props }: UserTableProps) => {
341341

342342
return (
343343
<>
344-
<div className="flex h-[50px] items-center gap-2 p-1 pr-0">
344+
<div className="flex h-12 items-center gap-2 p-1 pr-0">
345345
{data.length > 0 && (
346346
<>
347347
{selectedRowIDs.length > 0 && (
@@ -418,7 +418,7 @@ const UserTable = ({ data, isRefetching, ...props }: UserTableProps) => {
418418
})}
419419
</DropdownMenuContent>
420420
</DropdownMenu>
421-
<ExportButton data={data} />
421+
<ExportButton data={data} label="users" />
422422
<Button variant="secondary" disabled={isRefetching} onClick={props.refetch}>
423423
<span className={cn("material-symbols-sharp", isRefetching && "animate-spin")}>autorenew</span>
424424
<span className="ml-2 hidden sm:block">Sync{isRefetching && "ing"}</span>

src/app/profile/[id]/profile.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"use client"
22

3+
import { format } from "date-fns"
34
import Link from "next/link"
45
import { siDiscord, siGithub } from "simple-icons"
56

@@ -115,7 +116,7 @@ const ProfilePage = ({ id, currentUser }: ProfilePageProps) => {
115116
<div className="p-2 w-auto">
116117
<div className="">
117118
{currentUser?.id === user.id ? (
118-
user?.role === null && (
119+
user?.role === null ? (
119120
<div className="space-y-4 max-w-md">
120121
<div className="space-y-2">
121122
<h2 className="font-semibold leading-none tracking-tight">Membership</h2>
@@ -140,6 +141,16 @@ const ProfilePage = ({ id, currentUser }: ProfilePageProps) => {
140141
</p>
141142
)}
142143
</div>
144+
) : (
145+
user.role === "member" &&
146+
user.membership_expiry && (
147+
<div>
148+
Your membership will expire on{" "}
149+
<span className="text-primary">
150+
{format(new Date(String(user.membership_expiry)), "dd MMMM yyyy")}
151+
</span>
152+
</div>
153+
)
143154
)
144155
) : (
145156
<>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { desc } from "drizzle-orm"
2+
3+
import { adminProcedure } from "~/server/api/trpc"
4+
import { Payment } from "~/server/db/schema"
5+
6+
export const getAllPayments = adminProcedure.query(async ({ ctx }) => {
7+
const paymentList = await ctx.db.query.Payment.findMany({
8+
orderBy: [desc(Payment.createdAt), desc(Payment.id)],
9+
})
10+
11+
return paymentList
12+
})
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import { createTRPCRouter } from "~/server/api/trpc"
22

33
import { getGenderStatistics } from "./get-gender-statistics"
4+
import { getAllPayments } from "./get-payments"
45
import { getUserCount } from "./get-user-count"
56
import { getUsersPerDay } from "./get-users-per-day"
67

78
export const analyticsAdminRouter = createTRPCRouter({
89
getUserCount,
910
getUsersPerDay,
1011
getGenderStatistics,
12+
getAllPayments,
1113
})
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { desc } from "drizzle-orm"
2+
3+
import { adminProcedure } from "~/server/api/trpc"
4+
import { Project } from "~/server/db/schema"
5+
6+
export const getAllProjects = adminProcedure.query(async ({ ctx }) => {
7+
const projectList = await ctx.db.query.Project.findMany({
8+
orderBy: [desc(Project.createdAt), desc(Project.id)],
9+
})
10+
11+
return projectList
12+
})

src/server/api/routers/admin/projects/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { createTRPCRouter } from "~/server/api/trpc"
22

33
import { create } from "./create"
44
import { deleteProject } from "./delete"
5+
import { getAllProjects } from "./get-all"
56
import { getProjectById } from "./get-project-by-id"
67
import { getProjects, getPublicProjects } from "./get-projects"
78
import { update } from "./update"
@@ -13,4 +14,5 @@ export const projectsAdminRouter = createTRPCRouter({
1314
update,
1415
getProjectById,
1516
deleteProject,
17+
getAllProjects,
1618
})

src/server/api/routers/admin/users/get-all.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,6 @@ import { User } from "~/server/db/schema"
55

66
export const getAll = adminProcedure.query(async ({ ctx }) => {
77
const userList = await ctx.db.query.User.findMany({
8-
columns: {
9-
subscribe: false,
10-
square_customer_id: false,
11-
updatedAt: false,
12-
reminder_pending: false,
13-
},
148
orderBy: [desc(User.createdAt), desc(User.id)],
159
})
1610

0 commit comments

Comments
 (0)