Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 65 additions & 10 deletions src/components/card-course-group.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
"use client"

import { cva, type VariantProps } from "class-variance-authority"
import type { IconType } from "react-icons"
import { FaWhatsapp } from "react-icons/fa"
import { FiFlag } from "react-icons/fi"
import { LiaTelegramPlane } from "react-icons/lia"
import { ReportFinderDialog } from "@/components/groups/report/finder-dialog"
import { cn } from "@/lib/utils"
import { Card, CardAction, CardTitle } from "./ui/card"

Expand All @@ -23,16 +27,20 @@ export const cardCourseGroupVariants = cva(
export function CardCourseGroup({
groupName,
waLink,
waGroupId,
iconWhatsApp: IconWhatsApp = FaWhatsapp,
tgLink,
tgGroupId,
iconTelegram: IconTelegram = LiaTelegramPlane,
secondary = false,
stacked = false,
}: {
groupName: string
waLink?: string
waGroupId?: number
iconWhatsApp?: IconType
tgLink?: string
tgGroupId?: number
iconTelegram?: IconType
stacked?: boolean
} & VariantProps<typeof cardCourseGroupVariants>) {
Expand All @@ -48,17 +56,64 @@ export function CardCourseGroup({
{groupName}
</CardTitle>
<div className={cn("flex items-center gap-1.5", stacked ? "md:contents" : "contents")}>
{waLink && (
<a href={waLink} target="_blank" rel="noopener noreferrer" aria-label={`${groupName} su WhatsApp`}>
<CardAction gradient={false} className={actionClassName} icon={IconWhatsApp} iconSize="sm" />
</a>
)}
{tgLink && (
<a href={tgLink} target="_blank" rel="noopener noreferrer" aria-label={`${groupName} su Telegram`}>
<CardAction gradient={false} className={actionClassName} icon={IconTelegram} iconSize="sm" />
</a>
)}
<PlatformAction
platform="wa"
platformLabel="WhatsApp"
link={waLink}
groupId={waGroupId}
icon={IconWhatsApp}
groupName={groupName}
actionClassName={actionClassName}
/>
<PlatformAction
platform="tg"
platformLabel="Telegram"
link={tgLink}
groupId={tgGroupId}
icon={IconTelegram}
groupName={groupName}
actionClassName={actionClassName}
/>
</div>
</Card>
)
}

function PlatformAction({
platform,
platformLabel,
link,
groupId,
icon,
groupName,
actionClassName,
}: {
platform: "wa" | "tg"
platformLabel: string
link?: string
groupId?: number
icon: IconType
groupName: string
actionClassName: string
}) {
if (!link) return null

return (
<>
<a href={link} target="_blank" rel="noopener noreferrer" aria-label={`${groupName} su ${platformLabel}`}>
<CardAction gradient={false} className={actionClassName} icon={icon} iconSize="sm" />
</a>
{groupId !== undefined && (
<ReportFinderDialog
selectedReport={{ groupId, type: platform, reportType: "broken_link", reportedLink: link }}
selectedGroupName={groupName}
trigger={
<button type="button" aria-label={`Segnala il link ${platformLabel} di ${groupName} come non funzionante`}>
<CardAction gradient={false} className="p-2 text-text-secondary sm:p-3.75" icon={FiFlag} iconSize="xs" />
</button>
}
/>
)}
</>
)
}
9 changes: 8 additions & 1 deletion src/components/groups/extra-groups.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,14 @@ export async function ExtraGroups() {
{extraGroups.length > 0 ? (
<div className="flex flex-col gap-3 md:mt-25.75">
{extraGroups.map((g) => (
<CardCourseGroup key={g.key} groupName={g.title} waLink={g.waLink} tgLink={g.tgLink} />
<CardCourseGroup
key={g.key}
groupName={g.title}
waLink={g.waLink}
waGroupId={g.waGroupId}
tgLink={g.tgLink}
tgGroupId={g.tgGroupId}
/>
))}
</div>
) : (
Expand Down
27 changes: 21 additions & 6 deletions src/components/groups/groups-result.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,24 @@ import {
SITE_LABEL,
schoolLabel,
} from "@/utils/labels"
import { mergeGroupsByTitle } from "@/utils/merge-groups"
import { type MergedGroup, mergeGroupsByTitle } from "@/utils/merge-groups"
import { stepHref } from "../../utils/step-href"

function GroupCard({ group, secondary, stacked }: { group: MergedGroup; secondary?: boolean; stacked?: boolean }) {
return (
<CardCourseGroup
key={group.key}
groupName={group.title}
waLink={group.waLink}
waGroupId={group.waGroupId}
tgLink={group.tgLink}
tgGroupId={group.tgGroupId}
secondary={secondary}
stacked={stacked}
/>
)
}

function GroupSection({ title, groups }: { title: string; groups: VisibleGroup[] }) {
const merged = mergeGroupsByTitle(groups)
if (merged.length === 0) return null
Expand All @@ -33,7 +48,7 @@ function GroupSection({ title, groups }: { title: string; groups: VisibleGroup[]
</div>
<div className="flex flex-col gap-3 md:mt-6.75">
{merged.map((g) => (
<CardCourseGroup key={g.key} groupName={g.title} waLink={g.waLink} tgLink={g.tgLink} />
<GroupCard key={g.key} group={g} />
))}
</div>
</div>
Expand Down Expand Up @@ -116,16 +131,16 @@ export async function GroupsResult({
mergedGeneralCourseGroups.length > 0) && (
<div className="flex flex-row flex-wrap gap-3 md:mt-25.75">
{mergedSiteGroups.map((g) => (
<CardCourseGroup key={g.key} groupName={g.title} waLink={g.waLink} tgLink={g.tgLink} stacked />
<GroupCard key={g.key} group={g} stacked />
))}
{mergedSchoolGroups.map((g) => (
<CardCourseGroup key={g.key} groupName={g.title} waLink={g.waLink} tgLink={g.tgLink} stacked />
<GroupCard key={g.key} group={g} stacked />
))}
{mergedLevelGroups.map((g) => (
<CardCourseGroup key={g.key} groupName={g.title} waLink={g.waLink} tgLink={g.tgLink} secondary stacked />
<GroupCard key={g.key} group={g} secondary stacked />
))}
{mergedGeneralCourseGroups.map((g) => (
<CardCourseGroup key={g.key} groupName={g.title} waLink={g.waLink} tgLink={g.tgLink} secondary stacked />
<GroupCard key={g.key} group={g} secondary stacked />
))}
</div>
)}
Expand Down
5 changes: 5 additions & 0 deletions src/components/groups/report/fab.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
"use client"

import { usePathname } from "next/navigation"
import { FiFlag } from "react-icons/fi"
import { ButtonIcon } from "@/components/button-icon"
import { ReportFinderDialog } from "@/components/groups/report/finder-dialog"
import { missingLinkTargetFromPath } from "@/utils/missing-link-target"

export function ReportFab() {
const missingLinkTarget = missingLinkTargetFromPath(usePathname())

return (
<ReportFinderDialog
missingLinkTarget={missingLinkTarget}
trigger={
<ButtonIcon
icon={FiFlag}
Expand Down
22 changes: 20 additions & 2 deletions src/components/groups/report/finder-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ 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 type { MissingLinkTarget } from "@/components/groups/report/missing-link/types"
import { SelectableCardIcon } from "@/components/groups/report/selectable-card-icon"
import { Button } from "@/components/ui/button"
import { Card, CardAction, CardContent } from "@/components/ui/card"
Expand All @@ -21,7 +22,17 @@ import type { BrokenGroupLinkReportInput, MissingGroupLinkReportInput } from "@/

type View = "problem" | "broken" | "missing" | "confirmation" | "done"

export function ReportFinderDialog({ trigger }: { trigger: ReactNode }) {
export function ReportFinderDialog({
trigger,
selectedReport,
selectedGroupName,
missingLinkTarget,
}: {
trigger: ReactNode
selectedReport?: BrokenGroupLinkReportInput
selectedGroupName?: string
missingLinkTarget?: MissingLinkTarget
}) {
const [open, setOpen] = useState(false)
const [view, setView] = useState<View>("problem")
const [report, setReport] = useState<BrokenGroupLinkReportInput | null>(null)
Expand All @@ -41,7 +52,13 @@ export function ReportFinderDialog({ trigger }: { trigger: ReactNode }) {

function handleOpenChange(next: boolean) {
setOpen(next)
if (!next) reset()
if (!next) {
reset()
} else if (selectedReport && selectedGroupName) {
setReport(selectedReport)
setGroupName(selectedGroupName)
setView("confirmation")
}
}

async function submit(input: BrokenGroupLinkReportInput | MissingGroupLinkReportInput) {
Expand Down Expand Up @@ -123,6 +140,7 @@ export function ReportFinderDialog({ trigger }: { trigger: ReactNode }) {
onBack={reset}
onSubmit={(missingReport) => void submit(missingReport)}
status={status}
target={missingLinkTarget}
/>
) : (
<>
Expand Down
27 changes: 20 additions & 7 deletions src/components/groups/report/missing-link/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,26 @@ import { CourseStep } from "./course-step"
import { DetailsStep } from "./details-step"
import { LevelStep } from "./level-step"
import { SchoolStep } from "./school-step"
import type { ReportMissingLinkCategory, ReportMissingLinkSelection, ReportMissingLinkStep } from "./types"
import type {
MissingLinkTarget,
ReportMissingLinkCategory,
ReportMissingLinkSelection,
ReportMissingLinkStep,
} from "./types"

export function ReportMissingLinkFlow({
onBack,
onSubmit,
status,
target,
}: {
onBack: () => void
onSubmit: (report: MissingGroupLinkReportInput) => void
status: "idle" | "submitting" | "error"
target?: MissingLinkTarget
}) {
const [step, setStep] = useState<ReportMissingLinkStep>("category")
const [category, setCategory] = useState<ReportMissingLinkCategory | null>(null)
const [step, setStep] = useState<ReportMissingLinkStep>(target ? "details" : "category")
const [category, setCategory] = useState<ReportMissingLinkCategory | null>(target?.category ?? null)
const [selection, setSelection] = useState<ReportMissingLinkSelection>({ school: null, level: null, course: null })
const [courses, setCourses] = useState<string[] | null>(null)
const [details, setDetails] = useState("")
Expand All @@ -45,12 +52,13 @@ export function ReportMissingLinkFlow({

const school = selection.school ? getSchool(selection.school) : undefined
const level = selection.school && selection.level ? getLevel(selection.school, selection.level) : undefined
const path =
const selectedPath =
category === "extra"
? "Gruppi Extra"
: [school?.name, level?.name, selection.course ? humanizeSlug(selection.course) : undefined]
.filter(Boolean)
.join(" · ")
const path = target?.path ?? selectedPath

function selectCategory(next: ReportMissingLinkCategory) {
setCategory(next)
Expand Down Expand Up @@ -109,22 +117,27 @@ export function ReportMissingLinkFlow({
}

// step === "details"
setStep(detailsFrom ?? "category")
if (target) {
onBack()
} else {
setStep(detailsFrom ?? "category")
}
}

function submit() {
if (!details.trim() || status === "submitting") return

const label =
category === "extra"
target?.label ??
(category === "extra"
? "Gruppi Extra"
: selection.school && selection.course && selection.level
? courseLabel(selection.school, selection.level, selection.course)
: selection.school && selection.level
? levelLabel(selection.school, selection.level)
: selection.school
? schoolLabel(selection.school)
: null
: null)

if (label === null) return

Expand Down
6 changes: 6 additions & 0 deletions src/components/groups/report/missing-link/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,9 @@ export type ReportMissingLinkSelection = {
level: string | null
course: string | null
}

export type MissingLinkTarget = {
category: ReportMissingLinkCategory
label: string
path: string
}
11 changes: 10 additions & 1 deletion src/utils/merge-groups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ export type MergedGroup = {
key: string
title: string
waLink?: string
waGroupId?: number
tgLink?: string
tgGroupId?: number
}

/**
Expand All @@ -18,12 +20,19 @@ export function mergeGroupsByTitle(groups: VisibleGroup[]): MergedGroup[] {
const result: MergedGroup[] = []
for (const g of groups) {
const linkField = g.type === "wa" ? "waLink" : "tgLink"
const groupIdField = g.type === "wa" ? "waGroupId" : "tgGroupId"
const candidates = openByTitle.get(g.title) ?? []
const openEntry = candidates.find((m) => !m[linkField])
if (openEntry) {
openEntry[linkField] = g.link
openEntry[groupIdField] = g.id
} else {
const merged: MergedGroup = { key: `${g.type}:${g.id}`, title: g.title, [linkField]: g.link }
const merged: MergedGroup = {
key: `${g.type}:${g.id}`,
title: g.title,
[linkField]: g.link,
[groupIdField]: g.id,
}
candidates.push(merged)
openByTitle.set(g.title, candidates)
result.push(merged)
Expand Down
34 changes: 34 additions & 0 deletions src/utils/missing-link-target.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { getLevel, getSchool } from "@/components/groups/constants"
import type { MissingLinkTarget } from "@/components/groups/report/missing-link/types"
import { courseLabel, humanizeSlug, levelLabel, schoolLabel } from "@/utils/labels"

export function missingLinkTargetFromPath(pathname: string): MissingLinkTarget | undefined {
if (pathname === "/groups/extra") {
return { category: "extra", label: "Gruppi Extra", path: "Gruppi Extra" }
}

const [, groups, category, schoolSlug, levelSlug, course] = pathname.split("/")
if (groups !== "groups" || category !== "didattica" || !schoolSlug) return undefined

const school = getSchool(schoolSlug)
if (!school) return undefined
if (!levelSlug) {
return { category: "didattica", label: schoolLabel(schoolSlug), path: school.name }
}

const level = getLevel(schoolSlug, levelSlug)
if (!level) return undefined
if (!course) {
return {
category: "didattica",
label: levelLabel(schoolSlug, levelSlug),
path: `${school.name} · ${level.name}`,
}
}

return {
category: "didattica",
label: courseLabel(schoolSlug, levelSlug, course),
path: `${school.name} · ${level.name} · ${humanizeSlug(course)}`,
}
}
Loading