From 02166b4f93898cc6feff208780f907e3c81b946d Mon Sep 17 00:00:00 2001 From: Bianca Date: Thu, 9 Jul 2026 17:43:06 +0200 Subject: [PATCH 1/2] feat: implement guide management features including creation, deletion, and display --- .../(active)/azure/members/table.tsx | 6 +- .../dashboard/(active)/web/guide/columns.tsx | 35 ++++ .../(active)/web/guide/create-guide.tsx | 165 ++++++++++++++++++ .../(active)/web/guide/delete-guide.tsx | 79 +++++++++ src/app/dashboard/(active)/web/guide/page.tsx | 12 ++ .../dashboard/(active)/web/guide/table.tsx | 39 +++++ src/app/dashboard/(active)/web/guide/types.ts | 6 + src/components/dashboard-sidebar/data.tsx | 7 +- src/components/data-table.tsx | 7 +- src/server/actions/guides.ts | 31 ++++ 10 files changed, 378 insertions(+), 9 deletions(-) create mode 100644 src/app/dashboard/(active)/web/guide/columns.tsx create mode 100644 src/app/dashboard/(active)/web/guide/create-guide.tsx create mode 100644 src/app/dashboard/(active)/web/guide/delete-guide.tsx create mode 100644 src/app/dashboard/(active)/web/guide/page.tsx create mode 100644 src/app/dashboard/(active)/web/guide/table.tsx create mode 100644 src/app/dashboard/(active)/web/guide/types.ts create mode 100644 src/server/actions/guides.ts diff --git a/src/app/dashboard/(active)/azure/members/table.tsx b/src/app/dashboard/(active)/azure/members/table.tsx index 2a5bf70..8209cf5 100644 --- a/src/app/dashboard/(active)/azure/members/table.tsx +++ b/src/app/dashboard/(active)/azure/members/table.tsx @@ -49,11 +49,7 @@ export function AssocTable({ members }: { members: AzureMember[] }) { - + ) } diff --git a/src/app/dashboard/(active)/web/guide/columns.tsx b/src/app/dashboard/(active)/web/guide/columns.tsx new file mode 100644 index 0000000..3e3ba00 --- /dev/null +++ b/src/app/dashboard/(active)/web/guide/columns.tsx @@ -0,0 +1,35 @@ +"use client" +import { createColumnHelper } from "@tanstack/react-table" +import { format } from "date-fns" +import { DeleteGuide } from "./delete-guide" +import type { Guide } from "./types" + +const ch = createColumnHelper() + +export const columns = (onDeleted: (id: number) => void) => [ + ch.accessor("version", { + id: "version", + header: "Version", + cell: ({ getValue }) => {getValue()}, + }), + ch.accessor("date", { + id: "date", + header: "Date", + cell: ({ getValue }) => format(new Date(getValue()), "dd/MM/yyyy"), + }), + ch.accessor("file", { + id: "file", + header: "File", + cell: ({ getValue }) => ( + + Download + + ), + }), + ch.group({ + id: "actions", + cell: (props) => ( + + ), + }), +] diff --git a/src/app/dashboard/(active)/web/guide/create-guide.tsx b/src/app/dashboard/(active)/web/guide/create-guide.tsx new file mode 100644 index 0000000..fa8322b --- /dev/null +++ b/src/app/dashboard/(active)/web/guide/create-guide.tsx @@ -0,0 +1,165 @@ +"use client" + +import { format } from "date-fns" +import { ChevronDownIcon, Plus, Upload } from "lucide-react" +import { useState } from "react" +import { toast } from "sonner" +import { Button } from "@/components/ui/button" +import { Calendar } from "@/components/ui/calendar" +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { Field, FieldLabel } from "@/components/ui/field" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover" +import { createGuide } from "@/server/actions/guides" +import type { Guide } from "./types" + +export function CreateGuide({ + existingVersions, + latestVersion, + onCreated, +}: { + existingVersions: string[] + latestVersion?: string + onCreated: (guide: Guide) => void +}) { + const [open, setOpen] = useState(false) + const [version, setVersion] = useState(latestVersion ?? "") + const [date, setDate] = useState(new Date()) + const [datePickerOpen, setDatePickerOpen] = useState(false) + const [file, setFile] = useState(null) + const [pending, setPending] = useState(false) + + const trimmedVersion = version.trim() + const isDuplicate = trimmedVersion.length > 0 && existingVersions.includes(trimmedVersion) + const disabled = !trimmedVersion || !date || !file || isDuplicate || pending + + function handleOpenChange(v: boolean) { + setOpen(v) + if (v) { + setVersion(latestVersion ?? "") + setDate(new Date()) + setFile(null) + } + } + + async function handleSubmit() { + if (!date || !file || isDuplicate) return + setPending(true) + + try { + const { guide, error } = await createGuide({ version: trimmedVersion, date: date.toISOString(), file }) + + if (error === "UNAUTHORIZED") toast.error("You don't have permission to add guides.") + else if (error === "DUPLICATE_VERSION") toast.error("This version already exists.") + else if (!guide) toast.error("There was an error creating the guide.") + else { + toast.success("Guide created successfully") + onCreated(guide) + handleOpenChange(false) + } + } catch (err) { + toast.error("There was an error") + console.error(err) + } finally { + setPending(false) + } + } + + return ( + + + Add Guide + + } + /> + + + Add Guide + Upload a new version of the Guida della Matricola. + + +
+
+ + setVersion(e.target.value)} + /> + {isDuplicate &&

This version already exists.

} +
+ + + Date + + + {date ? format(date, "dd/MM/yyyy") : "Select date"} + + + } + /> + + { + setDate(d) + setDatePickerOpen(false) + }} + /> + + + + +
+ + + setFile(e.target.files?.[0] ?? null)} + className="sr-only" + /> +
+
+ + + Cancel} /> + + +
+
+ ) +} diff --git a/src/app/dashboard/(active)/web/guide/delete-guide.tsx b/src/app/dashboard/(active)/web/guide/delete-guide.tsx new file mode 100644 index 0000000..9d88482 --- /dev/null +++ b/src/app/dashboard/(active)/web/guide/delete-guide.tsx @@ -0,0 +1,79 @@ +"use client" + +import { OctagonX, Trash } from "lucide-react" +import { useState } from "react" +import { toast } from "sonner" +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogMedia, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/components/ui/alert-dialog" +import { Button } from "@/components/ui/button" +import { deleteGuide } from "@/server/actions/guides" + +export function DeleteGuide({ + id, + version, + onDeleted, +}: { + id: number + version: string + onDeleted: (id: number) => void +}) { + const [open, setOpen] = useState(false) + + async function handleDelete() { + try { + const { error } = await deleteGuide(id) + + if (error === "UNAUTHORIZED") toast.error("You don't have permission to delete guides.") + else if (error === "NOT_FOUND") toast.info("This guide was already deleted.") + else { + toast.success("Guide deleted successfully") + onDeleted(id) + } + } catch (err) { + toast.error("There was an error") + console.error(err) + } finally { + setOpen(false) + } + } + + return ( + + + + + } + /> + + + + + + Delete Guide + + Are you sure you want to delete version {version}?
+ This action cannot be undone. +
+
+ + Cancel + + Confirm + + +
+
+ ) +} diff --git a/src/app/dashboard/(active)/web/guide/page.tsx b/src/app/dashboard/(active)/web/guide/page.tsx new file mode 100644 index 0000000..8746554 --- /dev/null +++ b/src/app/dashboard/(active)/web/guide/page.tsx @@ -0,0 +1,12 @@ +import { getAllGuides } from "@/server/actions/guides" +import { GuideTable } from "./table" + +export default async function GuideMatricolaPage() { + const guides = await getAllGuides() + + return ( +
+ +
+ ) +} diff --git a/src/app/dashboard/(active)/web/guide/table.tsx b/src/app/dashboard/(active)/web/guide/table.tsx new file mode 100644 index 0000000..860b9cc --- /dev/null +++ b/src/app/dashboard/(active)/web/guide/table.tsx @@ -0,0 +1,39 @@ +"use client" + +import { useRouter } from "next/navigation" +import { useState } from "react" +import { DataTable } from "@/components/data-table" +import { columns } from "./columns" +import { CreateGuide } from "./create-guide" +import type { Guide } from "./types" + +export function GuideTable({ guides }: { guides: Guide[] }) { + const router = useRouter() + const [items, setItems] = useState(guides) + + function handleCreated(guide: Guide) { + setItems((prev) => [guide, ...prev]) + router.refresh() + } + + function handleDeleted(id: number) { + setItems((prev) => prev.filter((g) => g.id !== id)) + router.refresh() + } + + return ( +
+
+

Guida della Matricola

+
+ g.version)} + latestVersion={items[0]?.version} + onCreated={handleCreated} + /> +
+ + +
+ ) +} diff --git a/src/app/dashboard/(active)/web/guide/types.ts b/src/app/dashboard/(active)/web/guide/types.ts new file mode 100644 index 0000000..fdc74b2 --- /dev/null +++ b/src/app/dashboard/(active)/web/guide/types.ts @@ -0,0 +1,6 @@ +export type Guide = { + id: number + version: string + date: string + file: string +} diff --git a/src/components/dashboard-sidebar/data.tsx b/src/components/dashboard-sidebar/data.tsx index 7401a0c..c2e862d 100644 --- a/src/components/dashboard-sidebar/data.tsx +++ b/src/components/dashboard-sidebar/data.tsx @@ -1,4 +1,4 @@ -import { MessageCircleMoreIcon, Sparkle, Users } from "lucide-react" +import { BookOpen, Globe, MessageCircleMoreIcon, Sparkle, Users } from "lucide-react" import Image from "next/image" import azureSvg from "@/assets/svg/azure.svg" import telegramSvg from "@/assets/svg/telegram.svg" @@ -19,6 +19,11 @@ export const DSData = { icon: azure logo, items: [{ title: "Members", url: "/dashboard/azure/members", icon: }], }, + { + title: "Web", + icon: , + items: [{ title: "Guide", url: "/dashboard/web/guide", icon: }], + }, ], } diff --git a/src/components/data-table.tsx b/src/components/data-table.tsx index acc07c7..c97f7bc 100644 --- a/src/components/data-table.tsx +++ b/src/components/data-table.tsx @@ -11,12 +11,13 @@ import { import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" -interface DataTableProps { - columns: ColumnDef[] +interface DataTableProps { + // biome-ignore lint/suspicious/noExplicitAny: columns commonly mix accessor value types (string, number, group columns...) + columns: ColumnDef[] data: TData[] } -export function DataTable({ columns, data }: DataTableProps) { +export function DataTable({ columns, data }: DataTableProps) { const table = useReactTable({ data, columns, diff --git a/src/server/actions/guides.ts b/src/server/actions/guides.ts new file mode 100644 index 0000000..579e78a --- /dev/null +++ b/src/server/actions/guides.ts @@ -0,0 +1,31 @@ +"use server" + +import { requireRole } from "../auth" +import { trpc } from "../trpc" + +export async function getAllGuides() { + return await trpc.web.matricole.getAllGuides.query() +} + +export async function createGuide(input: { version: string; date: string; file: File }) { + const { allowed, telegramId } = await requireRole(["owner", "direttivo", "president"]) + if (!allowed) return { guide: null, error: "UNAUTHORIZED" as const } + + const formData = new FormData() + formData.set("version", input.version) + formData.set("date", input.date) + formData.set("file", input.file) + formData.set("createdBy", String(telegramId)) + + const result = await trpc.web.matricole.addGuide.mutate(formData) + if ("error" in result) return { guide: null, error: result.error } + + return { guide: result, error: null } +} + +export async function deleteGuide(id: number) { + const { allowed } = await requireRole(["owner", "direttivo", "president"]) + if (!allowed) return { error: "UNAUTHORIZED" as const } + + return await trpc.web.matricole.deleteGuide.mutate({ id }) +} From 064b59414eaf54a229b0fc8d5ddd6e1e2d91faf3 Mon Sep 17 00:00:00 2001 From: Bianca Date: Fri, 10 Jul 2026 22:46:16 +0200 Subject: [PATCH 2/2] fix: update @polinetwork/backend dependency version to ^0.16.4 --- next.config.ts | 13 ++++++++++++- package.json | 2 +- pnpm-lock.yaml | 10 +++++----- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/next.config.ts b/next.config.ts index 609ecca..3fa77a8 100644 --- a/next.config.ts +++ b/next.config.ts @@ -10,9 +10,20 @@ const config: NextConfig = { ignoreBuildErrors: true, }, output: "standalone", - transpilePackages: ["@t3-oss/env-nextjs", "@t3-oss/env-core"], + transpilePackages: ["@t3-oss/env-core", "@t3-oss/env-nextjs"], + images: { + remotePatterns: [ + { + protocol: "https", + hostname: "**.blob.core.windows.net", + }, + ], + }, experimental: { reactCompiler: true, + serverActions: { + bodySizeLimit: "2mb", + }, }, } diff --git a/package.json b/package.json index cc55384..68257da 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "@base-ui/react": "^1.3.0", "@better-auth/passkey": "^1.5.5", "@hookform/resolvers": "^3.9.1", - "@polinetwork/backend": "^0.16.0", + "@polinetwork/backend": "^0.16.4", "@radix-ui/react-dialog": "^1.1.15", "@t3-oss/env-nextjs": "^0.13.10", "@tanstack/react-table": "^8.21.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index eb0e106..02ffb24 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,8 +18,8 @@ importers: specifier: ^3.9.1 version: 3.10.0(react-hook-form@7.55.0(react@18.3.1)) '@polinetwork/backend': - specifier: ^0.16.0 - version: 0.16.0 + specifier: ^0.16.4 + version: 0.16.4 '@radix-ui/react-dialog': specifier: ^1.1.15 version: 1.1.15(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -841,8 +841,8 @@ packages: resolution: {integrity: sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA==} engines: {node: '>=20.0.0'} - '@polinetwork/backend@0.16.0': - resolution: {integrity: sha512-z/xuzvQv9w8frEtKAw59NvfFEllJnx2lw9YVUwTsP9TXh9tfPu5jP1njVUhFGQe15UE+IxwXH1hV4rO0/9/MLg==} + '@polinetwork/backend@0.16.4': + resolution: {integrity: sha512-IspiFAA92uPfYHR2eKj3FCQxX8lVbvlL6lMmEFxEhJbIIaca3Fq1vduRlyDWSo7QC1bpXqzpjjVDUa/jrK+OtA==} '@radix-ui/number@1.1.1': resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} @@ -4172,7 +4172,7 @@ snapshots: tslib: 2.8.1 tsyringe: 4.10.0 - '@polinetwork/backend@0.16.0': {} + '@polinetwork/backend@0.16.4': {} '@radix-ui/number@1.1.1': {}