From 58ba7f133edadc0c067413c08d39e16003805bc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriele=20Vigan=C3=B2?= Date: Tue, 27 Oct 2026 16:53:25 +0000 Subject: [PATCH 01/14] feat(contact): align Contattaci with the shared layout and fix the card grid MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Page uses the shared container, title/lead styles and vertical rhythm of the other redesigned pages instead of ad-hoc paddings and a 1616px breakpoint. - Email cards: four in a row from xl as in Figma, 2×2 below, one column on phones, so a card is never left alone on a row; cards size to content instead of the fixed 264×312 box. - "Trova il tuo gruppo" had no action: it now links to /groups, like the inline link in the same paragraph. - Group photo keeps Figma's crop next to the text from xl and is capped when stacked; text is left-aligned at every width. Co-Authored-By: Claude Opus 5.5 --- src/app/contact/page.tsx | 15 ++++---- src/components/contact-card.tsx | 6 ++-- src/components/contact/collabora-section.tsx | 21 +++++------- src/components/contact/email-section.tsx | 12 +++---- src/components/contact/groups-section.tsx | 36 +++++++++++--------- 5 files changed, 47 insertions(+), 43 deletions(-) diff --git a/src/app/contact/page.tsx b/src/app/contact/page.tsx index 256adc98..c1a8d014 100644 --- a/src/app/contact/page.tsx +++ b/src/app/contact/page.tsx @@ -2,6 +2,7 @@ import type { Metadata } from "next" import { CollaboraSection } from "@/components/contact/collabora-section" import { EmailSection } from "@/components/contact/email-section" import { GroupsSection } from "@/components/contact/groups-section" +import { CONTAINER, PAGE_HERO, PAGE_MAIN } from "@/components/layout/page" import { Hero } from "@/components/ui/hero" export const metadata: Metadata = { @@ -11,12 +12,14 @@ export const metadata: Metadata = { export default function Contact() { return ( -
- +
+
+ +
diff --git a/src/components/contact-card.tsx b/src/components/contact-card.tsx index 742a8160..716db936 100644 --- a/src/components/contact-card.tsx +++ b/src/components/contact-card.tsx @@ -9,12 +9,12 @@ export type ContactCardData = { export function ContactCard({ title, description, email }: ContactCardData) { return ( - + - {title} + {title} {description} - + {email} diff --git a/src/components/contact/collabora-section.tsx b/src/components/contact/collabora-section.tsx index e035b39e..a0b682de 100644 --- a/src/components/contact/collabora-section.tsx +++ b/src/components/contact/collabora-section.tsx @@ -1,27 +1,24 @@ import Link from "next/link" import { FiArrowUpRight } from "react-icons/fi" +import { CONTAINER, SECTION_LEAD, SECTION_TITLE } from "@/components/layout/page" import { Button } from "@/components/ui/button" export function CollaboraSection() { return ( -
-
-

- Vuoi sviluppare un progetto insieme a noi? -

+
+
+

Vuoi sviluppare un progetto insieme a noi?

-
-

+

+

Siamo sempre felici di{" "} - collaborare con aziende o associazioni studentesche per portare + collaborare con aziende o associazioni studentesche per portare agli studenti del PoliMi eventi e attività di valore.

-

- Consulta la pagina dedicata per scoprire come possiamo unire le forze! -

+

Consulta la pagina dedicata per scoprire come possiamo unire le forze!

-
-
- Foto di gruppo della community PoliNetwork -
+ {/* Figma crops the photo to ~2.1:1 next to the text; it keeps that ratio at every width instead of growing tall */} + Foto di gruppo della community PoliNetwork
) } From 56a2ffdd605bdf21823021cdb1710e1c43b7a7aa Mon Sep 17 00:00:00 2001 From: Bianca Roberta Ianosel Date: Mon, 2 Nov 2026 09:26:33 +0000 Subject: [PATCH 02/14] fix(contact): sharp group photo, honest link label, one link style - The group photo is 512x246: it was declared 721x342 and stretched up to 768px wide. It now shows at its real size at most. - "Candidature" linked to /join with the label "Struttura"; the label now matches the page it opens. - The inline links use INLINE_LINK, with a visible focus ring. --- src/components/contact/email-section.tsx | 12 ++++++------ src/components/contact/groups-section.tsx | 13 ++++++------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/components/contact/email-section.tsx b/src/components/contact/email-section.tsx index b659a88a..2e67be97 100644 --- a/src/components/contact/email-section.tsx +++ b/src/components/contact/email-section.tsx @@ -1,6 +1,6 @@ import Link from "next/link" import { ContactCard, type ContactCardData } from "../contact-card" -import { CONTAINER, SECTION_LEAD, SECTION_TITLE } from "../layout/page" +import { CONTAINER, INLINE_LINK, SECTION_LEAD, SECTION_TITLE } from "../layout/page" const contactCards: ContactCardData[] = [ { @@ -19,8 +19,8 @@ const contactCards: ContactCardData[] = [ <> Se vuoi candidarti come admin o come membro di un team, ti consigliamo di passare dal form disponibile nella pagina{" "} - - Struttura + + Unisciti a noi . @@ -32,7 +32,7 @@ const contactCards: ContactCardData[] = [ description: ( <> Se sei un'azienda o associazione e vuoi proporre una collaborazione, puoi anche consultare la pagina{" "} - + Collabora con Noi {" "} per capire cosa offriamo. @@ -48,7 +48,7 @@ export function EmailSection() {

Hai una richiesta specifica?

Per tutto il resto,{" "} - + puoi scriverci direttamente . @@ -56,7 +56,7 @@ export function EmailSection() { Qui sotto trovi i contatti in base al tipo di richiesta.

- {/* Four requests, four columns as in Figma; 2×2 in between so no card is ever left alone on a row */} + {/* 2×2 between one and four columns, so no card is left alone on a row */}
{contactCards.map((card) => ( diff --git a/src/components/contact/groups-section.tsx b/src/components/contact/groups-section.tsx index e67efcce..c3ea204c 100644 --- a/src/components/contact/groups-section.tsx +++ b/src/components/contact/groups-section.tsx @@ -1,7 +1,7 @@ import Image from "next/image" import Link from "next/link" import { FiArrowUpRight } from "react-icons/fi" -import { CONTAINER, SECTION_LEAD, SECTION_TITLE } from "@/components/layout/page" +import { CONTAINER, INLINE_LINK, SECTION_LEAD, SECTION_TITLE } from "@/components/layout/page" import { Button } from "@/components/ui/button" export function GroupsSection() { @@ -16,7 +16,7 @@ export function GroupsSection() {

Il modo più rapido per ottenere risposte su esami, burocrazia universitaria o sulla vita al Politecnico è{" "} - + entrare nel gruppo del tuo corso , su Telegram o su WhatsApp, dove gli admin sono presenti ogni giorno. @@ -30,14 +30,13 @@ export function GroupsSection() {

- {/* Figma crops the photo to ~2.1:1 next to the text; it keeps that ratio at every width instead of growing tall */} Foto di gruppo della community PoliNetwork
) From 7dccd609d3a1f671f782f5577424a1ce9675ae01 Mon Sep 17 00:00:00 2001 From: Bianca Roberta Ianosel Date: Mon, 2 Nov 2026 15:26:13 +0000 Subject: [PATCH 03/14] fix(contact): let the group photo fill its column, centre the Collabora block --- src/components/contact/collabora-section.tsx | 2 +- src/components/contact/groups-section.tsx | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/contact/collabora-section.tsx b/src/components/contact/collabora-section.tsx index a0b682de..12dd95c3 100644 --- a/src/components/contact/collabora-section.tsx +++ b/src/components/contact/collabora-section.tsx @@ -6,7 +6,7 @@ import { Button } from "@/components/ui/button" export function CollaboraSection() { return (
-
+

Vuoi sviluppare un progetto insieme a noi?

diff --git a/src/components/contact/groups-section.tsx b/src/components/contact/groups-section.tsx index c3ea204c..b5863373 100644 --- a/src/components/contact/groups-section.tsx +++ b/src/components/contact/groups-section.tsx @@ -35,8 +35,9 @@ export function GroupsSection() { alt="Foto di gruppo della community PoliNetwork" width={512} height={246} - // The source is 512px wide: shown no larger, so it stays sharp - className="h-auto w-full max-w-128 rounded-rectangles" + sizes="(min-width: 80rem) 600px, (min-width: 42rem) 672px, 100vw" + // The source is only 512px wide: capped at 672px so it never stretches past ~1.3x + className="mx-auto h-auto w-full max-w-2xl rounded-rectangles xl:max-w-none" />
) From 3de45756fe687b489332190c4d651f3c1bf54ebb Mon Sep 17 00:00:00 2001 From: Bianca Roberta Ianosel Date: Mon, 2 Nov 2026 15:28:44 +0000 Subject: [PATCH 04/14] fix(contact): centre the groups text when the photo wraps below it --- src/components/contact/groups-section.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/contact/groups-section.tsx b/src/components/contact/groups-section.tsx index b5863373..f8cf963a 100644 --- a/src/components/contact/groups-section.tsx +++ b/src/components/contact/groups-section.tsx @@ -7,7 +7,7 @@ import { Button } from "@/components/ui/button" export function GroupsSection() { return (
-
+

Hai mille dubbi sul Poli?
From 3ee1b482b651d921cd8147050691cc2ace9d8016 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriele=20Vigan=C3=B2?= Date: Tue, 27 Oct 2026 17:02:50 +0000 Subject: [PATCH 05/14] feat(projects): content-sized project cards and a working collection search - One ProjectCard for the three lists: the project's logo replaces Figma's placeholder icons (crop / upload), the title gets a per-line gradient, and the footer says what the project page offers (web app or source code). - One ProjectList: cards in a min/max-bounded auto-fill grid instead of fixed 312x264 boxes at fixed breakpoints; on phones a carousel with the next card peeking in. - The collection search box now filters by name and description (it had no handler), with an explicit "no match" state. - Empty lists render a dashed placeholder that points to "Hai un'idea?". - "Hai un'idea?" sits as text + two reference cards side by side, with a readable lead and a real mailto link. - /projects and /projects/[id] use the shared container, titles and rhythm; the detail page drops its gradient panels and closes on the background like the other pages. Co-Authored-By: Claude Opus 5.5 --- src/app/projects/[id]/page.tsx | 28 ++--- src/app/projects/page.tsx | 9 +- src/components/projects/collection.tsx | 123 ++++++++------------- src/components/projects/community-news.tsx | 82 ++++---------- src/components/projects/deprecated.tsx | 98 +++------------- src/components/projects/project-card.tsx | 50 +++++++++ src/components/projects/project-list.tsx | 80 ++++++++++++++ src/components/projects/section.ts | 4 + src/components/projects/upload.tsx | 49 ++++---- 9 files changed, 267 insertions(+), 256 deletions(-) create mode 100644 src/components/projects/project-card.tsx create mode 100644 src/components/projects/project-list.tsx create mode 100644 src/components/projects/section.ts diff --git a/src/app/projects/[id]/page.tsx b/src/app/projects/[id]/page.tsx index 8d1c623e..bf63da2f 100644 --- a/src/app/projects/[id]/page.tsx +++ b/src/app/projects/[id]/page.tsx @@ -5,7 +5,8 @@ import { notFound } from "next/navigation" import { FiArrowLeft, FiArrowUpRight, FiUploadCloud } from "react-icons/fi" import { Glass } from "@/components/glass" import { GradientIcon } from "@/components/gradient-icon" -import { SECTION_LEAD, SECTION_TITLE } from "@/components/layout/page" +import { ClosingCta } from "@/components/layout/closing-cta" +import { CONTAINER, PAGE_HERO, PAGE_MAIN, SECTION_LEAD, SECTION_TITLE } from "@/components/layout/page" import { getProjectResources, resourceDetails } from "@/components/projects/resources" import { Button } from "@/components/ui/button" import { Card, CardContent, CardTitle } from "@/components/ui/card" @@ -39,15 +40,15 @@ export default async function ProjectPage({ params }: Props) { const resources = getProjectResources(project) return ( -
-
+
+
Tutti i progetti - + {project.logo ? ( // Project logos are app icons with their own background and corners: shown as they are, not framed {resources.length > 0 && ( -
+

Come usarlo @@ -135,19 +133,17 @@ export default async function ProjectPage({ params }: Props) {

)} -
- -

Scopri gli altri progetti

-

- Strumenti e app realizzati dagli studenti del Politecnico, per gli studenti del Politecnico. -

+ Tutti i progetti -
-
+ } + />
) } diff --git a/src/app/projects/page.tsx b/src/app/projects/page.tsx index fbfb0d74..ee9ee6af 100644 --- a/src/app/projects/page.tsx +++ b/src/app/projects/page.tsx @@ -1,7 +1,9 @@ +import { CONTAINER, PAGE_HERO, PAGE_MAIN } from "@/components/layout/page" import { Collection } from "@/components/projects/collection" import { CommunityNews } from "@/components/projects/community-news" import { Deprecated } from "@/components/projects/deprecated" import { Upload } from "@/components/projects/upload" +import { Hero } from "@/components/ui/hero" import { getAllProjects } from "@/queries/projects" export const metadata = { @@ -11,7 +13,7 @@ export const metadata = { export const dynamic = "force-dynamic" -export default async function Home() { +export default async function ProjectsPage() { const projects = await getAllProjects() const news = projects.filter((project) => project.category === "news") @@ -19,7 +21,10 @@ export default async function Home() { const deprecated = projects.filter((project) => project.category === "deprecated") return ( -
+
+
+ +
diff --git a/src/components/projects/collection.tsx b/src/components/projects/collection.tsx index 61345010..485667e3 100644 --- a/src/components/projects/collection.tsx +++ b/src/components/projects/collection.tsx @@ -1,94 +1,69 @@ "use client" -import { useId, useState } from "react" -import { FiArrowDown, FiArrowUp, FiSearch, FiUploadCloud } from "react-icons/fi" -import type { ApiOutput } from "@/types" -import { CardCaption } from "../card-caption" -import { Button } from "../ui/button" -import { Carousel, CarouselContent, CarouselDots, CarouselItem } from "../ui/carousel" +import { useState } from "react" +import { FiSearch } from "react-icons/fi" +import { SECTION_TITLE } from "../layout/page" import { Input } from "../ui/input" +import { ProjectList, ProjectsEmpty } from "./project-list" +import type { Project } from "./resources" +import { SECTION } from "./section" -type Project = ApiOutput["web"]["projects"]["getAllProjects"][number] - -const INITIAL_PROJECT_COUNT = 8 +/** Case- and accent-insensitive, so "universita" finds "Università". */ +function normalize(text: string) { + return text + .normalize("NFD") + .replace(/\p{Diacritic}/gu, "") + .toLowerCase() +} export function Collection({ projects }: { projects: Project[] }) { - const [expanded, setExpanded] = useState(false) - const gridId = useId() - const visibleProjects = expanded ? projects : projects.slice(0, INITIAL_PROJECT_COUNT) + const [query, setQuery] = useState("") + const needle = normalize(query.trim()) + const matches = needle + ? projects.filter((project) => normalize(`${project.title} ${project.descriptionIt}`).includes(needle)) + : projects return ( -
-
-

+
+ {/* The collection is the page's tool: its heading and search stay centred, as in Figma */} +
+

Esplora la raccolta completa dei progetti

-
+ {projects.length > 0 && ( } - type="text" - placeholder="Search by name" - aria-label="Search by name" - containerClassName="max-w-xl" + type="search" + placeholder="Cerca un progetto" + aria-label="Cerca un progetto per nome o descrizione" + containerClassName="w-full max-w-xl" className="typo-body-medium" + value={query} + onChange={(event) => setQuery(event.target.value)} /> -
+ )}
- {projects.length === 0 ? ( -

Nessun progetto disponibile al momento.

- ) : ( - <> -
-
- {visibleProjects.map((project) => ( - - ))} -
- {projects.length > INITIAL_PROJECT_COUNT && ( -
- -
+ + {needle ? ( + <>Nessun progetto corrisponde a “{query.trim()}”. + ) : ( + <> + La raccolta è vuota per ora.{" "} + + Proponi il primo progetto + + )} -
- -
- - - {projects.map((project) => ( - -
- -
-
- ))} -
- -
-
- - )} + + } + />
) } diff --git a/src/components/projects/community-news.tsx b/src/components/projects/community-news.tsx index ee7440ed..51ef6852 100644 --- a/src/components/projects/community-news.tsx +++ b/src/components/projects/community-news.tsx @@ -1,68 +1,28 @@ -import { FiCrop } from "react-icons/fi" -import type { ApiOutput } from "@/types" -import { CardCaption } from "../card-caption" -import { Carousel, CarouselContent, CarouselDots, CarouselItem } from "../ui/carousel" -import { Hero } from "../ui/hero" - -type Project = ApiOutput["web"]["projects"]["getAllProjects"][number] +import { SECTION_LEAD, SECTION_TITLE } from "../layout/page" +import { ProjectList, ProjectsEmpty } from "./project-list" +import type { Project } from "./resources" +import { SECTION } from "./section" export function CommunityNews({ projects }: { projects: Project[] }) { return ( -
- - -
-
-

- Le novità della community -

-

- Rimani aggiornato sulle idee appena condivise dagli studenti del Politecnico -

-
- - {projects.length === 0 ? ( -

Nessuna novità disponibile al momento.

- ) : ( - <> - {/* Desktop Grid */} -
- {projects.map((project) => ( - - ))} -
- - {/* Mobile Carousel */} -
- - - {projects.map((project) => ( - -
- -
-
- ))} -
- -
-
- - )} +
+
+

+ Le novità della community +

+

Rimani aggiornato sulle idee appena condivise dagli studenti del Politecnico.

+ + Nessuna novità al momento.{" "} + + Hai un'idea? Proponila + + + } + />
) } diff --git a/src/components/projects/deprecated.tsx b/src/components/projects/deprecated.tsx index 34a0fd08..211ecc7d 100644 --- a/src/components/projects/deprecated.tsx +++ b/src/components/projects/deprecated.tsx @@ -1,87 +1,25 @@ -"use client" - -import { useId, useState } from "react" -import { FiArrowDown, FiArrowUp, FiUploadCloud } from "react-icons/fi" -import type { ApiOutput } from "@/types" -import { CardCaption } from "../card-caption" -import { Button } from "../ui/button" -import { Carousel, CarouselContent, CarouselDots, CarouselItem } from "../ui/carousel" - -type Project = ApiOutput["web"]["projects"]["getAllProjects"][number] - -const INITIAL_PROJECT_COUNT = 8 +import { SECTION_LEAD, SECTION_TITLE } from "../layout/page" +import { ProjectList, ProjectsEmpty } from "./project-list" +import type { Project } from "./resources" +import { SECTION } from "./section" export function Deprecated({ projects }: { projects: Project[] }) { - const [expanded, setExpanded] = useState(false) - const gridId = useId() - const visibleProjects = expanded ? projects : projects.slice(0, INITIAL_PROJECT_COUNT) - return ( -
-
-
-

Progetti deprecati

-

- Qui raccogliamo i progetti non più aggiornati o attivi.
- Puoi contribuire a riportarli in vita, migliorarli o usarli come base per nuove idee. -

-
- - {projects.length === 0 ? ( -

Nessun progetto deprecato al momento.

- ) : ( - <> -
-
- {visibleProjects.map((project) => ( - - ))} -
- {projects.length > INITIAL_PROJECT_COUNT && ( -
- -
- )} -
- -
- - - {projects.map((project) => ( - -
- -
-
- ))} -
- -
-
- - )} +
+
+

+ Progetti deprecati +

+

+ Qui raccogliamo i progetti non più aggiornati o attivi. Puoi contribuire a riportarli in vita, migliorarli o + usarli come base per nuove idee. +

+ Nessun progetto deprecato al momento.} + />
) } diff --git a/src/components/projects/project-card.tsx b/src/components/projects/project-card.tsx new file mode 100644 index 00000000..c0d42578 --- /dev/null +++ b/src/components/projects/project-card.tsx @@ -0,0 +1,50 @@ +import Image from "next/image" +import Link from "next/link" +import { FiArrowRight } from "react-icons/fi" +import { Card, CardContent, CardTitle } from "../ui/card" +import { getProjectResources, type Project, resourceDetails } from "./resources" + +/** + * A project in the lists. The whole card opens the project page; the footer says what's waiting there (the web app or + * the source code) when the project has a link. The project's own logo replaces Figma's placeholder icon. + */ +export function ProjectCard({ project }: { project: Project }) { + const [resource] = getProjectResources(project) + const details = resource && resourceDetails[resource.kind] + + return ( + + +
+ {project.logo && ( + + )} + + {/* Gradient per line (box-decoration-clone): a two-line title doesn't fade into a paler second line */} + + {project.title} + + +
+ {project.descriptionIt} +
+ {details ? ( + + + {details.title} + + ) : ( + + )} + +
+
+ ) +} diff --git a/src/components/projects/project-list.tsx b/src/components/projects/project-list.tsx new file mode 100644 index 00000000..4128a03e --- /dev/null +++ b/src/components/projects/project-list.tsx @@ -0,0 +1,80 @@ +"use client" + +import { type ReactNode, useId, useState } from "react" +import { FiArrowDown, FiArrowUp } from "react-icons/fi" +import { Button } from "../ui/button" +import { Carousel, CarouselContent, CarouselDots, CarouselItem } from "../ui/carousel" +import { ProjectCard } from "./project-card" +import type { Project } from "./resources" + +/** + * Cards keep a readable width at every viewport (16rem min, one fr max), so the column count follows the space + * instead of jumping at fixed breakpoints. On phones the list becomes a carousel with the next card peeking in. + */ +export function ProjectList({ + projects, + initialCount, + empty, +}: { + projects: Project[] + /** Show only this many cards on larger screens until expanded. */ + initialCount?: number + empty: ReactNode +}) { + const [expanded, setExpanded] = useState(false) + const gridId = useId() + + if (projects.length === 0) return empty + + const collapsible = initialCount !== undefined && projects.length > initialCount + const visible = collapsible && !expanded ? projects.slice(0, initialCount) : projects + + return ( + <> +
+
    + {visible.map((project) => ( +
  • + +
  • + ))} +
+ {collapsible && ( + + )} +
+ +
+ + + {projects.map((project) => ( + + + + ))} + + + +
+ + ) +} + +/** Shown in place of an empty list: says so plainly and points to the one thing the reader can do about it. */ +export function ProjectsEmpty({ children }: { children: ReactNode }) { + return ( +
+ {children} +
+ ) +} diff --git a/src/components/projects/section.ts b/src/components/projects/section.ts new file mode 100644 index 00000000..9086bc34 --- /dev/null +++ b/src/components/projects/section.ts @@ -0,0 +1,4 @@ +import { CONTAINER } from "../layout/page" + +/** A list section of /projects: heading block, then the cards. */ +export const SECTION = `${CONTAINER} flex flex-col gap-10 md:gap-14` diff --git a/src/components/projects/upload.tsx b/src/components/projects/upload.tsx index 5bcd90bf..86e44066 100644 --- a/src/components/projects/upload.tsx +++ b/src/components/projects/upload.tsx @@ -1,22 +1,24 @@ import { FiMail } from "react-icons/fi" import { CardCaption } from "../card-caption" +import { CONTAINER, SECTION_LEAD, SECTION_TITLE } from "../layout/page" const cards = [ { title: "Contatti", caption: ( -
- - info@polinetwork.org - -
+ + info@polinetwork.org + ), }, { title: "Requisiti", caption: ( -
    -
  • opensource
  • +
      +
    • open source
    • utile alla community
    • senza scopo di lucro
    @@ -26,32 +28,33 @@ const cards = [ export function Upload() { return ( -
    -
    -
    -

    Hai un'idea?

    -

    Proponila qui!

    -
    -
    -

    +

    + {/* Figma keeps this block narrow and centred: the text and its two reference cards side by side */} +
    +
    +
    +

    + Hai un'idea? +

    +

    Proponila qui!

    +
    +

    Qui puoi proporre nuovi progetti o suggerire migliorie al sito. Il team di PoliNetwork può aiutarti a sviluppare la tua idea e, se rispetta i requisiti, pubblicarla qui.

    Dopo l'invio riceverai una risposta dai nostri admin con le istruzioni per proseguire.

    -
    -
    -

    +

    I diritti dei progetti rimangono tutti dei rispettivi progettisti, PoliNetwork non si assume responsabilità.

    -
    -
    - {cards.map((card) => ( - - ))} +
    + {cards.map((card) => ( + + ))} +
    ) From 6f53e7a80cd4cf312182cf539363b655dbeec793 Mon Sep 17 00:00:00 2001 From: Bianca Roberta Ianosel Date: Mon, 2 Nov 2026 13:41:17 +0000 Subject: [PATCH 06/14] refactor(projects): build cards on CardCaption and widen the search - ProjectCard is now a CardCaption with a logo, a clamped caption and a footer, instead of a second copy of its link, hover, focus and footer logic. CardCaption gains `image`, `titleClassName` and `captionClassName` for it. - The search looks through every project on the page, novelties and deprecated ones included, matches each word separately and announces the number of results. - PeekCarousel replaces the two copies of the phone carousel (homepage and project lists). - SECTION_STACK moves to layout/page and also serves /about. - t.me links are labelled Telegram, not Web. - The project page shows no placeholder icon when there is no logo, and "Hai un'idea?" sits on the container's left edge like the other sections. - Long search queries wrap inside the empty state. --- src/app/about/page.tsx | 9 ++-- src/app/projects/[id]/page.tsx | 21 ++++---- src/app/projects/page.tsx | 2 +- src/components/card-caption.tsx | 18 +++++-- src/components/home/projects.tsx | 25 ++++----- src/components/layout/page.ts | 3 ++ src/components/peek-carousel.tsx | 34 ++++++++++++ src/components/projects/collection.tsx | 20 ++++--- src/components/projects/community-news.tsx | 7 ++- src/components/projects/deprecated.tsx | 5 +- src/components/projects/project-card.tsx | 62 ++++++++-------------- src/components/projects/project-list.tsx | 22 +++----- src/components/projects/resources.ts | 20 +++++-- src/components/projects/section.ts | 4 -- src/components/projects/upload.tsx | 3 +- 15 files changed, 141 insertions(+), 114 deletions(-) create mode 100644 src/components/peek-carousel.tsx delete mode 100644 src/components/projects/section.ts diff --git a/src/app/about/page.tsx b/src/app/about/page.tsx index 6c8380ea..34afdc94 100644 --- a/src/app/about/page.tsx +++ b/src/app/about/page.tsx @@ -2,7 +2,7 @@ import type { Metadata } from "next" import Link from "next/link" import { FiArrowUpRight } from "react-icons/fi" import { ClosingCta } from "@/components/layout/closing-cta" -import { CONTAINER, PAGE_HERO, PAGE_MAIN, SECTION_TITLE } from "@/components/layout/page" +import { CONTAINER, PAGE_HERO, PAGE_MAIN, SECTION_STACK, SECTION_TITLE } from "@/components/layout/page" import { AssociationBodies } from "@/components/structure/association-bodies" import { BoardGallery } from "@/components/structure/board-gallery" import { LegalStatus } from "@/components/structure/legal-status" @@ -17,7 +17,6 @@ export const metadata: Metadata = { "Un’organizzazione pensata per durare: scopri l’organigramma, il Direttivo, gli organi associativi e i ruoli di PoliNetwork APS.", } -const containerClass = `${CONTAINER} flex flex-col gap-10 md:gap-14` const titleClass = `${SECTION_TITLE} text-center` export default function StructurePage() { @@ -27,7 +26,7 @@ export default function StructurePage() {
    -
    +

    Il Direttivo in carica

    @@ -38,13 +37,13 @@ export default function StructurePage() {

-
+

Come funziona l’associazione

-
+

Trova il tuo posto in PoliNetwork

diff --git a/src/app/projects/[id]/page.tsx b/src/app/projects/[id]/page.tsx index bf63da2f..a721908d 100644 --- a/src/app/projects/[id]/page.tsx +++ b/src/app/projects/[id]/page.tsx @@ -2,11 +2,11 @@ import type { Metadata } from "next" import Image from "next/image" import Link from "next/link" import { notFound } from "next/navigation" -import { FiArrowLeft, FiArrowUpRight, FiUploadCloud } from "react-icons/fi" +import { FiArrowLeft, FiArrowUpRight } from "react-icons/fi" import { Glass } from "@/components/glass" import { GradientIcon } from "@/components/gradient-icon" import { ClosingCta } from "@/components/layout/closing-cta" -import { CONTAINER, PAGE_HERO, PAGE_MAIN, SECTION_LEAD, SECTION_TITLE } from "@/components/layout/page" +import { CONTAINER, PAGE_HERO, PAGE_MAIN, SECTION_LEAD, SECTION_STACK, SECTION_TITLE } from "@/components/layout/page" import { getProjectResources, resourceDetails } from "@/components/projects/resources" import { Button } from "@/components/ui/button" import { Card, CardContent, CardTitle } from "@/components/ui/card" @@ -44,12 +44,17 @@ export default async function ProjectPage({ params }: Props) {
Tutti i progetti - - {project.logo ? ( + + {project.logo && ( // Project logos are app icons with their own background and corners: shown as they are, not framed - ) : ( - - - )}
{project.category in categoryLabels && ( @@ -75,7 +76,7 @@ export default async function ProjectPage({ params }: Props) {
{resources.length > 0 && ( -
+

Come usarlo diff --git a/src/app/projects/page.tsx b/src/app/projects/page.tsx index ee9ee6af..25e85bd6 100644 --- a/src/app/projects/page.tsx +++ b/src/app/projects/page.tsx @@ -26,7 +26,7 @@ export default async function ProjectsPage() {

- +

diff --git a/src/components/card-caption.tsx b/src/components/card-caption.tsx index 43fe8e3b..a26f3139 100644 --- a/src/components/card-caption.tsx +++ b/src/components/card-caption.tsx @@ -1,3 +1,4 @@ +import Image from "next/image" import Link from "next/link" import type { ReactNode } from "react" import type { IconType } from "react-icons" @@ -9,18 +10,25 @@ export function CardCaption({ caption, icon, iconPosition = "right", + image, href, footer, className, + titleClassName, + captionClassName, }: { title: string caption: ReactNode icon?: IconType iconPosition?: "top" | "right" + /** An image shown in place of the icon, e.g. a project's logo. */ + image?: string href?: string /** Pinned to the bottom of the card, e.g. where a link leads to. */ footer?: ReactNode className?: string + titleClassName?: string + captionClassName?: string }) { return ( // Only cards that lead somewhere react to hover @@ -32,10 +40,14 @@ export function CardCaption({ - {title} - {icon && } + {title} + {image ? ( + + ) : ( + icon && + )} - + {typeof caption === "string" ?

{caption}

: caption}
{footer &&
{footer}
} diff --git a/src/components/home/projects.tsx b/src/components/home/projects.tsx index d0078106..99ec6578 100644 --- a/src/components/home/projects.tsx +++ b/src/components/home/projects.tsx @@ -2,9 +2,9 @@ import Link from "next/link" import { FiArrowUpRight } from "react-icons/fi" import { CardCaption } from "@/components/card-caption" import { CONTAINER } from "@/components/layout/page" +import { PeekCarousel } from "@/components/peek-carousel" import { Button } from "@/components/ui/button" import { cn } from "@/lib/utils" -import { Carousel, CarouselContent, CarouselDots, CarouselItem } from "../ui/carousel" const featuredCards = [ { @@ -31,7 +31,7 @@ const featuredCards = [ }, ] as const -function ProjectCard({ className, ...card }: (typeof featuredCards)[number] & { className?: string }) { +function FeaturedProjectCard({ className, ...card }: (typeof featuredCards)[number] & { className?: string }) { return ( {featuredCards.map((card) => ( - + ))}

- {/* Mobile: the next card peeks in from the edge, so the row reads as scrollable */} -
- - - {featuredCards.map((card) => ( - - - - ))} - - - -
+ card.title} + render={(card) => } + className="pt-6 sm:hidden" + />
) } diff --git a/src/components/layout/page.ts b/src/components/layout/page.ts index ca8cc332..495b9d2a 100644 --- a/src/components/layout/page.ts +++ b/src/components/layout/page.ts @@ -24,3 +24,6 @@ export const PAGE_MAIN = "flex w-full flex-col gap-24 pb-24 md:gap-32 md:pb-32 x */ export const PAGE_HERO_TOP = "pt-52 md:pt-57.5" export const PAGE_HERO = `${PAGE_HERO_TOP} pb-8 md:pb-12 xl:pb-24` + +/** A section made of a heading block followed by its content. */ +export const SECTION_STACK = `${CONTAINER} flex flex-col gap-10 md:gap-14` diff --git a/src/components/peek-carousel.tsx b/src/components/peek-carousel.tsx new file mode 100644 index 00000000..5d2afda2 --- /dev/null +++ b/src/components/peek-carousel.tsx @@ -0,0 +1,34 @@ +import type { ReactNode } from "react" +import { Carousel, CarouselContent, CarouselDots, CarouselItem } from "@/components/ui/carousel" +import { cn } from "@/lib/utils" + +/** + * Phone carousel whose next card peeks in from the screen edge, so the row reads as scrollable. + * It bleeds over the CONTAINER's 24px phone gutter: use it below `sm` only. + */ +export function PeekCarousel({ + items, + getKey, + render, + className, +}: { + items: readonly T[] + getKey: (item: T) => string | number + render: (item: T) => ReactNode + className?: string +}) { + return ( +
+ + + {items.map((item) => ( + + {render(item)} + + ))} + + + +
+ ) +} diff --git a/src/components/projects/collection.tsx b/src/components/projects/collection.tsx index 485667e3..c2c2c1c3 100644 --- a/src/components/projects/collection.tsx +++ b/src/components/projects/collection.tsx @@ -2,11 +2,10 @@ import { useState } from "react" import { FiSearch } from "react-icons/fi" -import { SECTION_TITLE } from "../layout/page" +import { INLINE_LINK, SECTION_STACK, SECTION_TITLE } from "../layout/page" import { Input } from "../ui/input" import { ProjectList, ProjectsEmpty } from "./project-list" import type { Project } from "./resources" -import { SECTION } from "./section" /** Case- and accent-insensitive, so "universita" finds "Università". */ function normalize(text: string) { @@ -16,16 +15,20 @@ function normalize(text: string) { .toLowerCase() } -export function Collection({ projects }: { projects: Project[] }) { +/** Lists the collection; a search looks through every project on the page, novelties and deprecated ones included. */ +export function Collection({ projects, searchable }: { projects: Project[]; searchable: Project[] }) { const [query, setQuery] = useState("") const needle = normalize(query.trim()) + const words = needle.split(/\s+/) const matches = needle - ? projects.filter((project) => normalize(`${project.title} ${project.descriptionIt}`).includes(needle)) + ? searchable.filter((project) => { + const text = normalize(`${project.title} ${project.descriptionIt}`) + return words.every((word) => text.includes(word)) + }) : projects return ( -
- {/* The collection is the page's tool: its heading and search stay centred, as in Figma */} +

Esplora la raccolta completa dei progetti @@ -44,6 +47,9 @@ export function Collection({ projects }: { projects: Project[] }) { )}

+

+ {needle && `${matches.length} progetti trovati`} +

La raccolta è vuota per ora.{" "} - + Proponi il primo progetto diff --git a/src/components/projects/community-news.tsx b/src/components/projects/community-news.tsx index 51ef6852..98a8fb75 100644 --- a/src/components/projects/community-news.tsx +++ b/src/components/projects/community-news.tsx @@ -1,11 +1,10 @@ -import { SECTION_LEAD, SECTION_TITLE } from "../layout/page" +import { INLINE_LINK, SECTION_LEAD, SECTION_STACK, SECTION_TITLE } from "../layout/page" import { ProjectList, ProjectsEmpty } from "./project-list" import type { Project } from "./resources" -import { SECTION } from "./section" export function CommunityNews({ projects }: { projects: Project[] }) { return ( -
+

Le novità della community @@ -17,7 +16,7 @@ export function CommunityNews({ projects }: { projects: Project[] }) { empty={ Nessuna novità al momento.{" "} - + Hai un'idea? Proponila diff --git a/src/components/projects/deprecated.tsx b/src/components/projects/deprecated.tsx index 211ecc7d..96b23070 100644 --- a/src/components/projects/deprecated.tsx +++ b/src/components/projects/deprecated.tsx @@ -1,11 +1,10 @@ -import { SECTION_LEAD, SECTION_TITLE } from "../layout/page" +import { SECTION_LEAD, SECTION_STACK, SECTION_TITLE } from "../layout/page" import { ProjectList, ProjectsEmpty } from "./project-list" import type { Project } from "./resources" -import { SECTION } from "./section" export function Deprecated({ projects }: { projects: Project[] }) { return ( -
+

Progetti deprecati diff --git a/src/components/projects/project-card.tsx b/src/components/projects/project-card.tsx index c0d42578..291485d4 100644 --- a/src/components/projects/project-card.tsx +++ b/src/components/projects/project-card.tsx @@ -1,50 +1,32 @@ -import Image from "next/image" -import Link from "next/link" import { FiArrowRight } from "react-icons/fi" -import { Card, CardContent, CardTitle } from "../ui/card" +import { CardCaption } from "../card-caption" import { getProjectResources, type Project, resourceDetails } from "./resources" -/** - * A project in the lists. The whole card opens the project page; the footer says what's waiting there (the web app or - * the source code) when the project has a link. The project's own logo replaces Figma's placeholder icon. - */ +/** A project in the lists: opens the project page, and says what it offers there when the project has a link. */ export function ProjectCard({ project }: { project: Project }) { const [resource] = getProjectResources(project) const details = resource && resourceDetails[resource.kind] return ( - - -
- {project.logo && ( - - )} - - {/* Gradient per line (box-decoration-clone): a two-line title doesn't fade into a paler second line */} - - {project.title} - - -
- {project.descriptionIt} -
- {details ? ( - - - {details.title} - - ) : ( - - )} - -
-
+ + {details && ( + + + {details.title} + + )} + + + } + /> ) } diff --git a/src/components/projects/project-list.tsx b/src/components/projects/project-list.tsx index 4128a03e..72ee5c4a 100644 --- a/src/components/projects/project-list.tsx +++ b/src/components/projects/project-list.tsx @@ -2,8 +2,8 @@ import { type ReactNode, useId, useState } from "react" import { FiArrowDown, FiArrowUp } from "react-icons/fi" +import { PeekCarousel } from "../peek-carousel" import { Button } from "../ui/button" -import { Carousel, CarouselContent, CarouselDots, CarouselItem } from "../ui/carousel" import { ProjectCard } from "./project-card" import type { Project } from "./resources" @@ -54,18 +54,12 @@ export function ProjectList({ )}

-
- - - {projects.map((project) => ( - - - - ))} - - - -
+ project.id} + render={(project) => } + className="sm:hidden" + /> ) } @@ -73,7 +67,7 @@ export function ProjectList({ /** Shown in place of an empty list: says so plainly and points to the one thing the reader can do about it. */ export function ProjectsEmpty({ children }: { children: ReactNode }) { return ( -
+
{children}
) diff --git a/src/components/projects/resources.ts b/src/components/projects/resources.ts index 09227e03..15759167 100644 --- a/src/components/projects/resources.ts +++ b/src/components/projects/resources.ts @@ -1,11 +1,11 @@ import type { IconType } from "react-icons" -import { FaAndroid, FaApple, FaGithub, FaLinux, FaWindows } from "react-icons/fa6" +import { FaAndroid, FaApple, FaGithub, FaLinux, FaTelegram, FaWindows } from "react-icons/fa6" import { FiGlobe } from "react-icons/fi" import type { ApiOutput } from "@/types" export type Project = ApiOutput["web"]["projects"]["getAllProjects"][number] -export type ProjectResourceKind = "web" | "windows" | "macos" | "linux" | "android" | "ios" | "source" +export type ProjectResourceKind = "web" | "telegram" | "windows" | "macos" | "linux" | "android" | "ios" | "source" /** A way to use or get a project: a download for a platform, the web app or the source code. */ export type ProjectResource = { kind: ProjectResourceKind; href: string } @@ -20,6 +20,12 @@ export const resourceDetails: Record< cta: "Apri il sito", icon: FiGlobe, }, + telegram: { + title: "Telegram", + description: "Lo trovi su Telegram, senza installare nient’altro.", + cta: "Apri su Telegram", + icon: FaTelegram, + }, windows: { title: "Windows", description: "Scarica l’installer per Windows.", @@ -40,10 +46,14 @@ export const resourceDetails: Record< /** * The backend does not expose per-platform downloads yet: until it does, the only resource is the project `link`, - * shown as source code when it points to a repository and as the web app otherwise. Replace this with the API field. + * classified by its host: a repository, a Telegram bot or channel, or else the web app. Replace this with the API field. */ export function getProjectResources(project: Project): ProjectResource[] { if (!project.link) return [] - const isRepository = /^https:\/\/(github\.com|gitlab\.com)\//.test(project.link) - return [{ kind: isRepository ? "source" : "web", href: project.link }] + const kind = /^https:\/\/(github\.com|gitlab\.com)\//.test(project.link) + ? "source" + : /^https:\/\/t\.me\//.test(project.link) + ? "telegram" + : "web" + return [{ kind, href: project.link }] } diff --git a/src/components/projects/section.ts b/src/components/projects/section.ts deleted file mode 100644 index 9086bc34..00000000 --- a/src/components/projects/section.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { CONTAINER } from "../layout/page" - -/** A list section of /projects: heading block, then the cards. */ -export const SECTION = `${CONTAINER} flex flex-col gap-10 md:gap-14` diff --git a/src/components/projects/upload.tsx b/src/components/projects/upload.tsx index 86e44066..b057c9e2 100644 --- a/src/components/projects/upload.tsx +++ b/src/components/projects/upload.tsx @@ -29,8 +29,7 @@ const cards = [ export function Upload() { return (
- {/* Figma keeps this block narrow and centred: the text and its two reference cards side by side */} -
+

From 25427b19afe6ce82b9b7557079f2c542ebc7bad7 Mon Sep 17 00:00:00 2001 From: Bianca Roberta Ianosel Date: Mon, 2 Nov 2026 14:03:39 +0000 Subject: [PATCH 07/14] fix(projects): per-line title gradient on every caption card, search with any project CardCaption draws its title gradient per line, as ProjectCard did before it moved onto CardCaption: a wrapped title no longer fades on its second line. The search box shows whenever the page has projects to search, not only when the collection itself has some. --- src/components/card-caption.tsx | 7 ++++++- src/components/projects/collection.tsx | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/components/card-caption.tsx b/src/components/card-caption.tsx index a26f3139..70468f5c 100644 --- a/src/components/card-caption.tsx +++ b/src/components/card-caption.tsx @@ -40,7 +40,12 @@ export function CardCaption({ - {title} + + {/* Gradient per line (box-decoration-clone): a two-line title doesn't fade into a paler second line */} + + {title} + + {image ? ( ) : ( diff --git a/src/components/projects/collection.tsx b/src/components/projects/collection.tsx index c2c2c1c3..e320f5b2 100644 --- a/src/components/projects/collection.tsx +++ b/src/components/projects/collection.tsx @@ -33,7 +33,7 @@ export function Collection({ projects, searchable }: { projects: Project[]; sear

Esplora la raccolta completa dei progetti

- {projects.length > 0 && ( + {searchable.length > 0 && ( } type="search" From c26312e3112fa47c01b8e410d9fdc67c7b53e9d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriele=20Vigan=C3=B2?= Date: Wed, 28 Oct 2026 17:03:05 +0000 Subject: [PATCH 08/14] feat(groups): fix the results layout and give every link a hover state - Results and extra pages: one ~1024px column (Figma's list width) with a shared header component (back, centred title, close) laid out on a three-column grid, so long course names no longer run under the buttons on phones. - The general groups (site, school, level, course) were squeezed four per row with their names overlapping the platform buttons; they now sit one per row, two per row from lg, as in Figma. - Group rows: name and buttons share a row; when the card is narrower than 20rem the name gets its own line instead of breaking word by word. The `stacked` variant is gone. - Hover only where there is a link: the platform buttons darken, the report flags darken, course rows lighten and nudge their chevron, the landing cards get the identity hover. Keyboard focus rings on all of them. - Wizard: no wasted 16px inset on phones, where the glass panel is invisible, so the step labels fit at 320px. - Landing title uses the fluid display size and is the page's h1. Co-Authored-By: Claude Opus 5.5 --- src/app/groups/page.tsx | 5 +- src/components/card-course-group.tsx | 83 ++++++++++++++----------- src/components/card-course.tsx | 8 ++- src/components/groups/extra-groups.tsx | 24 ++----- src/components/groups/groups-result.tsx | 82 +++++++++--------------- src/components/groups/page-header.tsx | 44 +++++++++++++ src/components/wizard/course-step.tsx | 6 +- src/components/wizard/level-step.tsx | 5 +- src/components/wizard/wizard-shell.tsx | 4 +- 9 files changed, 147 insertions(+), 114 deletions(-) create mode 100644 src/components/groups/page-header.tsx diff --git a/src/app/groups/page.tsx b/src/app/groups/page.tsx index 1b6a4e18..b395c918 100644 --- a/src/app/groups/page.tsx +++ b/src/app/groups/page.tsx @@ -30,9 +30,9 @@ export default function Home() {
-

+

Groups -

+

@@ -46,6 +46,7 @@ export default function Home() { icon={group.icon} cta={group.cta} align="start" + hoverEffect className="w-full max-w-108" size="compact" /> diff --git a/src/components/card-course-group.tsx b/src/components/card-course-group.tsx index bfd21a3a..c38367b1 100644 --- a/src/components/card-course-group.tsx +++ b/src/components/card-course-group.tsx @@ -10,7 +10,7 @@ import { cn } from "@/lib/utils" import { Card, CardAction, CardTitle } from "./ui/card" export const cardCourseGroupVariants = cva( - "flex h-fit w-full min-w-0 flex-1 flex-row items-center gap-3 px-5 py-4 font-normal leading-6 tracking-[0.03125rem] sm:gap-5 sm:px-7.5 sm:py-6.25", + "@container h-auto w-full min-w-0 px-4 py-3 font-normal leading-6 tracking-[0.03125rem] sm:px-6 sm:py-4", { variants: { secondary: { @@ -33,7 +33,6 @@ export function CardCourseGroup({ tgGroupId, iconTelegram: IconTelegram = LiaTelegramPlane, secondary = false, - stacked = false, }: { groupName: string waLink?: string @@ -42,38 +41,42 @@ export function CardCourseGroup({ tgLink?: string tgGroupId?: number iconTelegram?: IconType - stacked?: boolean } & VariantProps) { - const actionClassName = cn("rounded-full p-2 sm:p-3.75", secondary ? "bg-[#51A2FF]" : "bg-[#74D4FF]") + // The platform buttons are the only links in the row, so they carry the hover state, not the card + const actionClassName = cn( + "rounded-full p-2 transition-[filter] group-hover/action:brightness-90 sm:p-3", + secondary ? "bg-[#51A2FF]" : "bg-[#74D4FF]" + ) return ( - - - {groupName} - -
- - + + {/* Name and buttons share a row; in a card narrower than 20rem the name gets its own line instead of wrapping word by word */} +
+ + {groupName} + +
+ + +
) @@ -100,7 +103,13 @@ function PlatformAction({ return ( <> - + {groupId !== undefined && ( @@ -108,8 +117,12 @@ function PlatformAction({ selectedReport={{ groupId, type: platform, reportType: "broken_link", reportedLink: link }} selectedGroupName={groupName} trigger={ - } /> diff --git a/src/components/card-course.tsx b/src/components/card-course.tsx index 25fa7766..6e562432 100644 --- a/src/components/card-course.tsx +++ b/src/components/card-course.tsx @@ -18,7 +18,8 @@ export function CardCourse({ iconSelect?: IconType }) { return ( - + // Always rendered inside a link or button: the row itself is the hover target +
{courseName} {(location || language) && ( @@ -37,7 +38,10 @@ export function CardCourse({ )}
- +
) diff --git a/src/components/groups/extra-groups.tsx b/src/components/groups/extra-groups.tsx index 6140f2f2..f0b25c96 100644 --- a/src/components/groups/extra-groups.tsx +++ b/src/components/groups/extra-groups.tsx @@ -1,6 +1,5 @@ -import Link from "next/link" -import { FiArrowLeft } from "react-icons/fi" import { CardCourseGroup } from "@/components/card-course-group" +import { GROUPS_COLUMN, GroupsPageHeader } from "@/components/groups/page-header" import { getVisibleGroups } from "@/queries/groups" import { EXTRA_LABEL, matchesLabelBranch } from "@/utils/labels" import { mergeGroupsByTitle } from "@/utils/merge-groups" @@ -12,22 +11,11 @@ export async function ExtraGroups() { ) return ( -
-
- - - -
-

Gruppi Extra

-
-
+
+ {extraGroups.length > 0 ? ( -
+
{extraGroups.map((g) => ( ) : ( -

- Nessun gruppo extra disponibile al momento. -

+

Nessun gruppo extra disponibile al momento.

)}
) diff --git a/src/components/groups/groups-result.tsx b/src/components/groups/groups-result.tsx index c1b3ba6d..1d92d879 100644 --- a/src/components/groups/groups-result.tsx +++ b/src/components/groups/groups-result.tsx @@ -1,7 +1,6 @@ -import Link from "next/link" import { notFound } from "next/navigation" -import { FiArrowLeft, FiX } from "react-icons/fi" import { CardCourseGroup } from "@/components/card-course-group" +import { GROUPS_COLUMN, GroupsPageHeader } from "@/components/groups/page-header" import { getLevel, getSchool } from "@/components/wizard/constants" import { getVisibleGroups } from "@/queries/groups" import type { VisibleGroup } from "@/queries/types" @@ -21,7 +20,7 @@ import { createStepHref } from "@/utils/step-href" const stepHref = createStepHref("/groups/didattica") -function GroupCard({ group, secondary, stacked }: { group: MergedGroup; secondary?: boolean; stacked?: boolean }) { +function GroupCard({ group, secondary }: { group: MergedGroup; secondary?: boolean }) { return ( ) } @@ -41,19 +39,19 @@ function GroupSection({ title, groups }: { title: string; groups: VisibleGroup[] if (merged.length === 0) return null return ( -
-
-

{title}

-

+

+
+

{title}

+

{merged.length} Gruppi

-
+
{merged.map((g) => ( ))}
-
+
) } @@ -101,48 +99,28 @@ export async function GroupsResult({ mergedLevelGroups.length > 0 || courseGroups.length > 0 + const generalGroups = [ + ...mergedSiteGroups.map((group) => ({ group, secondary: false })), + ...mergedSchoolGroups.map((group) => ({ group, secondary: false })), + ...mergedLevelGroups.map((group) => ({ group, secondary: true })), + ...mergedGeneralCourseGroups.map((group) => ({ group, secondary: true })), + ] + return ( -
-
- - - -
-

{courseName}

-

- Laurea {currentLevel.name} -

-
- - - -
- - {(mergedSiteGroups.length > 0 || - mergedSchoolGroups.length > 0 || - mergedLevelGroups.length > 0 || - mergedGeneralCourseGroups.length > 0) && ( -
- {mergedSiteGroups.map((g) => ( - - ))} - {mergedSchoolGroups.map((g) => ( - - ))} - {mergedLevelGroups.map((g) => ( - - ))} - {mergedGeneralCourseGroups.map((g) => ( - +
+ + + {/* The general groups (site, school, level, course) sit two per row, as in Figma, instead of squeezing into one */} + {generalGroups.length > 0 && ( +
+ {generalGroups.map(({ group, secondary }) => ( + ))}
)} @@ -156,7 +134,7 @@ export async function GroupsResult({ ))} {!hasAnyGroup && ( -

+

Nessun gruppo disponibile al momento per questo corso.

)} diff --git a/src/components/groups/page-header.tsx b/src/components/groups/page-header.tsx new file mode 100644 index 00000000..ef06eb8c --- /dev/null +++ b/src/components/groups/page-header.tsx @@ -0,0 +1,44 @@ +import Link from "next/link" +import { FiArrowLeft, FiX } from "react-icons/fi" + +/** Figma's list column for group results (~1040px at 1728), with the shared 24/40px gutters. */ +export const GROUPS_COLUMN = "mx-auto w-full min-w-0 max-w-5xl px-6 sm:px-10" + +const iconLinkClass = + "grid size-10 shrink-0 place-items-center rounded-full bg-white/60 outline-none transition-colors hover:bg-white focus-visible:ring-2 focus-visible:ring-blue-secondary" + +/** + * Back button, centred title and optional close button. A three-column grid keeps a long course name from running + * under the buttons on phones. + */ +export function GroupsPageHeader({ + title, + subtitle, + backHref, + replaceBack = false, + closeHref, +}: { + title: string + subtitle?: string + backHref: string + /** Go back without leaving a history entry, as the wizard steps do. */ + replaceBack?: boolean + closeHref?: string +}) { + return ( +
+ + + +
+

{title}

+ {subtitle &&

{subtitle}

} +
+ {closeHref && ( + + + + )} +
+ ) +} diff --git a/src/components/wizard/course-step.tsx b/src/components/wizard/course-step.tsx index 5f187649..a3b570ac 100644 --- a/src/components/wizard/course-step.tsx +++ b/src/components/wizard/course-step.tsx @@ -84,7 +84,11 @@ export async function CourseStep({ const courseCampus = campusFacet(facets) const courseLanguage = languageFacet(facets) return ( - + diff --git a/src/components/wizard/wizard-shell.tsx b/src/components/wizard/wizard-shell.tsx index f4adad57..f3f6bd40 100644 --- a/src/components/wizard/wizard-shell.tsx +++ b/src/components/wizard/wizard-shell.tsx @@ -34,8 +34,8 @@ export function WizardShell({ return (
- -
+ +
Date: Mon, 2 Nov 2026 13:42:06 +0000 Subject: [PATCH 09/14] fix(groups): roomier group actions, complete heading outline, no 500 offline - The four actions in a group row are 6px apart on phones too: 2px left the WhatsApp link and its report flag too easy to mistake. - The general groups get a visually hidden heading, so the outline no longer jumps from h1 to h3. - getVisibleGroups returns an empty list when the backend fails, like the other queries, so /groups/extra shows its empty state instead of a 500. --- src/components/card-course-group.tsx | 4 ++-- src/components/groups/groups-result.tsx | 9 ++++++--- src/queries/groups.ts | 9 +++++++-- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/components/card-course-group.tsx b/src/components/card-course-group.tsx index c38367b1..558a8de3 100644 --- a/src/components/card-course-group.tsx +++ b/src/components/card-course-group.tsx @@ -49,7 +49,7 @@ export function CardCourseGroup({ ) return ( - {/* Name and buttons share a row; in a card narrower than 20rem the name gets its own line instead of wrapping word by word */} + {/* In a card narrower than 20rem the name gets its own line, so it doesn't wrap word by word */}
{groupName} -
+
- {/* The general groups (site, school, level, course) sit two per row, as in Figma, instead of squeezing into one */} + {/* The site, school, level and course groups */} {generalGroups.length > 0 && ( -
+
+

+ Gruppi generali +

{generalGroups.map(({ group, secondary }) => ( ))} -
+
)} {cohorts.map((cohort) => ( diff --git a/src/queries/groups.ts b/src/queries/groups.ts index ad99fe23..9f58890f 100644 --- a/src/queries/groups.ts +++ b/src/queries/groups.ts @@ -10,8 +10,13 @@ export async function searchGroups(query: string, limit: number = 6) { } export async function getVisibleGroups(): Promise { - const groups = await trpc.groups.search.getAll.query() - return groups.filter((g): g is VisibleGroup => !g.hide && !!g.link) + try { + const groups = await trpc.groups.search.getAll.query() + return groups.filter((g): g is VisibleGroup => !g.hide && !!g.link) + } catch (error) { + console.error(error) + return [] + } } export async function getCoursesForLevel(school: string, level: string) { From 708280c65fe3e988718ac9b693201a09f822d0d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriele=20Vigan=C3=B2?= Date: Wed, 28 Oct 2026 17:09:35 +0000 Subject: [PATCH 10/14] fix(card-icon): generate the responsive size classes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CardIcon's responsive `size` ({ base, sm, md, lg }) builds "sm:…" classes at runtime, so Tailwind never generated the ones not written literally elsewhere (e.g. sm:px-8 for the homepage material shortcuts). Generate every prefixed variant of the size maps with @source inline(), and add a test that fails if classes.ts gains a class missing from that list. Co-Authored-By: Claude Opus 5.5 --- src/styles/globals.css | 7 +++++++ tests/card-icon-classes.test.mjs | 18 ++++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 tests/card-icon-classes.test.mjs diff --git a/src/styles/globals.css b/src/styles/globals.css index 98dafe80..f73816c4 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -8,6 +8,13 @@ @custom-variant dark (&:is(.dark *)); +/* + CardIcon's responsive `size` ({ base, sm, md, lg }) prefixes the classes in card-icon/classes.ts at runtime, so + Tailwind never sees "sm:h-14" and friends in the source. Generate every prefixed variant here. + tests/card-icon-classes.test.mjs fails if classes.ts gains a class that is missing from this list. +*/ +@source inline("{sm:,md:,lg:}{gap-2,gap-4,gap-6,h-6,h-10,h-12,h-14,h-16,h-32,h-44,w-6,w-10,w-12,w-14,w-16,w-32,w-44,p-3,p-6,p-8,px-8,py-4,typo-headline-extrasmall,typo-headline-medium,typo-label-large,typo-title-large}"); + @theme { --breakpoint-1xl: 85rem; /* 1360px */ --breakpoint-3xl: 90rem; /* 1440px */ diff --git a/tests/card-icon-classes.test.mjs b/tests/card-icon-classes.test.mjs new file mode 100644 index 00000000..3dfb96bf --- /dev/null +++ b/tests/card-icon-classes.test.mjs @@ -0,0 +1,18 @@ +import assert from "node:assert/strict" +import { readFileSync } from "node:fs" +import test from "node:test" + +const read = (path) => readFileSync(new URL(path, import.meta.url), "utf8") + +test("every CardIcon size class has its responsive variants generated", () => { + const classes = read("../src/components/card-icon/classes.ts") + const used = new Set([...classes.matchAll(/:\s*"([^"]+)"/g)].flatMap((match) => match[1].split(/\s+/))) + + const css = read("../src/styles/globals.css") + const inline = css.match(/@source inline\("\{sm:,md:,lg:\}\{([^}]+)\}"\)/) + assert.ok(inline, "the CardIcon @source inline() list is missing from globals.css") + const generated = new Set(inline[1].split(",")) + + const missing = [...used].filter((name) => !generated.has(name)) + assert.deepEqual(missing, [], `add these classes to the @source inline() list in globals.css: ${missing.join(", ")}`) +}) From 0398964f0d912dfd8f98e082efa4bd447071eaee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriele=20Vigan=C3=B2?= Date: Wed, 28 Oct 2026 17:14:20 +0000 Subject: [PATCH 11/14] feat(matricole): align Matricole with the shared layout and fix its grids - Hero, sections and FAQ use the shared container, hero offset, rhythm and section titles; the featured resources close the hero. - "Guide e Utility" is a section title (it was 22px on phones, smaller than the Telegram Hub card title) and sits closer to its own content. - Large resource cards: descriptions capped at 448px instead of 240px. - The large guide sits beside the 2x2 small guides only from xl, on a 5/7 split repeated by Materiali / Tech Tools, so "Mappa Microonde" no longer wraps; the four small guides use 20px titles. - Telegram Hub: two columns until xl (four columns clipped "Mercatino" at 1024px); the group has 24px padding on phones instead of 48px, so its links span the width as in the Figma mobile frame. - Resources with a link react to hover, the others don't. - Resources hidden on phones stay hidden: that's the Figma mobile frame, an open design decision. Co-Authored-By: Claude Opus 5.5 --- src/app/matricole/page.tsx | 5 +-- src/components/card-group/index.tsx | 8 +++-- src/components/matricole/cards.ts | 12 +++++++ src/components/matricole/faqs.tsx | 10 +++--- src/components/matricole/guides.tsx | 56 +++++++++++++++-------------- src/components/matricole/intro.tsx | 41 +++++++++------------ 6 files changed, 72 insertions(+), 60 deletions(-) diff --git a/src/app/matricole/page.tsx b/src/app/matricole/page.tsx index 1ac27122..59248e53 100644 --- a/src/app/matricole/page.tsx +++ b/src/app/matricole/page.tsx @@ -1,4 +1,5 @@ import type { Metadata } from "next" +import { CONTAINER, PAGE_MAIN } from "@/components/layout/page" import { FAQsPage } from "@/components/matricole/faqs" import { MatricoleGuides } from "@/components/matricole/guides" import { MatricoleIntro } from "@/components/matricole/intro" @@ -14,7 +15,7 @@ export const dynamic = "force-dynamic" export default async function MatricolePage() { const content = await getFreshmanContent() return ( -
+
{content ? ( <> @@ -22,7 +23,7 @@ export default async function MatricolePage() { ) : ( -

+

Le risorse per le matricole non sono disponibili al momento. Riprova più tardi.

)} diff --git a/src/components/card-group/index.tsx b/src/components/card-group/index.tsx index 84c994ae..8abe2f1c 100644 --- a/src/components/card-group/index.tsx +++ b/src/components/card-group/index.tsx @@ -5,16 +5,18 @@ import type { CardGroupProps } from "./types" export function CardGroup({ icon, title, description, children, horizontal = false, className }: CardGroupProps) { return ( - + {horizontal ? ( -
+
{children}
) : ( <> -
{children}
+
{children}
)} diff --git a/src/components/matricole/cards.ts b/src/components/matricole/cards.ts index 20889aea..2393a9b6 100644 --- a/src/components/matricole/cards.ts +++ b/src/components/matricole/cards.ts @@ -41,5 +41,17 @@ export function freshmanCardProps(card: FreshmanCard): CardIconProps { description: card.description, icon: icons[card.icon], href: card.href || undefined, + // Only resources that lead somewhere react to hover + hoverEffect: Boolean(card.href), } } + +/** + * The large resource cards (featured, "Guida della matricola", "Materiali"). Their one-sentence descriptions were + * capped at 240px inside cards three times as wide; 448px keeps them to one or two lines. + */ +export const FEATURED_CARD = { + align: "start", + size: "compact", + className: "h-full min-h-50 [&_p]:max-w-md", +} as const satisfies Partial diff --git a/src/components/matricole/faqs.tsx b/src/components/matricole/faqs.tsx index 3b6b4329..7a7d27fd 100644 --- a/src/components/matricole/faqs.tsx +++ b/src/components/matricole/faqs.tsx @@ -1,4 +1,6 @@ import AccordionList from "@/components/accordion-list" +import { CONTAINER, SECTION_TITLE } from "@/components/layout/page" +import { cn } from "@/lib/utils" import type { FreshmanContent } from "@/queries/types" export function FAQsPage({ faqs }: { faqs: FreshmanContent["faqs"] }) { @@ -8,12 +10,10 @@ export function FAQsPage({ faqs }: { faqs: FreshmanContent["faqs"] }) { content: faq.descriptionIt, })) return ( -
-

- Domande Frequenti tra le Matricole -

+
+

Domande Frequenti tra le Matricole

-
+
diff --git a/src/components/matricole/guides.tsx b/src/components/matricole/guides.tsx index 73ad7508..4381908d 100644 --- a/src/components/matricole/guides.tsx +++ b/src/components/matricole/guides.tsx @@ -1,7 +1,9 @@ import { CardIcon } from "@/components/card-icon" +import { CONTAINER, SECTION_TITLE } from "@/components/layout/page" +import { cn } from "@/lib/utils" import type { FreshmanContent } from "@/queries/types" import { CardGroup } from "../card-group" -import { freshmanCardProps } from "./cards" +import { FEATURED_CARD, freshmanCardProps } from "./cards" import { hub, techTools } from "./constants" export function MatricoleGuides({ content }: { content: FreshmanContent }) { @@ -9,55 +11,57 @@ export function MatricoleGuides({ content }: { content: FreshmanContent }) { const materials = content.resources.filter((card) => card.group === "materials") const tools = content.resources.filter((card) => card.group === "tools") return ( -
-
-

Guide e Utility

+
+

Guide e Utility

-
- {/* Hidden in mobile */} +
+ {/* + The large guide sits beside the small ones only from xl, and gets 5/12: at 1:1 the 2×2 grid squeezed the + titles onto two lines. The Materiali / Tech Tools row repeats the split so the columns line up. + */} +
+ {/* Hidden on phones, as in the Figma mobile frame */} {guides.slice(0, 1).map((guide) => ( ))} -
+
{guides.slice(1).map((guide) => ( - + ))}
- -
+ +
{content.telegram.map((card) => ( ))}
-
- {/* hidden in mobile */} +
+ {/* Hidden on phones, as in the Figma mobile frame */} {materials.map((material) => ( ))} - -
+ +
{tools.map((card) => ( ))} diff --git a/src/components/matricole/intro.tsx b/src/components/matricole/intro.tsx index 6152cdbb..d92d3f1b 100644 --- a/src/components/matricole/intro.tsx +++ b/src/components/matricole/intro.tsx @@ -1,33 +1,26 @@ import { CardIcon } from "@/components/card-icon" +import { CONTAINER, PAGE_HERO } from "@/components/layout/page" +import { Hero } from "@/components/ui/hero" +import { cn } from "@/lib/utils" import type { FreshmanContent } from "@/queries/types" -import { freshmanCardProps } from "./cards" +import { FEATURED_CARD, freshmanCardProps } from "./cards" export function MatricoleIntro({ resources }: { resources: FreshmanContent["resources"] }) { return ( -
-
-
-

- Matricole -

-

- Ecco una raccolta curata di risorse utili, guide e strumenti per supportare il tuo percorso. -

-
+ // The featured resources close the hero, so the hero's extra bottom air is dropped: the page gap follows +
+ -
- {resources - .filter((resource) => resource.group === "featured") - .map((resource) => ( - - ))} -
+ {/* Hidden on phones as in the Figma mobile frame (3351:13333): an open design decision, not changed here */} +
+ {resources + .filter((resource) => resource.group === "featured") + .map((resource) => ( + + ))}
) From 56a596e4a3f37d450a3f4f623d2272d9996ae84f Mon Sep 17 00:00:00 2001 From: Bianca Roberta Ianosel Date: Mon, 2 Nov 2026 13:42:51 +0000 Subject: [PATCH 12/14] fix(matricole): use the responsive card sizes and fill the tablet rows - The school tiles of the groups wizard use the sm size on phones, now that responsive CardIcon sizes are generated: at 320px they were about 400px tall each. - Materiali and Tech Tools sit side by side from lg: below xl the full-width card left half its width empty. - CardIcon's hover defaults to whether the card has an href, so callers no longer repeat Boolean(href). - The intro uses PAGE_HERO_TOP instead of cancelling PAGE_HERO's bottom padding at every breakpoint. --- src/components/card-icon/index.tsx | 2 +- src/components/card-icon/types.ts | 1 + src/components/home/materials.tsx | 1 - src/components/matricole/cards.ts | 2 -- src/components/matricole/guides.tsx | 11 ++++------- src/components/matricole/intro.tsx | 8 ++++---- src/components/wizard/school-step.tsx | 2 +- 7 files changed, 11 insertions(+), 16 deletions(-) diff --git a/src/components/card-icon/index.tsx b/src/components/card-icon/index.tsx index 79105d70..0a7fc50d 100644 --- a/src/components/card-icon/index.tsx +++ b/src/components/card-icon/index.tsx @@ -9,7 +9,7 @@ import type { CardIconProps } from "./types" import { getAlignmentClasses, getCardPaddingClasses, getContentGapClasses, getTitleSizeClasses } from "./utils" export function CardIcon(props: CardIconProps) { - const { title, icon, size = "md", href, hoverEffect = false, align = "center", className } = props + const { title, icon, size = "md", href, hoverEffect = Boolean(props.href), align = "center", className } = props const description = "description" in props ? props.description : undefined const cta = "cta" in props ? props.cta : undefined const Root = href ? "a" : "div" diff --git a/src/components/card-icon/types.ts b/src/components/card-icon/types.ts index d40cbef8..cdff14ae 100644 --- a/src/components/card-icon/types.ts +++ b/src/components/card-icon/types.ts @@ -22,6 +22,7 @@ export type SharedCardProps = { size?: ResponsiveCardSize align?: CardAlign href?: string + /** Defaults to whether the card has an `href`: only cards that lead somewhere react to hover. */ hoverEffect?: boolean className?: string } diff --git a/src/components/home/materials.tsx b/src/components/home/materials.tsx index fff6659b..9c732929 100644 --- a/src/components/home/materials.tsx +++ b/src/components/home/materials.tsx @@ -79,7 +79,6 @@ export function Materials() { description={card.description} icon={card.icon} href={card.href} - hoverEffect={Boolean(card.href)} size="md" className="hidden h-full xl:block" /> diff --git a/src/components/matricole/cards.ts b/src/components/matricole/cards.ts index 2393a9b6..dbd1e6b7 100644 --- a/src/components/matricole/cards.ts +++ b/src/components/matricole/cards.ts @@ -41,8 +41,6 @@ export function freshmanCardProps(card: FreshmanCard): CardIconProps { description: card.description, icon: icons[card.icon], href: card.href || undefined, - // Only resources that lead somewhere react to hover - hoverEffect: Boolean(card.href), } } diff --git a/src/components/matricole/guides.tsx b/src/components/matricole/guides.tsx index 4381908d..77b047dd 100644 --- a/src/components/matricole/guides.tsx +++ b/src/components/matricole/guides.tsx @@ -15,12 +15,9 @@ export function MatricoleGuides({ content }: { content: FreshmanContent }) {

Guide e Utility

- {/* - The large guide sits beside the small ones only from xl, and gets 5/12: at 1:1 the 2×2 grid squeezed the - titles onto two lines. The Materiali / Tech Tools row repeats the split so the columns line up. - */} + {/* 5/12 for the large card: at 1:1 the 2×2 grid next to it squeezed its titles onto two lines */}
- {/* Hidden on phones, as in the Figma mobile frame */} + {/* Hidden on phones, as in the mobile design */} {guides.slice(0, 1).map((guide) => ( -
- {/* Hidden on phones, as in the Figma mobile frame */} +
+ {/* Hidden on phones, as in the mobile design */} {materials.map((material) => ( + // The featured resources close the hero, so it takes only the hero's top padding +
- {/* Hidden on phones as in the Figma mobile frame (3351:13333): an open design decision, not changed here */} + {/* Hidden on phones, as in the mobile design */}
{resources .filter((resource) => resource.group === "featured") diff --git a/src/components/wizard/school-step.tsx b/src/components/wizard/school-step.tsx index 4c1fa760..1dd8eb18 100644 --- a/src/components/wizard/school-step.tsx +++ b/src/components/wizard/school-step.tsx @@ -27,7 +27,7 @@ export function SchoolStep({ title={school.name} icon={SCHOOL_ICONS[school.slug] ?? DEFAULT_SCHOOL_ICON} href={stepHref({ school: school.slug })} - hoverEffect + size={{ base: "sm", md: "md" }} /> ))}
From a905c4be6e8a3f317f75873cd18eeb17dcffb913 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriele=20Vigan=C3=B2?= Date: Wed, 28 Oct 2026 17:30:59 +0000 Subject: [PATCH 13/14] feat(associations): honest logo fallback and a steadier accordion - Associations without a logo show their initials instead of ESN's logo, which came from the Figma example; the unused placeholder asset is gone. - Page on the shared container, hero and rhythm; the list keeps Figma's 1200px column. - Accordion: constant padding open or closed (the name no longer jumps), description aligned with the name from md and capped at a readable measure, names at 20/22/28px instead of 22/45px, smaller logos on phones. - The clickable header brightens on hover and rings the item on keyboard focus; social links get accessible names, 40px targets and rel=noopener; "Segui l'associazione" drops its blue gradient and only shows with links. - Empty or unreachable list: a dashed placeholder like /projects. - Shared Hero: the title size is capped so its longest word fits the content width; "Associazioni" and "Contattaci" overflowed phones and tablets. Titles that already fit keep the Figma sizes. - /about association bodies keep their look on the new accordion padding. Co-Authored-By: Claude Opus 5.5 --- public/logos/esn.svg | 9 -- src/app/associations/page.tsx | 16 ++-- .../accordion-content.tsx | 14 +-- .../accordion-association/accordion-item.tsx | 8 +- .../accordion-trigger.tsx | 7 +- .../accordion-association/accordion.tsx | 8 +- .../accordion-association/index.tsx | 86 +++++++++++++------ src/components/accordion-association/types.ts | 3 +- .../associations/associations-list.tsx | 17 ++-- .../structure/association-bodies.tsx | 6 +- src/components/ui/hero.tsx | 14 ++- 11 files changed, 114 insertions(+), 74 deletions(-) delete mode 100644 public/logos/esn.svg diff --git a/public/logos/esn.svg b/public/logos/esn.svg deleted file mode 100644 index 60ad33c7..00000000 --- a/public/logos/esn.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/src/app/associations/page.tsx b/src/app/associations/page.tsx index e4760a91..8a8d2426 100644 --- a/src/app/associations/page.tsx +++ b/src/app/associations/page.tsx @@ -1,5 +1,6 @@ import type { Metadata } from "next" import { AssociationsList } from "@/components/associations/associations-list" +import { CONTAINER, PAGE_HERO, PAGE_MAIN } from "@/components/layout/page" import { Hero } from "@/components/ui/hero" import { getAllAssociations } from "@/queries/associations" @@ -14,12 +15,17 @@ export default async function AssociationsPage() { const associations = await getAllAssociations() return ( -
- +
+
+ +
-
- -
+ {/* Figma's list column: 1200px at 1728 */} +
+
+ +
+
) } diff --git a/src/components/accordion-association/accordion-content.tsx b/src/components/accordion-association/accordion-content.tsx index 69f03cc7..1437aedb 100644 --- a/src/components/accordion-association/accordion-content.tsx +++ b/src/components/accordion-association/accordion-content.tsx @@ -10,19 +10,11 @@ export function AccordionContent({ return ( -
- {children} -
+ {/* From md the text lines up with the name: trigger padding (32) + logo (64) + gap (24) */} +
{children}
) } diff --git a/src/components/accordion-association/accordion-item.tsx b/src/components/accordion-association/accordion-item.tsx index 5668b289..0abe9a37 100644 --- a/src/components/accordion-association/accordion-item.tsx +++ b/src/components/accordion-association/accordion-item.tsx @@ -6,14 +6,16 @@ import { AccordionBackground } from "./accordion-background" export function AccordionItem({ className, children, ...props }: React.ComponentProps) { return ( - + - {} +
{children}
diff --git a/src/components/accordion-association/accordion-trigger.tsx b/src/components/accordion-association/accordion-trigger.tsx index 221a3f27..715c06c2 100644 --- a/src/components/accordion-association/accordion-trigger.tsx +++ b/src/components/accordion-association/accordion-trigger.tsx @@ -13,10 +13,9 @@ export function AccordionTrigger({ svg]:rotate-180", className )} diff --git a/src/components/accordion-association/accordion.tsx b/src/components/accordion-association/accordion.tsx index ac919542..888a7d3e 100644 --- a/src/components/accordion-association/accordion.tsx +++ b/src/components/accordion-association/accordion.tsx @@ -3,5 +3,11 @@ import type * as React from "react" import { cn } from "@/lib/utils" export function Accordion({ className, ...props }: React.ComponentProps) { - return + return ( + + ) } diff --git a/src/components/accordion-association/index.tsx b/src/components/accordion-association/index.tsx index 470f3cb6..ff257a04 100644 --- a/src/components/accordion-association/index.tsx +++ b/src/components/accordion-association/index.tsx @@ -4,7 +4,35 @@ import { Accordion } from "./accordion" import { AccordionContent } from "./accordion-content" import { AccordionItem } from "./accordion-item" import { AccordionTrigger } from "./accordion-trigger" -import type { AccordionAssociationProps } from "./types" +import type { AccordionAssociationItem, AccordionAssociationProps } from "./types" + +const LOGO_CLASS = "size-10 shrink-0 rounded-full bg-white sm:size-12 md:size-16" + +/** First letter of the first two words: "Lista Aperta - Per il diritto…" → "LA" */ +function initials(name: string) { + return name + .split(/\s+/) + .filter((word) => /\p{L}/u.test(word)) + .slice(0, 2) + .map((word) => word.match(/\p{L}/u)?.[0] ?? "") + .join("") + .toUpperCase() +} + +function AssociationLogo({ name, logo }: Pick) { + // The name sits right next to the logo, so both are decorative for screen readers + if (!logo) { + return ( + + {initials(name)} + + ) + } + return +} export default function AccordionAssociation({ accordionItems, defaultValue }: AccordionAssociationProps) { return ( @@ -12,35 +40,39 @@ export default function AccordionAssociation({ accordionItems, defaultValue }: A {accordionItems.map((item) => ( -
- {item.name} - {item.name} -
+ + + {/* Long single words may still break on the narrowest phones rather than run under the chevron */} + {item.name} +
- {item.content} -
- - Segui l'associazione - -
- {item.links.map((link) => ( - - - - - ))} + {item.content &&

{item.content}

} + {item.links.length > 0 && ( +
+ Segui l'associazione +
    + {item.links.map((link) => ( +
  • + + + + +
  • + ))} +
-
+ )} ))} diff --git a/src/components/accordion-association/types.ts b/src/components/accordion-association/types.ts index fce94d35..216727e5 100644 --- a/src/components/accordion-association/types.ts +++ b/src/components/accordion-association/types.ts @@ -4,7 +4,8 @@ import type { GradientIconType } from "../gradient-icon" export type AccordionAssociationItem = { value: string name: string - logo: StaticImageData | string + /** `null` when the association has no logo: its initials are shown instead */ + logo: StaticImageData | string | null content: React.ReactNode links: { key: string diff --git a/src/components/associations/associations-list.tsx b/src/components/associations/associations-list.tsx index 847a39a3..65dd312a 100644 --- a/src/components/associations/associations-list.tsx +++ b/src/components/associations/associations-list.tsx @@ -13,7 +13,6 @@ import { import AccordionAssociation from "@/components/accordion-association" import type { GradientIconType } from "@/components/gradient-icon" import type { ApiOutput } from "@/types" -import esnLogo from "../../../public/logos/esn.svg" type Association = ApiOutput["web"]["associations"]["getAllAssociations"][number] type AssociationLinks = Association["links"] @@ -33,13 +32,19 @@ const LINK_CONFIG: { key: keyof AssociationLinks; label: string; icon: GradientI export function AssociationsList({ associations }: { associations: Association[] }) { if (associations.length === 0) { - return

Nessuna associazione disponibile al momento.

+ // The query returns an empty list on backend errors too, so the message covers both cases + return ( +

+ Le associazioni non sono disponibili al momento. Riprova più tardi. +

+ ) } const accordionItems = associations.map((association) => ({ value: String(association.id), name: association.name, - logo: association.logo ?? esnLogo, + // No placeholder logo: an association without one gets its initials (see AccordionAssociation) + logo: association.logo, content: association.descriptionIt, links: LINK_CONFIG.filter(({ key }) => association.links[key]).map(({ key, label, icon, mailto }) => { const value = association.links[key] as string @@ -47,9 +52,5 @@ export function AssociationsList({ associations }: { associations: Association[] }), })) - return ( -
- -
- ) + return } diff --git a/src/components/structure/association-bodies.tsx b/src/components/structure/association-bodies.tsx index a808421e..2ea436e6 100644 --- a/src/components/structure/association-bodies.tsx +++ b/src/components/structure/association-bodies.tsx @@ -13,12 +13,10 @@ export function AssociationBodies() { {associationBodies.map((body) => ( - + {body.title} - - {body.description} - + {body.description} ))} diff --git a/src/components/ui/hero.tsx b/src/components/ui/hero.tsx index bcd563e3..d82b2b08 100644 --- a/src/components/ui/hero.tsx +++ b/src/components/ui/hero.tsx @@ -17,6 +17,10 @@ const descriptionGradientStyle = { "--hero-gradient-desktop": DESCRIPTION_GRADIENT_DESKTOP, } as CSSProperties +function longestWord(text: string) { + return Math.max(...text.split(/\s+/).map((word) => word.length)) +} + export function Hero({ title, description, @@ -33,7 +37,15 @@ export function Hero({ {/* Figma "lightheme-XLarge text effect": #020618 → #6e6e6e at 82.69% */} {/* The gradient is drawn per line (box-decoration-clone), so a title that wraps keeps the full effect on each line */}

- + {/* + A word can't wrap, so a long one ("Associazioni") overflowed narrow phones at 57px and tablets at 120px. + The size is capped so the longest word fits the content width (DM Sans ≈ 0.55em per letter, measured on "Contattaci"); titles whose + words already fit keep the Figma sizes. + */} + {title}

From 0c68c217c4d0f38665cfc0d9b8a480ff3fb47e86 Mon Sep 17 00:00:00 2001 From: Bianca Roberta Ianosel Date: Mon, 2 Nov 2026 13:43:18 +0000 Subject: [PATCH 14/14] refactor(hero): a description prop instead of reaching into Hero This PR sets the title size on Hero's inner span, so /storia's [&_h2]:typo-display-fluid-extralarge override stopped having any effect. It is removed, and the description width /storia needs goes through a new descriptionClassName prop. Hero's comments are cut to the reason behind the size cap. --- src/components/storia/hero.tsx | 2 +- src/components/ui/hero.tsx | 18 ++++++++---------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/components/storia/hero.tsx b/src/components/storia/hero.tsx index 071ac805..172cd87e 100644 --- a/src/components/storia/hero.tsx +++ b/src/components/storia/hero.tsx @@ -11,7 +11,7 @@ export function HistoryHero() { title="La nostra storia" description="Da un gruppo Telegram a una rete costruita dagli studenti, per gli studenti." gradientDescription - className="[&_h2]:typo-display-fluid-extralarge [&_p]:max-w-145 [&_p]:text-balance" + descriptionClassName="max-w-145 text-balance" />