From b342211da0e381cb2c85c52d5aa4b8bc8dbc5945 Mon Sep 17 00:00:00 2001 From: Bianca Date: Tue, 8 Sep 2026 21:22:45 +0200 Subject: [PATCH] feat: implement reporting functionality for broken and missing group links --- src/app/groups/layout.tsx | 11 ++ src/components/card-course-group.tsx | 2 +- src/components/groups/constants.ts | 11 ++ src/components/groups/groups-result.tsx | 3 +- .../groups/report/broken-link-flow.tsx | 32 ++++ src/components/groups/report/fab.tsx | 21 ++ .../groups/report/finder-dialog.tsx | 139 ++++++++++++++ .../report/missing-link/category-step.tsx | 24 +++ .../report/missing-link/course-step.tsx | 54 ++++++ .../report/missing-link/details-step.tsx | 61 ++++++ .../groups/report/missing-link/index.tsx | 181 ++++++++++++++++++ .../groups/report/missing-link/level-step.tsx | 43 +++++ .../report/missing-link/school-step.tsx | 23 +++ .../groups/report/missing-link/types.ts | 9 + .../groups/report/selectable-card-icon.tsx | 22 +++ src/components/groups/school-step.tsx | 14 +- src/components/home/group-search.tsx | 44 +++-- src/components/ui/dialog.tsx | 158 +++++++++++++++ src/queries/groups.ts | 20 +- src/queries/types.ts | 9 + src/utils/merge-groups.ts | 2 +- 21 files changed, 848 insertions(+), 35 deletions(-) create mode 100644 src/app/groups/layout.tsx create mode 100644 src/components/groups/report/broken-link-flow.tsx create mode 100644 src/components/groups/report/fab.tsx create mode 100644 src/components/groups/report/finder-dialog.tsx create mode 100644 src/components/groups/report/missing-link/category-step.tsx create mode 100644 src/components/groups/report/missing-link/course-step.tsx create mode 100644 src/components/groups/report/missing-link/details-step.tsx create mode 100644 src/components/groups/report/missing-link/index.tsx create mode 100644 src/components/groups/report/missing-link/level-step.tsx create mode 100644 src/components/groups/report/missing-link/school-step.tsx create mode 100644 src/components/groups/report/missing-link/types.ts create mode 100644 src/components/groups/report/selectable-card-icon.tsx create mode 100644 src/components/ui/dialog.tsx create mode 100644 src/queries/types.ts diff --git a/src/app/groups/layout.tsx b/src/app/groups/layout.tsx new file mode 100644 index 0000000..957b823 --- /dev/null +++ b/src/app/groups/layout.tsx @@ -0,0 +1,11 @@ +import type { ReactNode } from "react" +import { ReportFab } from "@/components/groups/report/fab" + +export default function GroupsLayout({ children }: { children: ReactNode }) { + return ( + <> + {children} + + + ) +} diff --git a/src/components/card-course-group.tsx b/src/components/card-course-group.tsx index f1d5532..2813272 100644 --- a/src/components/card-course-group.tsx +++ b/src/components/card-course-group.tsx @@ -47,7 +47,7 @@ export function CardCourseGroup({ {groupName} -
+
{waLink && ( diff --git a/src/components/groups/constants.ts b/src/components/groups/constants.ts index d9479db..a2e702a 100644 --- a/src/components/groups/constants.ts +++ b/src/components/groups/constants.ts @@ -1,3 +1,5 @@ +import { FiBox } from "react-icons/fi" +import type { GradientIconType } from "@/components/gradient-icon" import type { Level, School } from "@/components/groups/types" export const SCHOOLS: School[] = [ @@ -7,6 +9,15 @@ export const SCHOOLS: School[] = [ { slug: "design", name: "Scuola di Design" }, ] +export const SCHOOL_ICONS: Record = { + "ingegneria-industriale-informazione": "/icons/ingegneria.png", + auic: "/icons/architettura.png", + "ingegneria-civile-ambientale-territoriale": "/icons/civile.png", + design: "/icons/design.png", +} + +export const DEFAULT_SCHOOL_ICON: GradientIconType = FiBox + export const LEVELS: Level[] = [ { slug: "triennale", name: "Triennale" }, { slug: "magistrale", name: "Magistrale" }, diff --git a/src/components/groups/groups-result.tsx b/src/components/groups/groups-result.tsx index ebfee8a..ba15d7f 100644 --- a/src/components/groups/groups-result.tsx +++ b/src/components/groups/groups-result.tsx @@ -3,7 +3,8 @@ import { notFound } from "next/navigation" import { FiArrowLeft, FiX } from "react-icons/fi" import { CardCourseGroup } from "@/components/card-course-group" import { getLevel, getSchool } from "@/components/groups/constants" -import { getVisibleGroups, type VisibleGroup } from "@/queries/groups" +import { getVisibleGroups } from "@/queries/groups" +import type { VisibleGroup } from "@/queries/types" import { cohortLabelText, courseCohortFromLabels, diff --git a/src/components/groups/report/broken-link-flow.tsx b/src/components/groups/report/broken-link-flow.tsx new file mode 100644 index 0000000..c5e5738 --- /dev/null +++ b/src/components/groups/report/broken-link-flow.tsx @@ -0,0 +1,32 @@ +import { GroupSearch, type SearchGroup } from "@/components/home/group-search" +import { DialogDescription, DialogHeader } from "@/components/ui/dialog" +import type { BrokenGroupLinkReportInput } from "@/queries/types" + +export function ReportBrokenLinkFlow({ + onBack, + onSelect, +}: { + onBack: () => void + onSelect: (report: BrokenGroupLinkReportInput, groupName: string) => void +}) { + function selectGroup(group: SearchGroup) { + onSelect( + { + groupId: group.telegramId, + type: group.type, + reportType: "broken_link", + reportedLink: group.link, + }, + group.title + ) + } + + return ( + <> + + Cerca il gruppo a cui si riferisce la segnalazione. + + + + ) +} diff --git a/src/components/groups/report/fab.tsx b/src/components/groups/report/fab.tsx new file mode 100644 index 0000000..32dbc07 --- /dev/null +++ b/src/components/groups/report/fab.tsx @@ -0,0 +1,21 @@ +"use client" + +import { FiFlag } from "react-icons/fi" +import { ButtonIcon } from "@/components/button-icon" +import { ReportFinderDialog } from "@/components/groups/report/finder-dialog" + +export function ReportFab() { + return ( + + } + /> + ) +} diff --git a/src/components/groups/report/finder-dialog.tsx b/src/components/groups/report/finder-dialog.tsx new file mode 100644 index 0000000..19f661f --- /dev/null +++ b/src/components/groups/report/finder-dialog.tsx @@ -0,0 +1,139 @@ +"use client" + +import { type ReactNode, useRef, useState } from "react" +import { FiAlertCircle, FiCheckCircle, FiLink } from "react-icons/fi" +import { ButtonIcon } from "@/components/button-icon" +import { ReportBrokenLinkFlow } from "@/components/groups/report/broken-link-flow" +import { ReportMissingLinkFlow } from "@/components/groups/report/missing-link" +import { SelectableCardIcon } from "@/components/groups/report/selectable-card-icon" +import { Button } from "@/components/ui/button" +import { Card, CardAction, CardContent } from "@/components/ui/card" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTrigger, +} from "@/components/ui/dialog" +import { reportGroupLink } from "@/queries/groups" +import type { BrokenGroupLinkReportInput, MissingGroupLinkReportInput } from "@/queries/types" + +type View = "problem" | "broken" | "missing" | "confirmation" | "done" + +export function ReportFinderDialog({ trigger }: { trigger: ReactNode }) { + const [open, setOpen] = useState(false) + const [view, setView] = useState("problem") + const [report, setReport] = useState(null) + const [groupName, setGroupName] = useState("") + const [status, setStatus] = useState<"idle" | "submitting" | "error">("idle") + const inFlightRef = useRef(false) + const requestIdRef = useRef(0) + + function reset() { + requestIdRef.current++ + inFlightRef.current = false + setView("problem") + setReport(null) + setGroupName("") + setStatus("idle") + } + + function handleOpenChange(next: boolean) { + setOpen(next) + if (!next) reset() + } + + async function submit(input: BrokenGroupLinkReportInput | MissingGroupLinkReportInput) { + if (inFlightRef.current) return + inFlightRef.current = true + const requestId = ++requestIdRef.current + + setStatus("submitting") + const result = await reportGroupLink(input) + if (requestId !== requestIdRef.current) return // dialog was reset/closed meanwhile + + inFlightRef.current = false + if (result.ok) { + setView("done") + setStatus("idle") + return + } + + setStatus("error") + } + + return ( + + {trigger} + + {view === "done" ? ( + <> + + Grazie, la verificheremo al più presto. + + + + + + ) : view === "confirmation" && report ? ( + <> + { + setView("broken") + setStatus("idle") + }} + > + Controlla i dettagli prima di inviarla. + + + + + Il link {report.type === "tg" ? "Telegram" : "WhatsApp"} di “{groupName}” non funziona. + + + {status === "error" && ( +

+ Non è stato possibile inviare la segnalazione. Riprova tra qualche minuto. +

+ )} + + void submit(report)} + /> + + + ) : view === "broken" ? ( + { + setReport(selectedReport) + setGroupName(selectedGroupName) + setView("confirmation") + }} + /> + ) : view === "missing" ? ( + void submit(missingReport)} + status={status} + /> + ) : ( + <> + +
+ setView("broken")} /> + setView("missing")} /> +
+ + )} +
+
+ ) +} diff --git a/src/components/groups/report/missing-link/category-step.tsx b/src/components/groups/report/missing-link/category-step.tsx new file mode 100644 index 0000000..9e4cbe0 --- /dev/null +++ b/src/components/groups/report/missing-link/category-step.tsx @@ -0,0 +1,24 @@ +import { FiBook, FiStar } from "react-icons/fi" +import { SelectableCardIcon } from "@/components/groups/report/selectable-card-icon" +import { DialogDescription, DialogHeader } from "@/components/ui/dialog" +import type { ReportMissingLinkCategory } from "./types" + +export function CategoryStep({ + onBack, + onSelect, +}: { + onBack: () => void + onSelect: (category: ReportMissingLinkCategory) => void +}) { + return ( + <> + + Che tipo di gruppo manca? + +
+ onSelect("didattica")} /> + onSelect("extra")} /> +
+ + ) +} diff --git a/src/components/groups/report/missing-link/course-step.tsx b/src/components/groups/report/missing-link/course-step.tsx new file mode 100644 index 0000000..b5f0105 --- /dev/null +++ b/src/components/groups/report/missing-link/course-step.tsx @@ -0,0 +1,54 @@ +import { CardCourse } from "@/components/card-course" +import { Spinner } from "@/components/spinner" +import { Button } from "@/components/ui/button" +import { DialogDescription, DialogFooter, DialogHeader } from "@/components/ui/dialog" +import { humanizeSlug } from "@/utils/labels" + +export function CourseStep({ + path, + courses, + onBack, + onSelectCourse, + onSpecifyMissing, +}: { + path: string + courses: string[] | null + onBack: () => void + onSelectCourse: (course: string) => void + onSpecifyMissing: () => void +}) { + return ( + <> + + {path} + +
+ {courses ? ( + courses.length > 0 ? ( + courses.map((course) => ( + + )) + ) : ( +

Nessun corso disponibile al momento.

+ ) + ) : ( +
+ +
+ )} +
+ + + + + ) +} diff --git a/src/components/groups/report/missing-link/details-step.tsx b/src/components/groups/report/missing-link/details-step.tsx new file mode 100644 index 0000000..b906708 --- /dev/null +++ b/src/components/groups/report/missing-link/details-step.tsx @@ -0,0 +1,61 @@ +import { FiCheckCircle } from "react-icons/fi" +import { ButtonIcon } from "@/components/button-icon" +import { Glass } from "@/components/glass" +import { DialogDescription, DialogFooter, DialogHeader } from "@/components/ui/dialog" +import type { ReportMissingLinkCategory } from "./types" + +export function DetailsStep({ + path, + category, + details, + status, + onBack, + onChangeDetails, + onSubmit, +}: { + path: string + category: ReportMissingLinkCategory | null + details: string + status: "idle" | "submitting" | "error" + onBack: () => void + onChangeDetails: (value: string) => void + onSubmit: () => void +}) { + return ( + <> + + {path} + +