diff --git a/package.json b/package.json
index fd5d14d0..34993dd8 100644
--- a/package.json
+++ b/package.json
@@ -26,6 +26,7 @@
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-toggle": "^1.1.10",
"@radix-ui/react-toggle-group": "^1.1.11",
+ "@radix-ui/react-tooltip": "^1.2.8",
"@tailwindcss/vite": "^4.1.17",
"@tanstack/react-query": "^5.90.12",
"@tanstack/react-router": "^1.139.16",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 3b1d2ae2..a29254a4 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -38,6 +38,9 @@ importers:
'@radix-ui/react-toggle-group':
specifier: ^1.1.11
version: 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
+ '@radix-ui/react-tooltip':
+ specifier: ^1.2.8
+ version: 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
'@tailwindcss/vite':
specifier: ^4.1.17
version: 4.1.17(vite@7.2.6(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0))
@@ -974,6 +977,19 @@ packages:
'@types/react-dom':
optional: true
+ '@radix-ui/react-tooltip@1.2.8':
+ resolution: {integrity: sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
'@radix-ui/react-use-callback-ref@1.1.1':
resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==}
peerDependencies:
@@ -2742,6 +2758,26 @@ snapshots:
'@types/react': 19.2.7
'@types/react-dom': 19.2.3(@types/react@19.2.7)
+ '@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.1)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.1)
+ '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.2.7)(react@19.2.1)
+ '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
+ '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
+ '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
+ '@radix-ui/react-slot': 1.2.3(@types/react@19.2.7)(react@19.2.1)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.7)(react@19.2.1)
+ '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
+ react: 19.2.1
+ react-dom: 19.2.1(react@19.2.1)
+ optionalDependencies:
+ '@types/react': 19.2.7
+ '@types/react-dom': 19.2.3(@types/react@19.2.7)
+
'@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.7)(react@19.2.1)':
dependencies:
react: 19.2.1
diff --git a/src/app/__root.tsx b/src/app/__root.tsx
index 7ed3b464..6ac04091 100644
--- a/src/app/__root.tsx
+++ b/src/app/__root.tsx
@@ -5,6 +5,7 @@ import { IconContext } from "react-icons"
import DevSettings from "@/components/DevSettings"
import Layout from "@/components/Layout"
import { Toaster } from "@/components/ui/sonner"
+import { TooltipProvider } from "@/components/ui/tooltip"
import ContextProvider from "@/contexts/ContextProvider"
export const queryClient = new QueryClient()
@@ -19,14 +20,16 @@ function RootComponent() {
-
-
-
+
+
+
+
- {import.meta.env.DEV && }
-
-
-
+ {import.meta.env.DEV && }
+
+
+
+
diff --git a/src/app/_home/$school/$year/$id/index.tsx b/src/app/_home/$school/$year/$id/index.tsx
index a20cf8f5..db42b493 100644
--- a/src/app/_home/$school/$year/$id/index.tsx
+++ b/src/app/_home/$school/$year/$id/index.tsx
@@ -178,7 +178,7 @@ function RouteComponent() {
if (error)
return (
- Error: {error.message}
+ {error.message}
)
return
diff --git a/src/app/_home/$school/$year/index.tsx b/src/app/_home/$school/$year/index.tsx
index c81d3bef..6a858916 100644
--- a/src/app/_home/$school/$year/index.tsx
+++ b/src/app/_home/$school/$year/index.tsx
@@ -1,12 +1,9 @@
import { useQuery } from "@tanstack/react-query"
-import { createFileRoute, Link, redirect } from "@tanstack/react-router"
+import { createFileRoute, redirect } from "@tanstack/react-router"
import Page from "@/components/custom-ui/Page"
-import PhaseFlag from "@/components/custom-ui/PhaseFlag"
-import { ButtonGrid } from "@/components/Homepage/ButtonGrid"
+import { RankingSelector } from "@/components/Homepage/RankingSelector"
import PathBreadcrumb from "@/components/PathBreadcrumb"
-import { Button } from "@/components/ui/button"
import { useQueries } from "@/hooks/use-queries"
-import { getPhaseGroups, phaseGroupLabel, phaseLinkLabel } from "@/utils/phase"
import type { PhaseLink } from "@/utils/types/data/phase"
import { isSchool } from "@/utils/types/data/school"
@@ -24,63 +21,31 @@ function RouteComponent() {
const { school, year } = Route.useParams()
const queries = useQueries()
const { data, isPending } = useQuery(queries.index)
+
if (!data || isPending) return null
- const yearData = data[school]?.[year] ?? []
- const groups = getPhaseGroups(yearData).entriesArr()
+ const yearData = data[school]?.[year] ?? []
+ const phases: PhaseLink[] = yearData.map((entry) => ({
+ ...entry.phase,
+ id: entry.id,
+ }))
return (
- Scegli una graduatoria
- {groups.map(([n, phases]) => (
- 1}
- />
- ))}
-
- )
-}
+
+
+
+ Seleziona graduatoria
+
+
+ Scegli la graduatoria da consultare per{" "}
+ {school} {year}
+
+
-type GroupProps = {
- primary: number
- phases: PhaseLink[]
- showGeneral: boolean
-}
-
-function Group({ primary, phases, showGeneral }: GroupProps) {
- const { school, year } = Route.useParams()
- return (
- <>
- {(showGeneral || primary !== 0) &&
{phaseGroupLabel(primary)}
}
-
- {phases.map((phase) => (
-
-
-
- {phaseLinkLabel(phase)}
-
-
-
-
- ))}
-
- >
+
+
+
)
}
diff --git a/src/app/_home/$school/index.tsx b/src/app/_home/$school/index.tsx
index 6fa5df23..f59a7a9b 100644
--- a/src/app/_home/$school/index.tsx
+++ b/src/app/_home/$school/index.tsx
@@ -1,13 +1,11 @@
import { useQuery } from "@tanstack/react-query"
import { createFileRoute, Link, redirect } from "@tanstack/react-router"
-import { useState } from "react"
import Page from "@/components/custom-ui/Page"
-import Spinner from "@/components/custom-ui/Spinner"
-import { ButtonGrid } from "@/components/Homepage/ButtonGrid"
import PathBreadcrumb from "@/components/PathBreadcrumb"
-import { Button } from "@/components/ui/button"
+import { Badge } from "@/components/ui/badge"
import { useQueries } from "@/hooks/use-queries"
import { isSchool } from "@/utils/types/data/school"
+import { cn } from "@/utils/ui"
export const Route = createFileRoute("/_home/$school/")({
component: RouteComponent,
@@ -20,39 +18,62 @@ export const Route = createFileRoute("/_home/$school/")({
})
function RouteComponent() {
- const [clicked, setClicked] = useState(false)
const { school } = Route.useParams()
const queries = useQueries()
const { data, isPending } = useQuery(queries.index)
if (!data || isPending) return null
- const years = Object.keys(data[school] ?? {}).map(Number)
+ const years = Object.keys(data[school] ?? {})
+ .map(Number)
+ .sort((a, b) => b - a)
- return clicked ? (
-
-
-
- ) : (
+ return (
- Scegli un anno di immatricolazione
-
- {years
- .sort((a, b) => b - a)
- .map((year) => (
+
+
+ Anno di immatricolazione
+
+
+ Seleziona l'anno accademico di cui vuoi consultare le graduatorie
+
+
+
+
+ {years.map((year, index) => {
+ const isFirst = index === 0
+
+ return (
setClicked(true)}
+ className="group"
>
-
- {year}
-
+
+ {year}
+
- ))}
-
+ )
+ })}
+
+
+ {years.length > 0 && (
+
+ {years.length}{" "}
+ {years.length === 1 ? "anno disponibile" : "anni disponibili"} dal{" "}
+ {years[years.length - 1]} al {years[0]}
+
+ )}
)
}
diff --git a/src/app/_home/index.tsx b/src/app/_home/index.tsx
index 33248f50..c2a11fac 100644
--- a/src/app/_home/index.tsx
+++ b/src/app/_home/index.tsx
@@ -1,11 +1,10 @@
import { useQuery } from "@tanstack/react-query"
-import { createFileRoute, Link } from "@tanstack/react-router"
+import { createFileRoute } from "@tanstack/react-router"
import Alert from "@/components/custom-ui/Alert"
import Page from "@/components/custom-ui/Page"
import Spinner from "@/components/custom-ui/Spinner"
-import { ButtonGrid } from "@/components/Homepage/ButtonGrid"
-import { SchoolEmoji } from "@/components/school-emoji"
-import { Button } from "@/components/ui/button"
+import { ArchiveTip } from "@/components/Homepage/ArchiveTip"
+import { SchoolCard } from "@/components/Homepage/SchoolCard"
import { useQueries } from "@/hooks/use-queries"
export const Route = createFileRoute("/_home/")({
@@ -22,40 +21,36 @@ function RouteComponent() {
return (
-
-
- 👋 Ciao!{" "}
- Questo sito raccoglie {" "}
- lo storico {" "}
- delle graduatorie {" "}
- del Politecnico di Milano.
-
-
- Inizia scegliendo l'area di studi di tuo interesse
-
+
+
+
+ Storico Graduatorie PoliMi
+
+
+ Consulta le graduatorie storiche del Politecnico di Milano.
+ Seleziona un'area di studi per iniziare.
+
+
+
{schools && (
-
+
{schools.map((school) => (
-
-
-
- {school}
-
-
+
))}
-
+
+ )}
+
+ {isPending && (
+
+
+
)}
- {isPending && }
{error instanceof Error && {error.message} }
+
+
+
)
diff --git a/src/components/Homepage/ArchiveTip.tsx b/src/components/Homepage/ArchiveTip.tsx
new file mode 100644
index 00000000..9744190e
--- /dev/null
+++ b/src/components/Homepage/ArchiveTip.tsx
@@ -0,0 +1,79 @@
+import { useMemo } from "react"
+import { AiOutlineBulb } from "react-icons/ai"
+import type { BySchoolYearIndex } from "@/utils/types/data/ranking"
+
+type Props = {
+ data: BySchoolYearIndex
+}
+
+export function ArchiveTip({ data }: Props) {
+ const stats = useMemo(() => {
+ const years = new Set
()
+ let totalRankings = 0
+
+ for (const schoolData of Object.values(data)) {
+ for (const [year, rankings] of Object.entries(schoolData)) {
+ years.add(Number(year))
+ totalRankings += rankings.length
+ }
+ }
+
+ const sortedYears = [...years].sort((a, b) => a - b)
+
+ return {
+ yearCount: years.size,
+ minYear: sortedYears[0],
+ maxYear: sortedYears[sortedYears.length - 1],
+ totalRankings,
+ }
+ }, [data])
+
+ return (
+
+ {/* Decorative background elements */}
+
+
+
+
+ {/* Stats line with pill badges */}
+
+
+ L'archivio contiene{" "}
+
+ {stats.totalRankings} graduatorie
+ {" "}
+ distribuite su{" "}
+
+ {stats.yearCount} anni
+ {" "}
+
+ ({stats.minYear} - {stats.maxYear})
+
+
+
+ {/* Warning with subtle styling -- TODO: hidden for now, maybe readd it later (hidden -> flex) */}
+
+
+
+
+ Alcune graduatorie potrebbero non essere disponibili per determinati
+ anni o fasi.
+
+
+
+ )
+}
diff --git a/src/components/Homepage/RankingSelector.tsx b/src/components/Homepage/RankingSelector.tsx
new file mode 100644
index 00000000..1f4ce422
--- /dev/null
+++ b/src/components/Homepage/RankingSelector.tsx
@@ -0,0 +1,267 @@
+import { Link } from "@tanstack/react-router"
+import { useMemo } from "react"
+import { phaseGroupLabel } from "@/utils/phase"
+import { numberToRoman } from "@/utils/strings/numbers"
+import type { PhaseLink } from "@/utils/types/data/phase"
+import type { School } from "@/utils/types/data/school"
+import { cn } from "@/utils/ui"
+import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip"
+
+type Props = {
+ phases: PhaseLink[]
+ school: School
+ year: number
+}
+
+type Language = "IT" | "EN"
+
+type LanguageConfig = {
+ lang: Language
+ title: string
+ flag: string
+ headerStyles: string
+ separatorStyles: string
+}
+
+const LANGUAGES: LanguageConfig[] = [
+ {
+ lang: "IT",
+ title: "Italiano",
+ flag: "🇮🇹",
+ headerStyles:
+ "bg-green-100 text-green-800 dark:bg-green-900/50 dark:text-green-300",
+ separatorStyles:
+ "bg-green-50 text-green-700 dark:bg-green-900/20 dark:text-green-400",
+ },
+ {
+ lang: "EN",
+ title: "English",
+ flag: "🇬🇧",
+ headerStyles:
+ "bg-blue-100 text-blue-800 dark:bg-blue-900/50 dark:text-blue-300",
+ separatorStyles:
+ "bg-blue-50 text-blue-700 dark:bg-blue-900/20 dark:text-blue-400",
+ },
+]
+
+// Group phases by primary, then by secondary, keeping extraEu/normal pairs together
+type PhaseGroup = {
+ primary: number
+ secondaries: Array<{
+ secondary: number
+ normal?: PhaseLink
+ extraEu?: PhaseLink
+ }>
+}
+
+function groupPhases(phases: PhaseLink[], lang: Language): PhaseGroup[] {
+ const langPhases = phases.filter((p) => p.language === lang)
+
+ // Group by primary
+ const byPrimary = new Map()
+ for (const phase of langPhases) {
+ const existing = byPrimary.get(phase.primary) ?? []
+ existing.push(phase)
+ byPrimary.set(phase.primary, existing)
+ }
+
+ // For each primary, group by secondary and pair normal/extraEu
+ const result: PhaseGroup[] = []
+ for (const [primary, phasesInPrimary] of [...byPrimary.entries()].sort(
+ ([a], [b]) => a - b
+ )) {
+ const bySecondary = new Map<
+ number,
+ { normal?: PhaseLink; extraEu?: PhaseLink }
+ >()
+
+ for (const phase of phasesInPrimary) {
+ const existing = bySecondary.get(phase.secondary) ?? {}
+ if (phase.isExtraEu) {
+ existing.extraEu = phase
+ } else {
+ existing.normal = phase
+ }
+ bySecondary.set(phase.secondary, existing)
+ }
+
+ result.push({
+ primary,
+ secondaries: [...bySecondary.entries()]
+ .sort(([a], [b]) => a - b)
+ .map(([secondary, pair]) => ({ secondary, ...pair })),
+ })
+ }
+
+ return result
+}
+
+export function RankingSelector({ phases, school, year }: Props) {
+ const columnsData = useMemo(() => {
+ return LANGUAGES.map((config) => ({
+ config,
+ groups: groupPhases(phases, config.lang),
+ })).filter((c) => c.groups.length > 0)
+ }, [phases])
+
+ return (
+
+ {/* Columns grid */}
+
+ {columnsData.map(({ config, groups }) => (
+
+ ))}
+
+
+ {/* Summary */}
+
+
+ {phases.length} graduatorie disponibili in totale.{" "}
+
+
+
+ Perché non trovo una graduatoria?
+
+
+
+ Il Politecnico ha cambiato negli anni le modalità di pubblicazione
+ delle graduatorie, oltre a eliminare molto rapidamente i file
+ grezzi dai server pubblici, rendendo malfunzionante il nostro
+ script di scraping e parsing. Durante i processi (lenti) di
+ riscrittura/manutenzione dello script, alcune di queste
+ graduatorie sono andate perse o mai individuate perché già
+ eliminate.
+
+
+
+
+
+ )
+}
+
+function LanguageColumn({
+ config,
+ groups,
+ school,
+ year,
+}: {
+ config: LanguageConfig
+ groups: PhaseGroup[]
+ school: School
+ year: number
+}) {
+ return (
+
+ {/* Column header */}
+
+ {config.title}
+ {config.flag}
+
+
+ {/* Content with phase groups */}
+
+ {groups.map((group) => (
+
+ {/* Phase separator - only show if multiple phases */}
+
+ {phaseGroupLabel(group.primary)}
+
+
+ {/* Rankings rows - normal first, then extra-EU */}
+
+
+ ))}
+
+
+ )
+}
+
+function PhaseRankings({
+ group,
+ school,
+ year,
+}: {
+ group: PhaseGroup
+ school: School
+ year: number
+}) {
+ const normalPhases = group.secondaries
+ .filter((s) => s.normal)
+ .map((s) => s.normal as PhaseLink)
+ const extraEuPhases = group.secondaries
+ .filter((s) => s.extraEu)
+ .map((s) => s.extraEu as PhaseLink)
+
+ return (
+
+ {/* Normal rankings row */}
+ {normalPhases.length > 0 && (
+
+ {normalPhases.map((phase) => (
+
+ {getRankingLabel(phase)}
+
+ ))}
+
+ )}
+
+ {/* Extra-UE rankings row with dashed border and label */}
+ {extraEuPhases.length > 0 && (
+
+ {/* Label */}
+
+ Extra-UE
+
+ {/* Buttons */}
+
+ {extraEuPhases.map((phase) => (
+
+ {getRankingLabel(phase)}
+
+ ))}
+
+
+ )}
+
+ )
+}
+
+function getRankingLabel(phase: PhaseLink) {
+ if (phase.secondary === 0) {
+ return "Generica"
+ }
+ return numberToRoman(phase.secondary)
+}
diff --git a/src/components/Homepage/SchoolCard.tsx b/src/components/Homepage/SchoolCard.tsx
new file mode 100644
index 00000000..8ede03bf
--- /dev/null
+++ b/src/components/Homepage/SchoolCard.tsx
@@ -0,0 +1,78 @@
+import { Link } from "@tanstack/react-router"
+import type { School } from "@/utils/types/data/school"
+import { cn } from "@/utils/ui"
+
+type SchoolConfig = {
+ icon: string
+ description: string
+ gradient: string
+ hoverGradient: string
+}
+
+const SCHOOL_CONFIG: Record = {
+ Architettura: {
+ icon: "🏛️",
+ description: "Progettazione architettonica e spaziale",
+ gradient: "from-amber-500/10 to-orange-500/10",
+ hoverGradient: "hover:from-amber-500/20 hover:to-orange-500/20",
+ },
+ Design: {
+ icon: "🎨",
+ description: "Interni, prodotto industriale, comunicazione e moda",
+ gradient: "from-pink-500/10 to-purple-500/10",
+ hoverGradient: "hover:from-pink-500/20 hover:to-purple-500/20",
+ },
+ Ingegneria: {
+ icon: "⚙️",
+ description: "Scienze e tecnologie ingegneristiche",
+ gradient: "from-blue-500/10 to-cyan-500/10",
+ hoverGradient: "hover:from-blue-500/20 hover:to-cyan-500/20",
+ },
+ Urbanistica: {
+ icon: "🏙️",
+ description: "Pianificazione urbana e territoriale",
+ gradient: "from-green-500/10 to-emerald-500/10",
+ hoverGradient: "hover:from-green-500/20 hover:to-emerald-500/20",
+ },
+}
+
+type Props = {
+ school: School
+}
+
+export function SchoolCard({ school }: Props) {
+ const config = SCHOOL_CONFIG[school]
+
+ return (
+
+
+
+
+ {config.icon}
+
+
+
+
+ {school}
+
+
+ {config.description}
+
+
+
+
+
+ )
+}
diff --git a/src/components/PathBreadcrumb.tsx b/src/components/PathBreadcrumb.tsx
index 03fbfe28..5bd9a4a2 100644
--- a/src/components/PathBreadcrumb.tsx
+++ b/src/components/PathBreadcrumb.tsx
@@ -1,12 +1,19 @@
+import { useSuspenseQuery } from "@tanstack/react-query"
import { Link, useParams } from "@tanstack/react-router"
+import { Suspense } from "react"
import { LuArrowRight, LuHouse } from "react-icons/lu"
+import { useQueries } from "@/hooks/use-queries"
+import { numberToRoman } from "@/utils/strings/numbers"
+import { cn } from "@/utils/ui"
+import PhaseFlag from "./custom-ui/PhaseFlag"
+import { Badge } from "./ui/badge"
export default function PathBreadcrumb() {
const params = useParams({
shouldThrow: false,
strict: false,
- select({ school, year }) {
- return { school, year }
+ select({ school, year, id }) {
+ return { school, year, id }
},
})
@@ -30,6 +37,59 @@ export default function PathBreadcrumb() {
>
)}
+ {params.id && (
+
+
+
+ )}
)
}
+
+function RankingInfo({ id }: { id: string }) {
+ const q = useQueries()
+ const ranking = useSuspenseQuery(q.ranking(id))
+
+ if (ranking.error) return null
+
+ return (
+ <>
+
+
+
+
+ {ranking.data.phase.primary === 0
+ ? "Fase Generale"
+ : `${numberToRoman(ranking.data.phase.primary)} Fase`}
+
+
+ {numberToRoman(ranking.data.phase.secondary)} Grad.
+
+ {ranking.data.phase.isExtraEu && (
+
+ Extra-UE
+
+ )}
+
+ >
+ )
+}
diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx
index 6fff3b71..8798f09a 100644
--- a/src/components/ui/badge.tsx
+++ b/src/components/ui/badge.tsx
@@ -22,6 +22,7 @@ const badgeVariants = cva(
tiny: "px-2.5 py-0.5 text-xs",
small: "px-3 py-1 text-sm",
medium: "px-4 py-1.5 text-md",
+ large: "px-8 py-2 text-lg",
},
},
defaultVariants: {
diff --git a/src/components/ui/tooltip.tsx b/src/components/ui/tooltip.tsx
new file mode 100644
index 00000000..40709ae5
--- /dev/null
+++ b/src/components/ui/tooltip.tsx
@@ -0,0 +1,30 @@
+import * as React from "react"
+import * as TooltipPrimitive from "@radix-ui/react-tooltip"
+
+import { cn } from "@/utils/ui.ts"
+
+const TooltipProvider = TooltipPrimitive.Provider
+
+const Tooltip = TooltipPrimitive.Root
+
+const TooltipTrigger = TooltipPrimitive.Trigger
+
+const TooltipContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, sideOffset = 4, ...props }, ref) => (
+
+
+
+))
+TooltipContent.displayName = TooltipPrimitive.Content.displayName
+
+export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
diff --git a/src/contexts/DarkModeContext.tsx b/src/contexts/DarkModeContext.tsx
index 6f2bd6b9..dd2712a9 100644
--- a/src/contexts/DarkModeContext.tsx
+++ b/src/contexts/DarkModeContext.tsx
@@ -1,4 +1,5 @@
-import { createContext, useState } from "react"
+import { createContext, useEffect, useState } from "react"
+import { toast } from "sonner"
import { LOCAL_STORAGE } from "@/utils/constants"
export interface IDarkModeContext {
@@ -52,11 +53,21 @@ export function DarkModeProvider({ ...p }: Props) {
setIsDarkMode((value) => {
const newValue = !value
localStorage.setItem(LOCAL_STORAGE.darkMode, JSON.stringify(newValue))
+
updateDOMWithTheme(newValue)
return newValue
})
}
+ useEffect(() => {
+ if (!isDarkMode)
+ toast.warning("Light mode is experimental, expect issues", {
+ id: "light-mode-warn",
+ duration: 10000,
+ })
+ else toast.dismiss("light-mode-warn")
+ }, [isDarkMode])
+
return (
)
diff --git a/src/index.css b/src/index.css
index ba8249e2..66a78f1d 100644
--- a/src/index.css
+++ b/src/index.css
@@ -62,7 +62,7 @@
}
a {
- @apply text-blue-800 underline-offset-2 hover:underline dark:text-blue-300;
+ @apply text-blue-800 dark:text-blue-300 underline-offset-2 hover:underline;
}
button {
diff --git a/src/utils/errors.ts b/src/utils/errors.ts
index 0e1f90f3..9105f4ce 100644
--- a/src/utils/errors.ts
+++ b/src/utils/errors.ts
@@ -1 +1,3 @@
-export class NotFoundError extends Error {}
+export class NotFoundError extends Error {
+ message = "Not Found"
+}
diff --git a/src/utils/phase.ts b/src/utils/phase.ts
index a135965e..86763492 100644
--- a/src/utils/phase.ts
+++ b/src/utils/phase.ts
@@ -26,4 +26,4 @@ export const phaseGroupLabel = (primary: number) =>
export const phaseLinkLabel = (p: PhaseLink) =>
p.secondary === 0
? p.stripped
- : `${numberToRoman(p.secondary)} Graduatoria ${p.isExtraEu ? "(Extra-EU)" : ""}`.trimEnd()
+ : `${numberToRoman(p.secondary)} Graduatoria ${p.isExtraEu ? "(Extra-UE)" : ""}`.trimEnd()