From a0cf4797004683c81e25436e1df80f1557e08707 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriele=20Vigan=C3=B2?= Date: Tue, 27 Oct 2026 13:36:42 +0000 Subject: [PATCH 01/22] feat(home): refine homepage layout, proportions and card states MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Hero: fluid title (36→72px) back on two lines on desktop, freshman link grouped with the primary action instead of floating in the corner, shorter hero so Materials is reachable. - Materials and Projects: one container (max 1536px, 24/40/64px gutters), Figma's mirrored title/cards composition from xl, a centred stacked layout below it, and content-sized cards in min/max-bounded grids so card widths no longer halve across breakpoints. - Project cards show where they lead (domain + arrow) in the corner Figma reserves for an action; the mobile carousel peeks the next card. - Only cards with a destination react to hover; linked cards get a keyboard focus ring. "Carica" no longer links to "#", the material shortcuts open the real archive. - About us: section-sized instead of full screen, h2, grey gradient per the style guide rule (decorated background or coloured text, not both). - Shared: fluid display utilities, CardCaption footer slot, visible carousel dots. Co-Authored-By: Claude Opus 5.5 --- src/components/card-caption.tsx | 19 ++++-- src/components/card-icon/index.tsx | 3 +- src/components/home/about-us.tsx | 9 +-- src/components/home/hero.tsx | 40 +++++------ src/components/home/materials.tsx | 105 ++++++++++++++++------------- src/components/home/projects.tsx | 56 +++++++++------ src/components/ui/carousel.tsx | 7 +- src/lib/utils.ts | 2 + src/styles/typography.css | 14 ++++ 9 files changed, 159 insertions(+), 96 deletions(-) diff --git a/src/components/card-caption.tsx b/src/components/card-caption.tsx index 0f04b101..43fe8e3b 100644 --- a/src/components/card-caption.tsx +++ b/src/components/card-caption.tsx @@ -1,6 +1,7 @@ import Link from "next/link" import type { ReactNode } from "react" import type { IconType } from "react-icons" +import { cn } from "@/lib/utils" import { Card, CardAction, CardContent, CardHeader, CardTitle } from "./ui/card" export function CardCaption({ @@ -9,6 +10,7 @@ export function CardCaption({ icon, iconPosition = "right", href, + footer, className, }: { title: string @@ -16,18 +18,27 @@ export function CardCaption({ icon?: IconType iconPosition?: "top" | "right" href?: string + /** Pinned to the bottom of the card, e.g. where a link leads to. */ + footer?: ReactNode className?: string }) { return ( - - {href && } + // Only cards that lead somewhere react to hover + a:focus-visible]:ring-2 has-[>a:focus-visible]:ring-blue-secondary", className)} + > + {href && } {title} {icon && } - {typeof caption === "string" ?

{caption}

: caption}
+ + {typeof caption === "string" ?

{caption}

: caption} +
+ {footer &&
{footer}
}
) } diff --git a/src/components/card-icon/index.tsx b/src/components/card-icon/index.tsx index ea197258..79105d70 100644 --- a/src/components/card-icon/index.tsx +++ b/src/components/card-icon/index.tsx @@ -22,13 +22,14 @@ export function CardIcon(props: CardIconProps) { a:focus-visible]:ring-2 has-[>a:focus-visible]:ring-blue-secondary", className )} > diff --git a/src/components/home/about-us.tsx b/src/components/home/about-us.tsx index f879f913..37570e88 100644 --- a/src/components/home/about-us.tsx +++ b/src/components/home/about-us.tsx @@ -4,13 +4,14 @@ import { Button } from "@/components/ui/button" export function AboutUs() { return ( -
-

+
+ {/* Grey gradient only: the background already carries the decoration (style guide: shapes OR coloured text) */} +

PoliNetwork unisce gli studenti del Poli: gruppi, risorse e supporto, tutti in un'unica community. -

+

- - -
- + {/* The freshman path sits next to the primary action, instead of floating alone in the corner */} +
+ + +
+
) diff --git a/src/components/home/materials.tsx b/src/components/home/materials.tsx index 102a8a10..07fad77c 100644 --- a/src/components/home/materials.tsx +++ b/src/components/home/materials.tsx @@ -4,80 +4,95 @@ import { CardIcon } from "@/components/card-icon" import { Button } from "@/components/ui/button" import { CardCaption } from "../card-caption" +const MATERIALS_URL = "https://polinetwork.org/it/materials/view/" + +// "Carica" has no destination yet (the old upload page is empty), so it stays a plain card instead of a dead link const featuredCards = [ { title: "Carica", description: - "Hai appunti, dispense o temi d'esame che vuoi condividere? Caricali qui! Il tuo contributo é prezioso per aiutare migliaia di colleghi con materiale aggiornato!", + "Hai appunti, dispense o temi d'esame che vuoi condividere? Caricali qui! Il tuo contributo è prezioso per aiutare migliaia di colleghi con materiale aggiornato!", icon: FiUploadCloud, - size: "lg", - href: "#", }, { title: "Visualizza", description: - "Cerca cio che ti serve per il tuo prossimo esame. Naviga tra i corsi di studio e trova facilmente appunti, esercizi e dispense condivisi da altri studenti come te.", + "Cerca ciò che ti serve per il tuo prossimo esame. Naviga tra i corsi di studio e trova facilmente appunti, esercizi e dispense condivisi da altri studenti come te.", icon: FiBookOpen, - size: "lg", - href: "#", + href: MATERIALS_URL, }, -] as const +] +// The archive has no per-type pages: every shortcut opens it const quickLinks = [ - { title: "Dispense", icon: FiBook, size: { base: "xs", sm: "sm" }, href: "#" }, - { title: "Esami", icon: FiFileText, size: { base: "xs", sm: "sm" }, href: "#" }, - { - title: "Appunti", - icon: FiClipboard, - size: { base: "xs", sm: "sm" }, - href: "https://polinetwork.org/it/materials/view/", - }, -] as const + { title: "Dispense", icon: FiBook }, + { title: "Esami", icon: FiFileText }, + { title: "Appunti", icon: FiClipboard }, +] export function Materials() { return ( -
-
- {/* Desktop Cards */} -
- {featuredCards.map((card) => ( - - ))} -
- {/* Mobile Cards */} -
- {featuredCards.map((card) => ( - - ))} -
+
+

+ Materials +

-
- {quickLinks.map((card) => ( - - ))} -
-
- -
-

- Materials -

-

- Il piu grande archivio didattico creato dagli studenti per gli studenti del Politecnico di Milano. Cerca tra +

+

+ Il più grande archivio didattico creato dagli studenti per gli studenti del Politecnico di Milano. Cerca tra migliaia di appunti, dispense, temi d'esame e molto altro. Carica i tuoi file per far crescere la community e - trova tutto cio che ti serve, organizzato per corso di studi. + trova tutto ciò che ti serve, organizzato per corso di studi.

Il più grande archivio didattico degli studenti del Politecnico. Trova appunti, dispense ed esami, oppure carica i tuoi file per far crescere la community.

+ + {/* Below xl the cards follow the text, centered and capped so they keep their proportions on tablets */} +
+
+ {featuredCards.map((card) => ( +
+ + +
+ ))} +
+ +
+ {quickLinks.map((card) => ( + + ))} +
+
) } diff --git a/src/components/home/projects.tsx b/src/components/home/projects.tsx index b66fafcc..4d67233f 100644 --- a/src/components/home/projects.tsx +++ b/src/components/home/projects.tsx @@ -29,14 +29,31 @@ const featuredCards = [ }, ] as const +function ProjectCard({ className, ...card }: (typeof featuredCards)[number] & { className?: string }) { + return ( + + {new URL(card.href).hostname} + + + } + /> + ) +} + export function Projects() { return ( -
-
-

- 15+ Projects -

-

+

+

+ 15+ Projects +

+ +
+

Progetti e strumenti creati dalla nostra community. Qui trovi le iniziative nate dagli studenti del Politecnico per semplificare la vita universitaria e creare innovazione. Hai un'idea o un progetto? Proponilo! Puoi trovare collaboratori, ricevere supporto dal nostro team e dare vita alla tua soluzione. @@ -46,33 +63,30 @@ export function Projects() { l’innovazione. Hai un’idea? Proponila, trova collaboratori e realizza la tua soluzione.

-
-
- {featuredCards.map((card) => ( - - ))} -
+
+ {featuredCards.map((card) => ( + + ))}
-
- - + {/* Mobile: the next card peeks in from the edge, so the row reads as scrollable */} +
+ + {featuredCards.map((card) => ( - -
- -
+ + ))}
- +
diff --git a/src/components/ui/carousel.tsx b/src/components/ui/carousel.tsx index e6bc6262..9bdd9320 100644 --- a/src/components/ui/carousel.tsx +++ b/src/components/ui/carousel.tsx @@ -177,8 +177,11 @@ function CarouselDots({ className, ...props }: React.ComponentProps<"div">) { onClick={() => api?.scrollTo(index)} // Figma "slider dots": 16px current dot, 12px others, 6px apart. The pseudo-element enlarges the hit area. className={cn( - "after:-inset-1.5 relative shrink-0 rounded-full border border-white/50 bg-background-blur backdrop-blur-md transition-[width,height] after:absolute after:content-[''] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-secondary", - selectedIndex === index ? "size-4" : "size-3" + "after:-inset-1.5 relative shrink-0 rounded-full border backdrop-blur-md transition-[width,height] after:absolute after:content-[''] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-secondary", + // Glass-only dots disappear on the light background: give the current one the brand colour + selectedIndex === index + ? "size-4 border-blue-primary bg-blue-primary" + : "size-3 border-slate-300 bg-background-blur" )} /> ))} diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 753b67d7..b2bc774d 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -11,6 +11,8 @@ const twMerge = extendTailwindMerge({ "font-size": [ { typo: [ + "display-fluid-extralarge", + "display-fluid-large", "display-extralarge", "display-large", "display-medium", diff --git a/src/styles/typography.css b/src/styles/typography.css index 8daa9847..08f824fe 100644 --- a/src/styles/typography.css +++ b/src/styles/typography.css @@ -21,6 +21,20 @@ @apply font-dm-sans font-normal text-[2.25rem]/[44px] tracking-normal; } +/* + Fluid display, for page-level titles that would otherwise jump between fixed steps. + Figma only has 402px and 1728px frames; these interpolate between the two sizes it uses. +*/ +@utility typo-display-fluid-large { + /* ~34px at 320px, 36px at 402px viewport → 72px from ~1600px (Figma hero: 77px at 1728) */ + @apply font-dm-sans font-medium text-[clamp(2rem,1.5rem+3vw,4.5rem)] leading-[1.12] tracking-[-0.02em]; +} + +@utility typo-display-fluid-extralarge { + /* 44px at 402px viewport → 120px at 1728px (display-extralarge) */ + @apply font-dm-sans font-medium text-[clamp(2.75rem,1.25rem+6vw,7.5rem)] leading-[1.05] tracking-[-0.02em]; +} + /* Headline */ @utility typo-headline-large { @apply font-dm-sans font-normal text-[2rem]/[40px] tracking-normal; From d85d8a42c08e89a669221d04fdb7ba105e2412f9 Mon Sep 17 00:00:00 2001 From: Bianca Roberta Ianosel Date: Mon, 2 Nov 2026 09:17:07 +0000 Subject: [PATCH 02/22] fix(home): plain archive tiles, content-sized hero, visible carousel dots - Dispense, Esami and Appunti are no longer three links to the same archive page: they describe what it holds, like the Carica card. - The hero is sized by its content, so Materials is no longer pushed below the fold on 900px-tall screens. - Inactive carousel dots use slate-500, readable on the page background. - Keyed Fragment instead of a "contents" div; comments that narrated the change are removed. --- src/components/home/about-us.tsx | 1 - src/components/home/hero.tsx | 3 +-- src/components/home/materials.tsx | 23 ++++++++--------------- src/components/ui/carousel.tsx | 3 +-- 4 files changed, 10 insertions(+), 20 deletions(-) diff --git a/src/components/home/about-us.tsx b/src/components/home/about-us.tsx index 37570e88..50dc3bc5 100644 --- a/src/components/home/about-us.tsx +++ b/src/components/home/about-us.tsx @@ -5,7 +5,6 @@ import { Button } from "@/components/ui/button" export function AboutUs() { return (
- {/* Grey gradient only: the background already carries the decoration (style guide: shapes OR coloured text) */}

PoliNetwork unisce gli studenti del Poli: gruppi, risorse e supporto, tutti in un'unica community.

diff --git a/src/components/home/hero.tsx b/src/components/home/hero.tsx index 74644dd5..1d06d53f 100644 --- a/src/components/home/hero.tsx +++ b/src/components/home/hero.tsx @@ -5,7 +5,7 @@ import { GroupSearch } from "./group-search" export function Hero() { return ( -
+

Trova gruppi, risorse e supporto tra gli studenti del Polimi @@ -14,7 +14,6 @@ export function Hero() {
- {/* The freshman path sits next to the primary action, instead of floating alone in the corner */}
- {/* Below xl the cards follow the text, centered and capped so they keep their proportions on tablets */}
{featuredCards.map((card) => ( -
+ -
+ ))}
- {quickLinks.map((card) => ( - + {archiveTypes.map((card) => ( + ))}
diff --git a/src/components/ui/carousel.tsx b/src/components/ui/carousel.tsx index 9bdd9320..b2cce1a9 100644 --- a/src/components/ui/carousel.tsx +++ b/src/components/ui/carousel.tsx @@ -178,10 +178,9 @@ function CarouselDots({ className, ...props }: React.ComponentProps<"div">) { // Figma "slider dots": 16px current dot, 12px others, 6px apart. The pseudo-element enlarges the hit area. className={cn( "after:-inset-1.5 relative shrink-0 rounded-full border backdrop-blur-md transition-[width,height] after:absolute after:content-[''] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-secondary", - // Glass-only dots disappear on the light background: give the current one the brand colour selectedIndex === index ? "size-4 border-blue-primary bg-blue-primary" - : "size-3 border-slate-300 bg-background-blur" + : "size-3 border-slate-500 bg-background-blur" )} /> ))} From 67e6669a59a32c642b2730e5d08e9ab8736b773a Mon Sep 17 00:00:00 2001 From: Bianca Roberta Ianosel Date: Mon, 2 Nov 2026 14:18:31 +0000 Subject: [PATCH 03/22] fix(home): center the hero on screen, restore About us, keep descenders whole - The hero fills the screen height again and centres its content in it. - About us goes back to its full-screen block with the blue gradient title; it stays an h2, so the page keeps a single h1. - The fluid display styles use a 1.2 line height: at 1.12 and 1.05 the gradient's clip cut the descenders ("gli", "Projects"). --- src/components/home/about-us.tsx | 4 ++-- src/components/home/hero.tsx | 2 +- src/styles/typography.css | 5 +++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/components/home/about-us.tsx b/src/components/home/about-us.tsx index 50dc3bc5..10c789c9 100644 --- a/src/components/home/about-us.tsx +++ b/src/components/home/about-us.tsx @@ -4,8 +4,8 @@ import { Button } from "@/components/ui/button" export function AboutUs() { return ( -
-

+
+

PoliNetwork unisce gli studenti del Poli: gruppi, risorse e supporto, tutti in un'unica community.

diff --git a/src/components/home/hero.tsx b/src/components/home/hero.tsx index 1d06d53f..32d580ac 100644 --- a/src/components/home/hero.tsx +++ b/src/components/home/hero.tsx @@ -5,7 +5,7 @@ import { GroupSearch } from "./group-search" export function Hero() { return ( -
+

Trova gruppi, risorse e supporto tra gli studenti del Polimi diff --git a/src/styles/typography.css b/src/styles/typography.css index 08f824fe..e85c5d0d 100644 --- a/src/styles/typography.css +++ b/src/styles/typography.css @@ -24,15 +24,16 @@ /* Fluid display, for page-level titles that would otherwise jump between fixed steps. Figma only has 402px and 1728px frames; these interpolate between the two sizes it uses. + Line height 1.2 keeps descenders inside the box: gradient titles clip their text to it. */ @utility typo-display-fluid-large { /* ~34px at 320px, 36px at 402px viewport → 72px from ~1600px (Figma hero: 77px at 1728) */ - @apply font-dm-sans font-medium text-[clamp(2rem,1.5rem+3vw,4.5rem)] leading-[1.12] tracking-[-0.02em]; + @apply font-dm-sans font-medium text-[clamp(2rem,1.5rem+3vw,4.5rem)] leading-[1.2] tracking-[-0.02em]; } @utility typo-display-fluid-extralarge { /* 44px at 402px viewport → 120px at 1728px (display-extralarge) */ - @apply font-dm-sans font-medium text-[clamp(2.75rem,1.25rem+6vw,7.5rem)] leading-[1.05] tracking-[-0.02em]; + @apply font-dm-sans font-medium text-[clamp(2.75rem,1.25rem+6vw,7.5rem)] leading-[1.2] tracking-[-0.02em]; } /* Headline */ From 74ef0e810b18ea04f870c94b141c8c91773ba4c3 Mon Sep 17 00:00:00 2001 From: Bianca Roberta Ianosel Date: Mon, 2 Nov 2026 14:31:47 +0000 Subject: [PATCH 04/22] fix(ui): give every card the same white glass border --- src/components/ui/card.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx index 4e518304..27827005 100644 --- a/src/components/ui/card.tsx +++ b/src/components/ui/card.tsx @@ -18,7 +18,7 @@ function Card({ data-slot="card" data-size={size} className={cn( - "group/card relative flex h-66 w-78 flex-col gap-4 overflow-hidden rounded-[1.25rem] bg-background-blur text-card-foreground text-sm ring-foreground/10 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl", + "group/card relative flex h-66 w-78 flex-col gap-4 overflow-hidden rounded-rectangles border-white/50 bg-background-blur text-card-foreground text-sm ring-foreground/10 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl", className )} {...props} From b10fcbf677b86e5557dbf1a3de6829ffe1c4ffb8 Mon Sep 17 00:00:00 2001 From: Bianca Roberta Ianosel Date: Mon, 2 Nov 2026 14:35:39 +0000 Subject: [PATCH 05/22] fix(home): scale the About us statement with the viewport like the hero title --- src/components/home/about-us.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/home/about-us.tsx b/src/components/home/about-us.tsx index 10c789c9..9011e323 100644 --- a/src/components/home/about-us.tsx +++ b/src/components/home/about-us.tsx @@ -5,7 +5,7 @@ import { Button } from "@/components/ui/button" export function AboutUs() { return (
-

+

PoliNetwork unisce gli studenti del Poli: gruppi, risorse e supporto, tutti in un'unica community.

From 1f7131fa6e2b99d96ab2820ad95664ecfb32005e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriele=20Vigan=C3=B2?= Date: Mon, 2 Nov 2026 16:04:15 +0100 Subject: [PATCH 06/22] feat(team): calmer team sections with non-interactive specialisms (#203) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(storia): de-cardify the history page and align it with Figma - Hero: Figma's centred XL title (fluid) with the two anchor actions; the rotated portrait collage and floating "200+" chip are gone (the board portraits already appear under "I nostri direttivi"). - "Come è nato tutto": the two 2016/2022 cards that repeated the timeline right below are removed; the text stands alone until the archive photo Figma reserves exists. - Timeline: nodes on a line (vertical below lg, horizontal from lg) instead of four hoverable glass cards; the year is the link to its chapter. - Chapters: numeral in the margin and one continuous vertical rule across all six; diagrams drawn without containers (icon chains, 5 → ~25 → 4, before → after columns, status list) instead of stacks of glass cards. - No hover on things that are not links (board portraits, the closing statement); "L'obiettivo è raggiunto" is a statement, not a card. - Projects note, thanks list, board summary and "La storia continua" lose their glass panels; the closing gradient panel and the filled "?" pill (which read as a button) become years on a line with an open slot. - One container for every section (Figma's 1272px column, 24/40/64px gutters) and left alignment at every width for this long reading page. Co-Authored-By: Claude Opus 5.5 * refactor(storia): keep milestone links and icons with their data Each milestone carries its own anchor and icon instead of two arrays kept in step by position. The chapter numeral column is 3rem, wide enough for "02" at 36px. The closing statement is a div: a figure without a caption has no meaning here. * feat(chi-siamo): refine Su di noi layout within its current structure Improve the page where Figma v1 (implemented) and v2 agree, without switching to v2, which is still an open design decision. - Hero: drop the two CTAs (in neither version), add the "La nostra visione" label both versions use, same top spacing as /storia. - Il problema: questions become compact non-interactive chips (icon first) in a 5/7 rail, instead of three tall button-like cards. - I numeri: figures at headline size beside the icon (they were 45px and outranked the section titles). - Da dove veniamo / Come siamo organizzati and Sostieni: free columns on the background; no glass cards holding section titles, no nested panel, no 57px "5x1000" and 36px tax code. - One container, gutters and title/lead roles shared with /storia and the homepage; text left-aligned on mobile. Co-Authored-By: Claude Opus 5.5 * refactor(chi-siamo): reuse the section title and drop narrating comments The history blocks use SECTION_TITLE like every other section on the page. The #cosa-facciamo anchor lost its only link in this PR, so it goes too. * feat(struttura): unify the organisation chart and align /about with Figma - One OrganizationDiagram shared by /about and /storia instead of two copies. Nodes follow Figma's "Nodo organigramma" (glass tile, icon above a 22px label, 1px connectors); no CardIcon className overrides. - Mobile chart follows Figma's vertical "Sezione organigramma": team leads hang off a rail, Membri Team balances the admin column. - Soci box gets Figma's "Ruolo trasversale" tag and shares the chart's column width. - Board portraits no longer lift on hover (they are not links); the mobile carousel bleeds to the edge so the next portrait peeks in. - "Come funziona" and "Trova il tuo posto": plain text-primary titles as in Figma, no icon on the definitions. - Same container as /storia and /chi-siamo; the closing title uses the grey gradient (style guide: decorated background or coloured text). Co-Authored-By: Claude Opus 5.5 * fix(structure): connect the mobile Team growth branch and share the portrait card - Mobile chart: the stem under Team growth now turns left and meets the rail the team leads hang from; before it stopped in mid-air. - Node takes a `fill` flag instead of six call sites cancelling its min-width and padding. - The chart's accessible label lists the teams from teamLeads. - BoardMemberCard replaces the two copies of the Direttivo portrait in /about and /storia. - Comments that narrated Figma or the change are removed; the xl overlap on /storia gets one explaining why. * fix(struttura): blue closing statement, statute heading on one line * refactor(layout): share the page container and section title classes /storia, /chi-siamo and /about each defined the same content column and section title classes. They now come from components/layout/page. The two TextOnlyLayout sections in /storia keep their md:px-10 override explicitly; layouts are unchanged. Co-Authored-By: Claude Opus 5.5 * feat(team): calmer team sections with non-interactive specialisms - Specialisms are a plain icon + label list on hairlines instead of button-like pills: they are not links, and which of Figma's four tile variants becomes standard is still open. - Figma's alternating composition: text left, right for HR, and title and text side by side with the specialisms underneath for Events. - More space between teams on desktop, shared container and title styles, descriptions at the section-lead size, left-aligned on every width. - The closing call to action sits on the background instead of a panel with its own gradient. Co-Authored-By: Claude Opus 5.5 * refactor(layout): use the shared tokens everywhere and share the closing CTA - Every hand-written copy of SECTION_TITLE and SECTION_LEAD now uses the token, so all section titles change size at the same breakpoint (/about's closing title switched at md instead of sm). - The homepage sections use CONTAINER like every other page, so their edges line up with the footer and the rest of the site. - ClosingCta replaces the two copies of the closing call to action on /about and /team. - TextOnlyLayout drops its px-6 md:px-36 padding, which every caller cancelled; /storia no longer needs md:px-10 to undo it. - Comments that narrated Figma or the change are removed. * fix(team): lay out Events & Partnership like the other teams * fix(team): alternate the team sections left and right by position --------- Co-authored-by: Claude Opus 5.5 Co-authored-by: Bianca Roberta Ianosel --- src/app/about/page.tsx | 37 ++- src/app/chi-siamo/page.tsx | 2 +- src/app/projects/[id]/page.tsx | 22 +- src/app/storia/page.tsx | 91 ++---- src/app/team/page.tsx | 29 +- src/components/chi-siamo/answer-section.tsx | 11 +- src/components/chi-siamo/history-section.tsx | 22 +- src/components/chi-siamo/numbers-section.tsx | 38 +-- src/components/chi-siamo/path-section.tsx | 9 +- src/components/chi-siamo/problem-section.tsx | 28 +- src/components/chi-siamo/support-section.tsx | 44 ++- src/components/chi-siamo/vision-section.tsx | 22 +- src/components/home/materials.tsx | 9 +- src/components/home/projects.tsx | 9 +- src/components/join/admin.tsx | 4 +- src/components/join/introduction.tsx | 4 +- src/components/join/membership.tsx | 3 +- src/components/join/teams.tsx | 4 +- src/components/layout/TextOnlySection.tsx | 2 +- src/components/layout/closing-cta.tsx | 14 + src/components/layout/page.ts | 8 + src/components/storia/boards.tsx | 39 +-- src/components/storia/constants.ts | 10 + src/components/storia/continuation.tsx | 90 +++--- src/components/storia/hero.tsx | 88 ++---- src/components/storia/organization.tsx | 188 +---------- src/components/storia/projects.tsx | 44 ++- src/components/storia/thanks.tsx | 25 +- src/components/storia/timeline.tsx | 291 ++++++++++-------- .../structure/association-bodies.tsx | 8 +- src/components/structure/board-gallery.tsx | 34 +- .../structure/board-member-card.tsx | 34 ++ src/components/structure/constants.ts | 17 +- src/components/structure/legal-status.tsx | 8 +- .../structure/organization-chart.tsx | 271 ++++++++-------- src/components/structure/participation.tsx | 5 +- src/components/team/constants.ts | 4 +- src/components/team/team-section.tsx | 85 +++-- 38 files changed, 728 insertions(+), 925 deletions(-) create mode 100644 src/components/layout/closing-cta.tsx create mode 100644 src/components/layout/page.ts create mode 100644 src/components/structure/board-member-card.tsx diff --git a/src/app/about/page.tsx b/src/app/about/page.tsx index 8cab8cbf..a2851a4e 100644 --- a/src/app/about/page.tsx +++ b/src/app/about/page.tsx @@ -1,6 +1,8 @@ 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, 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" @@ -15,14 +17,13 @@ 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 = "mx-auto flex w-full max-w-330 flex-col gap-10 px-6 md:gap-14" -const titleClass = - "typo-headline-medium sm:typo-display-medium text-balance bg-linear-to-b from-text-primary to-text-secondary bg-clip-text text-center text-transparent" +const containerClass = `${CONTAINER} flex flex-col gap-10 md:gap-14` +const titleClass = `${SECTION_TITLE} text-center` export default function StructurePage() { return (
-
+
@@ -49,21 +50,19 @@ export default function StructurePage() {

-
-

- Tutto ciò che c’è da sapere su PoliNetwork. -

-

- Scopri chi siamo, cosa facciamo e come entrare a far parte della nostra realtà. -

- -
-
+ + + Unisciti a noi + + + + } + /> +
diff --git a/src/app/chi-siamo/page.tsx b/src/app/chi-siamo/page.tsx index 0fdae581..d96a6022 100644 --- a/src/app/chi-siamo/page.tsx +++ b/src/app/chi-siamo/page.tsx @@ -14,7 +14,7 @@ export const metadata: Metadata = { export default function ChiSiamoPage() { return ( -
+
diff --git a/src/app/projects/[id]/page.tsx b/src/app/projects/[id]/page.tsx index 3e023664..8d1c623e 100644 --- a/src/app/projects/[id]/page.tsx +++ b/src/app/projects/[id]/page.tsx @@ -5,6 +5,7 @@ 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 { getProjectResources, resourceDetails } from "@/components/projects/resources" import { Button } from "@/components/ui/button" import { Card, CardContent, CardTitle } from "@/components/ui/card" @@ -66,12 +67,8 @@ export default async function ProjectPage({ params }: Props) { {project.category in categoryLabels && (

{categoryLabels[project.category]}

)} -

- {project.title} -

-

- {project.descriptionIt} -

+

{project.title}

+

{project.descriptionIt}

@@ -82,13 +79,10 @@ export default async function ProjectPage({ params }: Props) { className="mx-auto flex w-full max-w-330 flex-col gap-10 px-6 md:gap-14" >
-

+

Come usarlo

-

+

{resources.length > 1 ? "Scegli la versione più adatta a te." : "Apri il progetto e segui le istruzioni per iniziare."} @@ -143,10 +137,8 @@ export default async function ProjectPage({ params }: Props) {

-

- Scopri gli altri progetti -

-

+

Scopri gli altri progetti

+

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

-
-
+ } + /> ) } diff --git a/src/components/chi-siamo/answer-section.tsx b/src/components/chi-siamo/answer-section.tsx index 0e718fc6..45ffdb98 100644 --- a/src/components/chi-siamo/answer-section.tsx +++ b/src/components/chi-siamo/answer-section.tsx @@ -1,5 +1,6 @@ import { FiAward, FiBookOpen, FiUsers } from "react-icons/fi" import { CardIcon } from "@/components/card-icon" +import { CONTAINER, SECTION_LEAD, SECTION_TITLE } from "@/components/layout/page" const cards = [ { @@ -23,12 +24,10 @@ const cards = [ export function AnswerSection() { return ( -
+
-

- La nostra risposta -

-

+

La nostra risposta

+

Costruiamo e manteniamo la rete di comunicazione studentesca del Politecnico di Milano.

@@ -40,7 +39,7 @@ export function AnswerSection() { {...card} size="compact" align="start" - className="h-full p-0 [&_h3]:text-balance" + className="h-full p-0 [&_h3]:text-balance [&_p]:max-w-none" /> ))}
diff --git a/src/components/chi-siamo/history-section.tsx b/src/components/chi-siamo/history-section.tsx index 4a2fbf46..0ccd9923 100644 --- a/src/components/chi-siamo/history-section.tsx +++ b/src/components/chi-siamo/history-section.tsx @@ -1,20 +1,17 @@ import Link from "next/link" -import { FiArrowUpRight, FiClock, FiUsers } from "react-icons/fi" -import { Glass } from "@/components/glass" -import { GradientIcon } from "@/components/gradient-icon" +import { FiArrowUpRight } from "react-icons/fi" +import { CONTAINER, SECTION_TITLE } from "@/components/layout/page" import { Button } from "@/components/ui/button" const blocks = [ { title: "Da dove veniamo", description: "Dal primo gruppo Telegram, nel 2016, a una rete costruita dagli studenti, per gli studenti.", - icon: FiClock, cta: "La nostra storia", href: "/storia", }, { title: "Come siamo organizzati", - icon: FiUsers, description: "Direttivo, team operativi, admin e soci. Guarda come funziona la macchina dall'interno.", cta: "La nostra struttura", href: "/about", @@ -23,21 +20,18 @@ const blocks = [ export function HistorySection() { return ( -
+
{blocks.map((block) => ( - - -

- {block.title} -

-

{block.description}

+
+

{block.title}

+

{block.description}

- +
))}
) diff --git a/src/components/chi-siamo/numbers-section.tsx b/src/components/chi-siamo/numbers-section.tsx index 947591b0..4baa82a4 100644 --- a/src/components/chi-siamo/numbers-section.tsx +++ b/src/components/chi-siamo/numbers-section.tsx @@ -1,6 +1,7 @@ import { FiBook, FiBookOpen, FiHeadphones, FiSend, FiUser, FiUsers } from "react-icons/fi" import { GradientIcon } from "@/components/gradient-icon" -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { CONTAINER, SECTION_LEAD, SECTION_TITLE } from "@/components/layout/page" +import { Card } from "@/components/ui/card" const stats = [ { value: "500+", label: "gruppi Telegram e WhatsApp attivi", icon: FiSend }, @@ -13,35 +14,30 @@ const stats = [ export function NumbersSection() { return ( -
+
-

- I numeri -

-

+

I numeri

+

Un network in continua crescita, costruito dagli studenti, per gli studenti.

-
+
    {stats.map((stat) => ( - - - - {stat.value} - - - - {stat.label} - +
  • + + +
    +

    {stat.value}

    +

    {stat.label}

    +
    +
    +
  • ))} -
+ -

+

Dal 2016 al fianco degli studenti del PoliMi

diff --git a/src/components/chi-siamo/path-section.tsx b/src/components/chi-siamo/path-section.tsx index 2b835347..263f4880 100644 --- a/src/components/chi-siamo/path-section.tsx +++ b/src/components/chi-siamo/path-section.tsx @@ -1,6 +1,7 @@ import { FiBook, FiClipboard, FiFileText, FiGift, FiUser, FiUserCheck } from "react-icons/fi" import { Glass } from "@/components/glass" import { GradientIcon } from "@/components/gradient-icon" +import { CONTAINER, SECTION_LEAD, SECTION_TITLE } from "@/components/layout/page" import { cn } from "@/lib/utils" const steps = [ @@ -18,10 +19,8 @@ const steps = [ export function PathSection() { return ( -
-

- Il tuo percorso, sempre al tuo fianco -

+
+

Il tuo percorso, sempre al tuo fianco

    {steps.map((step, index) => ( @@ -46,7 +45,7 @@ export function PathSection() { ))}
-

+

Dalla prima simulazione del TOL fino alla laurea, ogni studente ha sempre{" "} una risposta a portata di mano e{" "} qualcuno a cui chiederla. diff --git a/src/components/chi-siamo/problem-section.tsx b/src/components/chi-siamo/problem-section.tsx index d2bb57df..f1cd07e8 100644 --- a/src/components/chi-siamo/problem-section.tsx +++ b/src/components/chi-siamo/problem-section.tsx @@ -1,5 +1,6 @@ import { FiEdit, FiFileText, FiUsers } from "react-icons/fi" -import { Card, CardAction, CardContent } from "@/components/ui/card" +import { GradientIcon } from "@/components/gradient-icon" +import { CONTAINER, SECTION_LEAD, SECTION_TITLE } from "@/components/layout/page" const questions = [ { text: "Dove trovo il gruppo del mio corso?", icon: FiUsers }, @@ -9,23 +10,22 @@ const questions = [ export function ProblemSection() { return ( -

+
-

- Il problema -

-

- Il PoliMi è complesso. Le informazioni non dovrebbero esserlo. -

+

Il problema

+

Il PoliMi è complesso. Le informazioni non dovrebbero esserlo.

-
+
    {questions.map((question) => ( - - {question.text} - - +
  • + + {question.text} +
  • ))} -
+
) } diff --git a/src/components/chi-siamo/support-section.tsx b/src/components/chi-siamo/support-section.tsx index 8f3493ca..506a90fd 100644 --- a/src/components/chi-siamo/support-section.tsx +++ b/src/components/chi-siamo/support-section.tsx @@ -1,31 +1,25 @@ -import { FiHeart } from "react-icons/fi" -import { Glass } from "@/components/glass" -import { GradientIcon } from "@/components/gradient-icon" +import { CONTAINER, SECTION_TITLE } from "@/components/layout/page" export function SupportSection() { return ( -
- -
- -

Sostieni PoliNetwork

-

- Se vuoi supportare PoliNetwork economicamente, puoi destinarci il 5x1000 nella dichiarazione dei redditi. Si - tratta di una quota IRPEF che verseresti comunque, e che puoi scegliere di indirizzare a noi senza alcun - costo aggiuntivo. -

-
- -

5x1000

-

Il tuo sostegno può fare la differenza

-

- Nella sezione dedicata agli Enti del Terzo Settore, indica il nostro codice fiscale: -

-

- 97927490157 -

-
-
+
+
+

Sostieni PoliNetwork

+

+ Se vuoi supportare PoliNetwork economicamente, puoi destinarci il 5x1000 nella dichiarazione dei redditi. Si + tratta di una quota IRPEF che verseresti comunque, e che puoi scegliere di indirizzare a noi senza alcun costo + aggiuntivo. +

+
+
+

+ Il tuo 5x1000 può fare la differenza +

+

+ Nella sezione dedicata agli Enti del Terzo Settore, indica il nostro codice fiscale: +

+

97927490157

+
) } diff --git a/src/components/chi-siamo/vision-section.tsx b/src/components/chi-siamo/vision-section.tsx index ec6ba6d9..b95392ff 100644 --- a/src/components/chi-siamo/vision-section.tsx +++ b/src/components/chi-siamo/vision-section.tsx @@ -1,29 +1,17 @@ -import Link from "next/link" -import { FiArrowDown, FiArrowUpRight } from "react-icons/fi" -import { Button } from "@/components/ui/button" +import { CONTAINER, SECTION_LEAD } from "@/components/layout/page" +import { cn } from "@/lib/utils" export function VisionSection() { return ( -
+
+

La nostra visione

Un Politecnico in cui nessuno studente resti indietro per mancanza di{" "} informazioni.

-

+

Rendiamo le informazioni sul PoliMi più accessibili, comprensibili e facili da trovare.

-
- - -
) } diff --git a/src/components/home/materials.tsx b/src/components/home/materials.tsx index 199c96f5..fff6659b 100644 --- a/src/components/home/materials.tsx +++ b/src/components/home/materials.tsx @@ -2,7 +2,9 @@ import Link from "next/link" import { Fragment } from "react" import { FiArrowUpRight, FiBook, FiBookOpen, FiClipboard, FiFileText, FiUploadCloud } from "react-icons/fi" import { CardIcon } from "@/components/card-icon" +import { CONTAINER } from "@/components/layout/page" import { Button } from "@/components/ui/button" +import { cn } from "@/lib/utils" import { CardCaption } from "../card-caption" const MATERIALS_URL = "https://polinetwork.org/it/materials/view/" @@ -33,7 +35,12 @@ const archiveTypes = [ export function Materials() { return ( -
+

Materials

diff --git a/src/components/home/projects.tsx b/src/components/home/projects.tsx index 4d67233f..d0078106 100644 --- a/src/components/home/projects.tsx +++ b/src/components/home/projects.tsx @@ -1,7 +1,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 { Button } from "@/components/ui/button" +import { cn } from "@/lib/utils" import { Carousel, CarouselContent, CarouselDots, CarouselItem } from "../ui/carousel" const featuredCards = [ @@ -47,7 +49,12 @@ function ProjectCard({ className, ...card }: (typeof featuredCards)[number] & { export function Projects() { return ( -
+

15+ Projects

diff --git a/src/components/join/admin.tsx b/src/components/join/admin.tsx index a41f66c4..cf27b697 100644 --- a/src/components/join/admin.tsx +++ b/src/components/join/admin.tsx @@ -3,6 +3,8 @@ import connectorFirst from "@/assets/icons/join/connector-first.svg" import connectorSecond from "@/assets/icons/join/connector-second.svg" import { InfoCard } from "@/components/collabora/info-card" import { Highlight, SectionTitle } from "@/components/collabora/section" +import { SECTION_LEAD } from "@/components/layout/page" +import { cn } from "@/lib/utils" import { JoinCards } from "./cards" import { adminCards, adminSteps } from "./constants" @@ -14,7 +16,7 @@ export function AdminSection() {
Diventa Admin -
+

Un admin è la presenza di PoliNetwork nei gruppi ogni giorno.

Non servono competenze specifiche ma solamente{" "} diff --git a/src/components/join/introduction.tsx b/src/components/join/introduction.tsx index c464bcec..e49057a4 100644 --- a/src/components/join/introduction.tsx +++ b/src/components/join/introduction.tsx @@ -3,6 +3,8 @@ import { FiMessageCircle, FiUserCheck, FiUsers } from "react-icons/fi" import networkLogo from "@/assets/images/join/network-logo.png" import { CardIcon } from "@/components/card-icon" import { Highlight, SectionTitle } from "@/components/collabora/section" +import { SECTION_LEAD } from "@/components/layout/page" +import { cn } from "@/lib/utils" const paths = [ { title: "Diventa Admin", icon: FiMessageCircle, href: "#admin" }, @@ -15,7 +17,7 @@ export function Introduction() {

Cosa significa far parte di PoliNetwork -
+

PoliNetwork è gestita interamente da studenti del Politecnico di Milano.

diff --git a/src/components/join/membership.tsx b/src/components/join/membership.tsx index 0d7a82ee..ecec0b21 100644 --- a/src/components/join/membership.tsx +++ b/src/components/join/membership.tsx @@ -2,6 +2,7 @@ import Image from "next/image" import externalLink from "@/assets/icons/join/external-link.svg" import membersPhoto from "@/assets/images/join/members.webp" import { Highlight, SectionTitle } from "@/components/collabora/section" +import { SECTION_LEAD } from "@/components/layout/page" import { PhotoShapes } from "@/components/shapes" import { cn } from "@/lib/utils" import { membershipSteps } from "./constants" @@ -17,7 +18,7 @@ export function MembershipSection() { Diventa Socio Diventa Socio di PoliNetwork -
+

Il tesseramento è il modo per fare parte di PoliNetwork in modo formale, con{" "} diritto di voto nelle Assemblee dei Soci, di candidarsi alle elezioni del direttivo e{" "} diff --git a/src/components/join/teams.tsx b/src/components/join/teams.tsx index 82dbb24f..964b9659 100644 --- a/src/components/join/teams.tsx +++ b/src/components/join/teams.tsx @@ -1,7 +1,9 @@ import Link from "next/link" import { FiArrowUpRight } from "react-icons/fi" import { Highlight, SectionTitle } from "@/components/collabora/section" +import { SECTION_LEAD } from "@/components/layout/page" import { Button } from "@/components/ui/button" +import { cn } from "@/lib/utils" import { JoinCards } from "./cards" import { teamCards } from "./constants" @@ -13,7 +15,7 @@ export function TeamsSection() { >

Entra in un Team -
+

Entrare in un team significa avere un ruolo operativo con deliverable reali, non essere osservatori di un progetto altrui. diff --git a/src/components/layout/TextOnlySection.tsx b/src/components/layout/TextOnlySection.tsx index da6cbbd8..0cd50606 100644 --- a/src/components/layout/TextOnlySection.tsx +++ b/src/components/layout/TextOnlySection.tsx @@ -60,7 +60,7 @@ export default function TextOnlyLayout({ return (

diff --git a/src/components/layout/closing-cta.tsx b/src/components/layout/closing-cta.tsx new file mode 100644 index 00000000..34ca9cbe --- /dev/null +++ b/src/components/layout/closing-cta.tsx @@ -0,0 +1,14 @@ +import type { ReactNode } from "react" +import { CONTAINER, SECTION_LEAD, SECTION_TITLE } from "@/components/layout/page" +import { cn } from "@/lib/utils" + +/** The centred call to action that closes a page: title, a short lead and one button. */ +export function ClosingCta({ title, lead, action }: { title: ReactNode; lead: ReactNode; action: ReactNode }) { + return ( +
+

{title}

+

{lead}

+ {action} +
+ ) +} diff --git a/src/components/layout/page.ts b/src/components/layout/page.ts new file mode 100644 index 00000000..1070c10c --- /dev/null +++ b/src/components/layout/page.ts @@ -0,0 +1,8 @@ +/** Content column of the redesigned pages: Figma's ~1272px column with the homepage's 24/40/64px gutters. */ +export const CONTAINER = "mx-auto w-full max-w-350 px-6 sm:px-10 lg:px-16" + +/** Section title in the grey gradient. Closing statements (`ClosingCta`) switch it to blue. */ +export const SECTION_TITLE = + "typo-headline-medium sm:typo-display-medium text-balance bg-linear-to-b from-text-primary to-text-secondary bg-clip-text text-transparent" + +export const SECTION_LEAD = "typo-body-large sm:typo-headline-extrasmall text-pretty" diff --git a/src/components/storia/boards.tsx b/src/components/storia/boards.tsx index 1beca4d5..f3b6bc17 100644 --- a/src/components/storia/boards.tsx +++ b/src/components/storia/boards.tsx @@ -1,21 +1,17 @@ "use client" -import Image from "next/image" -import { Glass } from "@/components/glass" +import { CONTAINER, SECTION_TITLE } from "@/components/layout/page" +import { BoardMemberCard } from "@/components/structure/board-member-card" import TabsNavigation from "@/components/tabs" import { Tabs } from "@/components/tabs/tabs" import { TabsContent } from "@/components/tabs/tabs-content" -import { Card, CardContent } from "@/components/ui/card" -import { cn } from "@/lib/utils" import { boardMembers, boardTerms } from "./constants" export function Boards() { return ( -
+

I nostri direttivi

-

- Ogni mandato, un nuovo capitolo -

+

Ogni mandato, un nuovo capitolo

({ value: term, label: term }))} /> {boardTerms.map((term) => ( @@ -25,39 +21,26 @@ export function Boards() {
{boardMembers.map((member) => ( - - {member.name} -
- -

{member.name}

- {member.role &&

{member.role}

} -
- + member={member} + sizes="(max-width: 639px) 45vw, (max-width: 1279px) 30vw, 224px" + className="sm:w-[calc((100%-48px)/3)] xl:w-[calc((100%-72px)/4)] min-[360px]:w-[calc((100%-24px)/2)]" + /> ))}

Il Direttivo viene eletto ogni due anni dall’Assemblea dei Soci tra i soci dell’associazione.

- +

{term}

Il Direttivo

Si concentra sul rafforzamento della struttura, sull’evoluzione dei servizi e sullo sviluppo di nuovi progetti per la community.

- +
) : (
diff --git a/src/components/storia/constants.ts b/src/components/storia/constants.ts index 78b1325d..dae96aad 100644 --- a/src/components/storia/constants.ts +++ b/src/components/storia/constants.ts @@ -1,3 +1,5 @@ +import { FiUser, FiUserCheck, FiUsers } from "react-icons/fi" +import telegram from "@/assets/icons/telegram-gradient.svg" import ale from "@/assets/images/storia/ale-he.jpg" import alessandro from "@/assets/images/storia/alessandro-gabbini.jpg" import elia from "@/assets/images/storia/elia-falzoni.jpg" @@ -10,19 +12,27 @@ import tommaso from "@/assets/images/storia/tommaso-morganti.jpg" export const milestones = [ { year: "2016", + href: "#capitolo-1", + icon: FiUser, description: "Nasce il primo gruppo per rispondere a una necessità semplice: trovare informazioni e parlare con i compagni di corso.", }, { year: "2017", + href: "#capitolo-1", + icon: telegram.src, description: "I gruppi si moltiplicano su Telegram e WhatsApp e iniziamo a coprire tutti i corsi di laurea.", }, { year: "2022", + href: "#capitolo-5", + icon: FiUserCheck, description: "PoliNetwork diventa un’APS-ETS, con un direttivo eletto e una struttura ancora più democratica.", }, { year: "OGGI", + href: "#direttivi", + icon: FiUsers, description: "Oltre 200 admin volontari e una community che accompagna ogni giorno migliaia di studenti del Politecnico di Milano.", }, diff --git a/src/components/storia/continuation.tsx b/src/components/storia/continuation.tsx index 1b417174..a4cd0d18 100644 --- a/src/components/storia/continuation.tsx +++ b/src/components/storia/continuation.tsx @@ -1,53 +1,57 @@ import Link from "next/link" import { FiArrowUpRight } from "react-icons/fi" -import { Glass } from "@/components/glass" +import { CONTAINER, SECTION_LEAD, SECTION_TITLE } from "@/components/layout/page" import { Button } from "@/components/ui/button" import { cn } from "@/lib/utils" +const years = ["2016", "2022", "2026", "?"] + +// Figma: text on the left, the years on a line on the right. No panel or gradient behind it, and the "?" is a +// quiet open slot rather than a filled pill that looks like a button. export function Continuation() { return ( -
- -
-

- La storia continua. -

-

- Ogni anno qualcuno arriva, qualcuno si laurea e qualcun altro raccoglie il testimone. La missione rimane la - stessa: rendere la vita universitaria più semplice, accessibile e umana. -

- -
-
-
    - {["2016", "2022", "2026", "?"].map((year) => ( -
  1. - - {year} - -
  2. - ))} -
-

Nuove persone, nuove idee, nuovi progetti.

-

- Il prossimo capitolo lo scriviamo insieme. -

-
-
+
+
+

+ La storia continua. +

+

+ Ogni anno qualcuno arriva, qualcuno si laurea e qualcun altro raccoglie il testimone. Nuove persone, nuove + idee, nuovi progetti. La missione rimane la stessa: rendere la vita universitaria più semplice, accessibile e + umana. +

+
+ +
+
    + {years.map((year, index) => ( +
  1. 0 && "flex-1")}> + {index > 0 && } + + {year} + +
  2. + ))} +
+

+ Il prossimo capitolo lo scriviamo insieme. +

+ +
) } diff --git a/src/components/storia/hero.tsx b/src/components/storia/hero.tsx index 6c260137..b13235ee 100644 --- a/src/components/storia/hero.tsx +++ b/src/components/storia/hero.tsx @@ -1,76 +1,30 @@ -import Image from "next/image" import Link from "next/link" -import { FiArrowDown, FiUsers } from "react-icons/fi" -import { Glass } from "@/components/glass" -import { GradientIcon } from "@/components/gradient-icon" +import { FiArrowDown } from "react-icons/fi" +import { CONTAINER } from "@/components/layout/page" import { Button } from "@/components/ui/button" -import { Card, CardContent } from "@/components/ui/card" import { Hero } from "@/components/ui/hero" -import { cn } from "@/lib/utils" -import { boardMembers } from "./constants" - -const portraits = [ - { member: boardMembers[1], position: "top-12 left-0 -rotate-6" }, - { - member: boardMembers[0], - position: "-translate-x-1/2 top-0 left-1/2 z-10", - }, - { member: boardMembers[3], position: "top-12 right-0 rotate-6 sm:top-16" }, -] +// Figma: a centred XL title and nothing else. The board portraits appear further down, under "I nostri direttivi". export function HistoryHero() { return ( -
-
- -
- - -
-
-
-
- {portraits.map(({ member, position }) => ( - - {member.name} -
- - {member.role} - - - ))} -
- - -
-

200+

-

admin volontari, oggi

-
-
+
+ +
+ +
) diff --git a/src/components/storia/organization.tsx b/src/components/storia/organization.tsx index 7780ee4c..969b1fd7 100644 --- a/src/components/storia/organization.tsx +++ b/src/components/storia/organization.tsx @@ -1,187 +1,18 @@ import Link from "next/link" import { FiArrowDown } from "react-icons/fi" -import { - LuBookUser, - LuCodeXml, - LuCrown, - LuGlobe, - LuHandshake, - LuPencilRuler, - LuShield, - LuShieldCheck, - LuUserCog, - LuUsersRound, -} from "react-icons/lu" -import { CardIcon } from "@/components/card-icon" -import { GradientIcon } from "@/components/gradient-icon" +import { CONTAINER, SECTION_LEAD } from "@/components/layout/page" +import { OrganizationDiagram } from "@/components/structure/organization-chart" import { Button } from "@/components/ui/button" -import { Card, CardTitle } from "@/components/ui/card" import { cn } from "@/lib/utils" -const teamLeads = [ - { title: "Team lead HR", icon: LuBookUser }, - { title: "Team lead IT", icon: LuCodeXml }, - { title: "Team lead Design & Marketing", icon: LuPencilRuler }, - { title: "Team lead International", icon: LuGlobe }, - { title: "Team lead Eventi & Partnership", icon: LuHandshake }, -] - -const nodeClassName = "[&_h3]:text-text-primary" -const mainNodeClassName = "[&_h3]:typo-title-large w-fit px-4 [&_h3]:whitespace-nowrap" - -type BranchPosition = "first" | "middle" | "last" | "only" - -/** Horizontal rail and arrow above one child of a branch: the rail runs from the first child's center to the last's. */ -function BranchArrow({ position }: { position: BranchPosition }) { - return ( -
- {position !== "only" && ( - - )} - -
- ) -} - -function branchPosition(index: number, count: number): BranchPosition { - if (count === 1) return "only" - if (index === 0) return "first" - return index === count - 1 ? "last" : "middle" -} - -function Stem() { - return -} - -function DesktopChart() { - return ( -
- - -
- {/* Rail from the center of the admin column (1/12) to the center of the Team growth columns (7/12) */} -
- - - -
-
- - - -
-
- - -
    - {teamLeads.map((lead, index) => ( -
  • - -
    - -
    - -
  • - ))} -
-
- - - - Membri dei team - - -
-
-
-
- ) -} - -function MobileChart() { - return ( -
- - -
-
- - - -
-
- - - -
-
- - -
    - {teamLeads.map((lead) => ( -
  • - - - - {lead.title} - - -
  • - ))} -
- - -
-
-
- ) -} - export function Organization() { return ( -
+

Come è organizzata PoliNetwork

-

+

Direttivo, team operativi, admin e soci. Guarda come funziona la macchina dall’interno.

- + {/* From xl the diagram rises beside the intro: its narrow top (the Direttivo node) fits next to the text */} +
) } diff --git a/src/components/storia/projects.tsx b/src/components/storia/projects.tsx index ee42bc09..aae29a9e 100644 --- a/src/components/storia/projects.tsx +++ b/src/components/storia/projects.tsx @@ -4,9 +4,10 @@ import { FiAirplay, FiArrowUpRight, FiBarChart, FiBook, FiCheck, FiHeadphones } import telegram from "@/assets/icons/telegram-gradient.svg" import projectsImage from "@/assets/images/storia/progetti.png" import { CardIcon } from "@/components/card-icon" -import { Glass } from "@/components/glass" +import { CONTAINER, SECTION_LEAD, SECTION_TITLE } from "@/components/layout/page" import TextOnlyLayout from "@/components/layout/TextOnlySection" import { Button } from "@/components/ui/button" +import { cn } from "@/lib/utils" const projects = [ { @@ -35,15 +36,13 @@ const projects = [ export function HistoryProjects() { return ( -
+
-

- Le cose che abbiamo costruito. -

-

+

Le cose che abbiamo costruito.

+

Nel corso degli anni PoliNetwork ha sviluppato e mantenuto un ecosistema di{" "} strumenti gratuiti e open source pensati per rispondere ai bisogni concreti degli studenti. @@ -51,22 +50,23 @@ export function HistoryProjects() { } classNames={{ - section: "gap-12 px-0 md:px-0 xl:flex-row xl:items-center [&>div>h2]:typo-title-large", - textDiv: "min-w-0 xl:w-1/2 xl:items-start", - title: "text-blue-secondary xl:text-start", - descriptionDiv: "xl:text-start", + section: "gap-12 xl:flex-row xl:items-center [&>div>h2]:typo-title-large", + textDiv: "min-w-0 items-start xl:w-1/2", + title: "text-start text-blue-secondary", + descriptionDiv: "text-start", contentDiv: "min-w-0 xl:w-1/2", }} > Il sito PoliNetwork su computer e smartphone -

-
+ {/* Figma: the six tools on the left, the note about where they come from beside them, without a panel */} +
+
{projects.map((project) => ( ))}
- -
-

- Molti dei progetti di PoliNetwork sono nati da idee individuali: studenti che hanno - creato autonomamente uno strumento utile e che, nel tempo, sono entrati a far parte del network. -

-

Da un’idea di uno studente a uno strumento per migliaia di studenti.

-
- -
+
) diff --git a/src/components/storia/thanks.tsx b/src/components/storia/thanks.tsx index faf9b32e..e7ea6bfe 100644 --- a/src/components/storia/thanks.tsx +++ b/src/components/storia/thanks.tsx @@ -2,12 +2,13 @@ import { useState } from "react" import { FiSearch } from "react-icons/fi" -import { Glass } from "@/components/glass" +import { CONTAINER, SECTION_LEAD, SECTION_TITLE } from "@/components/layout/page" import TextOnlyLayout from "@/components/layout/TextOnlySection" import TabsNavigation from "@/components/tabs" import { Tabs } from "@/components/tabs/tabs" import { TabsContent } from "@/components/tabs/tabs-content" import { Input } from "@/components/ui/input" +import { cn } from "@/lib/utils" import { contributors } from "./constants" const categories = ["Tutti", "Direttivi", "Admin", "Collaboratori"] @@ -21,28 +22,24 @@ export function Thanks() { title="Un grazie speciale" description={ <> -

- Centinaia di persone, una sola comunità. -

-

+

Centinaia di persone, una sola comunità.

+

PoliNetwork esiste grazie a tutte le persone che, in modi diversi, hanno dedicato tempo, energie e idee alla sua crescita. Alcune hanno coordinato un corso, altre sviluppato uno strumento, organizzato un evento, moderato un gruppo o semplicemente aiutato qualcuno nel momento giusto.

-

- Questa è la nostra memoria collettiva. -

+

Questa è la nostra memoria collettiva.

} classNames={{ - section: "mx-auto max-w-330 gap-12 px-6 md:px-6 xl:flex-row [&>div>h2]:typo-title-large", - textDiv: "xl:w-1/2 xl:items-start xl:text-start", - title: "text-blue-secondary xl:text-start", - descriptionDiv: "xl:text-start", + section: `${CONTAINER} gap-12 xl:flex-row xl:gap-16 [&>div>h2]:typo-title-large`, + textDiv: "items-start text-start xl:w-1/2", + title: "text-start text-blue-secondary", + descriptionDiv: "text-start", contentDiv: "min-w-0 xl:w-1/2", }} > - +
} @@ -80,7 +77,7 @@ export function Thanks() { ) })} - +
) } diff --git a/src/components/storia/timeline.tsx b/src/components/storia/timeline.tsx index 63b93047..872aeb08 100644 --- a/src/components/storia/timeline.tsx +++ b/src/components/storia/timeline.tsx @@ -1,50 +1,69 @@ +import Image from "next/image" import Link from "next/link" -import { - FiArrowDown, - FiCheckCircle, - FiClipboard, - FiClock, - FiLayers, - FiUser, - FiUserCheck, - FiUsers, -} from "react-icons/fi" +import type { ReactNode } from "react" +import { FiArrowDown, FiArrowRight, FiCheck, FiClipboard, FiClock, FiUser, FiUserCheck, FiUsers } from "react-icons/fi" import telegram from "@/assets/icons/telegram-gradient.svg" -import { CardCaption } from "@/components/card-caption" -import { CardIcon } from "@/components/card-icon" -import { Glass } from "@/components/glass" +import { GradientIcon, type GradientIconType } from "@/components/gradient-icon" +import { CONTAINER, SECTION_LEAD, SECTION_TITLE } from "@/components/layout/page" import { Button } from "@/components/ui/button" +import { cn } from "@/lib/utils" import { chapters, milestones } from "./constants" -const milestoneLinks = ["#capitolo-1", "#capitolo-1", "#capitolo-5", "#direttivi"] - -const milestoneIcons = [FiUser, telegram.src, FiUserCheck, FiUsers] +/** The small icon tile Figma uses for timeline nodes and diagram steps. */ +function IconTile({ icon, className }: { icon: GradientIconType | string; className?: string }) { + return ( + + {typeof icon === "string" ? ( + + ) : ( + + )} + + ) +} export function Timeline() { return ( -
-

+
+

Da un semplice gruppo a una rete costruita dagli studenti,{" "} per gli studenti.

-
    - {milestones.map((milestone, index) => ( -
  1. - + + {/* Nodes on a line: vertical below lg, horizontal from lg (Figma) */} +
      + {milestones.map((milestone) => ( +
    1. + + + {milestone.year} + +

      + {milestone.description} +

    2. ))}
    +
    -

    +

    Dieci anni di crescita, persone e progetti. La nostra storia{" "} continua, insieme a te.

    @@ -58,122 +77,124 @@ export function Timeline() { ) } -const chapterVisuals = [ -
    - {[ - { icon: telegram.src, title: "Il primo gruppo" }, - { icon: FiUser, title: "I primi admin e volontari" }, - { icon: FiUserCheck, title: "Test su Informatica" }, - { icon: FiUsers, title: "Tutti i corsi" }, - ].map((item) => ( - - ))} -
    , -
    - {[ - { icon: FiClipboard, title: "Regole interne" }, - { icon: FiUserCheck, title: "Comunicazioni selezionate" }, - { icon: FiUsers, title: "Una community indipendente" }, - ].map((item) => ( - - ))} -
    , -
    +function Arrow({ className }: { className?: string }) { + return +} + +/** A left-to-right sequence of steps, joined by arrows. */ +function Steps({ items }: { items: { label: string; icon?: GradientIconType | string; value?: string }[] }) { + return ( +
      + {items.map((item, index) => ( +
    1. + {index > 0 && } +
      + {item.icon && } + {item.value && {item.value}} + {item.label} +
      +
    2. + ))} +
    + ) +} + +/** Two columns read as "before → after". */ +function BeforeAfter({ before, after }: { before: { title: string; items: string[] }; after: typeof before }) { + return ( +
    + {[before, after].map((column, index) => ( +
    +

    {column.title}

    +
      + {column.items.map((item) => ( +
    • {item}
    • + ))} +
    +
    + ))} + +
    + ) +} + +const chapterVisuals: ReactNode[] = [ + , + , + , + , + , +
      {[ - { title: "5", caption: "persone", icon: FiUser }, - { title: "~25", caption: "rappresentanti", icon: FiUsers }, - { title: "4", caption: "composizioni", icon: FiLayers }, + { title: "APS-ETS", status: "Riconosciuto dallo Stato", done: true }, + { title: "RUNTS", status: "Iscritta come ETS", done: true }, + { title: "Albo PoliMi", status: "Ancora da raggiungere", done: false }, ].map((item) => ( - +
    • + {item.done ? ( + + ) : ( + + )} + {item.title} + {item.status} +
    • ))} -
    , -
    - - -
    , -
    - - -
    , -
    - - - -
    , + , ] export function Chapters() { return ( -
    -
      +
      +
        {chapters.map((chapter, index) => (
      1. -
        - - {String(index + 1).padStart(2, "0")} - -

        - {chapter.title} -

        -

        - {chapter.subtitle} -

        -

        - {chapter.description} -

        + + {String(index + 1).padStart(2, "0")} + +
        +

        {chapter.title}

        +

        {chapter.subtitle}

        +

        {chapter.description}

        +
        +
        + {chapterVisuals[index]}
        -
        {chapterVisuals[index]}
      2. ))}
      diff --git a/src/components/structure/association-bodies.tsx b/src/components/structure/association-bodies.tsx index 4249de00..a808421e 100644 --- a/src/components/structure/association-bodies.tsx +++ b/src/components/structure/association-bodies.tsx @@ -4,7 +4,6 @@ import { Accordion } from "@/components/accordion-association/accordion" import { AccordionContent } from "@/components/accordion-association/accordion-content" import { AccordionItem } from "@/components/accordion-association/accordion-item" import { AccordionTrigger } from "@/components/accordion-association/accordion-trigger" -import { GradientIcon } from "@/components/gradient-icon" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { associationBodies } from "./constants" @@ -26,9 +25,10 @@ export function AssociationBodies() {
      {associationBodies.map((body) => ( - - - {body.title} + + + {body.title} + {body.description} diff --git a/src/components/structure/board-gallery.tsx b/src/components/structure/board-gallery.tsx index d28bd40a..6193ba05 100644 --- a/src/components/structure/board-gallery.tsx +++ b/src/components/structure/board-gallery.tsx @@ -1,34 +1,22 @@ -import Image from "next/image" import { boardMembers } from "@/components/storia/constants" -import { Card, CardContent } from "@/components/ui/card" +import { BoardMemberCard } from "@/components/structure/board-member-card" import { Carousel, CarouselContent, CarouselDots, CarouselItem } from "@/components/ui/carousel" -import { cn } from "@/lib/utils" export function BoardGallery() { return ( - + {boardMembers.map((member) => ( - - - {member.name} -
      - -

      {member.name}

      - {member.role &&

      {member.role}

      } -
      - + + ))} diff --git a/src/components/structure/board-member-card.tsx b/src/components/structure/board-member-card.tsx new file mode 100644 index 00000000..3d96b274 --- /dev/null +++ b/src/components/structure/board-member-card.tsx @@ -0,0 +1,34 @@ +import Image from "next/image" +import type { boardMembers } from "@/components/storia/constants" +import { Card, CardContent } from "@/components/ui/card" +import { cn } from "@/lib/utils" + +/** A Direttivo member's portrait, with name and role over a gradient. */ +export function BoardMemberCard({ + member, + sizes, + className, +}: { + member: (typeof boardMembers)[number] + sizes: string + className?: string +}) { + return ( + + {member.name} +
      + +

      {member.name}

      + {member.role &&

      {member.role}

      } +
      + + ) +} diff --git a/src/components/structure/constants.ts b/src/components/structure/constants.ts index 716ce349..9750ef89 100644 --- a/src/components/structure/constants.ts +++ b/src/components/structure/constants.ts @@ -1,42 +1,27 @@ import { FiBriefcase, FiCompass, FiFlag, FiUsers } from "react-icons/fi" -import { - LuBookUser, - LuCodeXml, - LuCrown, - LuGlobe, - LuHandshake, - LuPencilRuler, - LuScale, - LuShield, - LuUserCog, - LuUsersRound, -} from "react-icons/lu" +import { LuBookUser, LuCodeXml, LuGlobe, LuHandshake, LuPencilRuler, LuShield, LuUsersRound } from "react-icons/lu" export const associationBodies = [ { id: "assemblea", - icon: FiUsers, title: "Assemblea dei Soci", description: "È l’organo sovrano di PoliNetwork. Si riunisce almeno una volta l’anno per approvare il bilancio consuntivo e, ogni due anni, per eleggere il Consiglio Direttivo e il Collegio dei Probiviri. Approva eventuali modifiche allo Statuto. Ogni socio ha diritto di voto e può candidarsi direttamente a entrambi gli organi.", }, { id: "consiglio", - icon: LuCrown, title: "Consiglio Direttivo", description: "È l’organo esecutivo e di garanzia dell’associazione. Vigila sulle attività di PoliNetwork per preservare i valori dello Statuto, gestisce le finanze e i rapporti istituzionali con partner esterni, emana i regolamenti interni. Approva o respinge le linee d’azione proposte dai singoli team e le strategie delineate dal Team Growth, senza gestire direttamente le attività quotidiane dei team.", }, { id: "probiviri", - icon: LuScale, title: "Collegio dei Probiviri", description: "È l’organo di garanzia interna, indipendente dal Consiglio Direttivo. Interviene in caso di controversie tra membri o organi dell’associazione, contestazioni sulle elezioni, e vigila sul rispetto dello Statuto e dei regolamenti quando è in gioco l’imparzialità del Direttivo stesso. Viene eletto dall’Assemblea dei Soci con lo stesso mandato biennale del Direttivo, in modo indipendente da esso.", }, { id: "growth", - icon: LuUserCog, title: "Team Growth", description: "È il livello di coordinamento tra i cinque team operativi: riunisce i Capi Dipartimento e alcuni membri del Direttivo per allineare le priorità comuni, le nomine, il recruiting e le decisioni che coinvolgono più team contemporaneamente. Non è un team a sé e non si aprono candidature dirette: si entra in Team Growth diventando Capo Dipartimento di uno dei cinque team.", diff --git a/src/components/structure/legal-status.tsx b/src/components/structure/legal-status.tsx index 02df69a9..5987707f 100644 --- a/src/components/structure/legal-status.tsx +++ b/src/components/structure/legal-status.tsx @@ -1,15 +1,15 @@ import { FiArrowUpRight } from "react-icons/fi" import { Glass } from "@/components/glass" import { GradientIcon } from "@/components/gradient-icon" +import { SECTION_TITLE } from "@/components/layout/page" import { Button } from "@/components/ui/button" +import { cn } from "@/lib/utils" import { legalFacts, regulationsHref, statuteHref } from "./constants" export function LegalStatus() { return ( -

      - Forma giuridica -

      +

      Forma giuridica

      {legalFacts.map((fact) => (
      @@ -24,7 +24,7 @@ export function LegalStatus() { ))}
      -

      +

      Consulta Statuto e Regolamenti

      diff --git a/src/components/structure/organization-chart.tsx b/src/components/structure/organization-chart.tsx index 917b6a05..6a26b8af 100644 --- a/src/components/structure/organization-chart.tsx +++ b/src/components/structure/organization-chart.tsx @@ -1,42 +1,64 @@ -import Image from "next/image" -import { FiArrowDown, FiUsers } from "react-icons/fi" +import type { IconType } from "react-icons" +import { FiArrowDown, FiArrowLeft, FiUsers } from "react-icons/fi" import { LuCrown, LuShield, LuShieldCheck, LuUserCog, LuUsersRound } from "react-icons/lu" -import arrow from "@/assets/icons/structure/arrow.svg" -import branch from "@/assets/icons/structure/branch.svg" -import { CardIcon } from "@/components/card-icon" import { Glass } from "@/components/glass" import { GradientIcon } from "@/components/gradient-icon" -import { Card, CardTitle } from "@/components/ui/card" +import { CONTAINER } from "@/components/layout/page" import { cn } from "@/lib/utils" import { teamLeads } from "./constants" -function DownArrow() { +const teamNames = new Intl.ListFormat("it").format(teamLeads.map((lead) => lead.title.replace("Team lead ", ""))) +const CHART_LABEL = `Il Direttivo coordina i Capi admin e il Team growth. I Capi admin coordinano gli Admin. Il Team growth coordina i team lead ${teamNames}, con i rispettivi membri.` + +// Nodes are not links, so they have no hover state +const tileClassName = "rounded-rectangles border-white/50 bg-background-blur text-text-primary" + +/** `fill` stretches the node to its column instead of sizing it by its label. */ +function Node({ icon, label, fill = false }: { icon: IconType; label: string; fill?: boolean }) { return ( -
      - -
      + + + {label} + ) } -const nodeClassName = "[&_h3]:text-text-primary" -// The admin column is one sixth of the chart: a smaller title and padding keep "Capi admin" on one line -const adminNodeClassName = "[&_h3]:typo-headline-small [&>*]:px-4 [&_h3]:whitespace-nowrap" +function LeadNode({ icon, label }: { icon: IconType; label: string }) { + return ( + + + {label} + + ) +} + +function Arrow({ className }: { className?: string }) { + return +} /** Horizontal rail and arrow above one child of a branch: the rail runs from the first child's center to the last's. */ function BranchArrow({ position }: { position: "first" | "middle" | "last" | "only" }) { return ( -
      +
      {position !== "only" && ( )} - +
      ) } @@ -48,58 +70,45 @@ function branchPosition(index: number, count: number) { } function Stem() { - return + return } function DesktopChart() { return ( -
      - +
      + -
      - {/* Rail from the center of the admin column (1/12) to the center of the Team Growth columns (7/12) */} -
      - - - +
      + {/* Rail from the center of the admin column (1/12) to the center of the Team growth columns (7/12) */} +
      + + +
      -
      -
      - - - -
      +
      + + +
      -
      - +
      + -
        +
          {teamLeads.map((lead, index) => (
        • - +
        • ))}
        -
        - +
        + - - Membri Team - - + Membri Team +
      @@ -107,92 +116,94 @@ function DesktopChart() { ) } -export function OrganizationChart() { +/** Figma mobile "Sezione organigramma": two columns, team leads hanging off a vertical rail, Membri Team balancing the admin column. */ +function MobileChart() { return ( -
      - -
      - -
      -
      - -
      -
      - -
      +
      + + +
      +
      + + +
      -
      -
      - - - +
      + + + +
      + + {/* Arrow from the team leads' rail, across the column gap */} + + + +
      -
      - - -
        - {teamLeads.map((lead) => ( -
      • - - - - {lead.title} - - -
      • - ))} -
      - - +
      +
      + + {/* Down from the node, then left to the rail the team leads hang from */} +
      + +
      +
        + {teamLeads.map((lead) => ( +
      • + + + + {lead.title} + +
      • + ))} +
      - -
      - -

      Soci

      -
      -
      -

      Soci

      -

      - Essere socio è una condizione trasversale, compatibile con qualsiasi ruolo all’interno di PoliNetwork. Ogni - persona può ricoprire più ruoli contemporaneamente. -

      -
      -
      +
      + ) +} + +/** The organisation diagram, shared by /about and /storia. */ +export function OrganizationDiagram({ id, className }: { id?: string; className?: string }) { + return ( + + ) +} + +export function OrganizationChart() { + return ( +
      +
      + + + +
      +
      +

      + + Soci +

      + + Ruolo trasversale + +
      +

      + Essere socio è una condizione trasversale, compatibile con qualsiasi ruolo all’interno di PoliNetwork. + Ogni persona può ricoprire più ruoli contemporaneamente. +

      +
      +
      +
      ) } diff --git a/src/components/structure/participation.tsx b/src/components/structure/participation.tsx index e5331189..c8648b21 100644 --- a/src/components/structure/participation.tsx +++ b/src/components/structure/participation.tsx @@ -12,7 +12,10 @@ export function Participation() { - + {path.title} diff --git a/src/components/team/constants.ts b/src/components/team/constants.ts index 29f2622c..383c6ed5 100644 --- a/src/components/team/constants.ts +++ b/src/components/team/constants.ts @@ -21,10 +21,9 @@ export type Team = { title: string description: string roles: Pick[] - reverse?: boolean + /** Figma alternates the sections: text on the right for HR, title and text side by side for Events. */ } -// Copy and specialisms from the desktop Team frame (5546:10874). export const teams: Team[] = [ { id: "it", @@ -62,7 +61,6 @@ export const teams: Team[] = [ { title: "Community & Engagement", icon: FiAward }, { title: "Crescita del team", icon: FiCornerRightUp }, ], - reverse: true, }, { id: "events-partnership", diff --git a/src/components/team/team-section.tsx b/src/components/team/team-section.tsx index 3ccb9421..ff200180 100644 --- a/src/components/team/team-section.tsx +++ b/src/components/team/team-section.tsx @@ -1,49 +1,66 @@ +import Image from "next/image" import { FiUsers } from "react-icons/fi" -import { CardIcon } from "@/components/card-icon" +import { GradientIcon } from "@/components/gradient-icon" +import { CONTAINER, SECTION_LEAD, SECTION_TITLE } from "@/components/layout/page" import { Button } from "@/components/ui/button" import { cn } from "@/lib/utils" import { applicationHref, type Team } from "./constants" -export function TeamSection({ team }: { team: Team }) { +/** The specialisms are not links: a plain list, not tiles. */ +function RoleList({ team }: { team: Team }) { + return ( +
        + {team.roles.map(({ title, icon }) => ( +
      • + {typeof icon === "string" ? ( + + ) : ( + + )} + {title} +
      • + ))} +
      + ) +} + +function JoinButton({ team }: { team: Team }) { + return ( + + ) +} + +/** `reverse` puts the specialisms on the left: the page alternates sides from one team to the next. */ +export function TeamSection({ team, reverse = false }: { team: Team; reverse?: boolean }) { + const title = ( +

      + {team.title} +

      + ) + const description =

      {team.description}

      + return (
      -
      -
      -

      - {team.title} -

      -

      {team.description}

      - +
      + {title} + {description} +
      +
      -
        - {team.roles.map((role) => ( -
      • - -
      • - ))} -
      +
      ) } From 5967edceb6f8c373f533da3765103668693cc343 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriele=20Vigan=C3=B2?= Date: Mon, 2 Nov 2026 16:15:00 +0100 Subject: [PATCH 07/22] feat(join): restore Figma's rhythm and drop the extra path cards on /join (#204) * feat(join): restore Figma's rhythm and drop the extra path cards on /join - Same container, section titles and copy style as the other refined pages; copy is left-aligned at every width instead of centred on mobile. - The three "ways in" cards (not in Figma) are gone: the intro paragraph that already lists them now links to their sections. - Section spacing grows on large screens (224px from xl, 288px from 2xl), so the 1728px title positions sit within ~6% of Figma instead of -18%. - Team cards link to their team on /team and are the only cards that react to hover; InfoCard gains an optional href for this. - The members photo uses object-cover anchored to the bottom instead of Figma's percentage offsets, so the crop survives a different photo or box. - Mobile: the admin-task carousel runs to the screen edges so the next card peeks in; membership step titles are 20px and their notes 14px. Co-Authored-By: Claude Opus 5.5 * refactor(join): one inline-link style with a visible focus ring INLINE_LINK replaces the SectionLink component, whose focus state was only a thicker underline. The membership step links get the same ring. --------- Co-authored-by: Claude Opus 5.5 Co-authored-by: Bianca Roberta Ianosel --- src/app/join/page.tsx | 5 +-- src/components/collabora/info-card.tsx | 24 ++++++++++--- src/components/join/admin.tsx | 14 ++++---- src/components/join/cards.tsx | 5 +-- src/components/join/constants.ts | 4 +++ src/components/join/introduction.tsx | 47 ++++++++++++-------------- src/components/join/membership.tsx | 29 ++++++++-------- src/components/join/teams.tsx | 15 ++++---- src/components/layout/page.ts | 4 +++ 9 files changed, 81 insertions(+), 66 deletions(-) diff --git a/src/app/join/page.tsx b/src/app/join/page.tsx index c22fc5c4..04c84c41 100644 --- a/src/app/join/page.tsx +++ b/src/app/join/page.tsx @@ -3,6 +3,7 @@ import { AdminSection } from "@/components/join/admin" import { Introduction } from "@/components/join/introduction" import { MembershipSection } from "@/components/join/membership" import { TeamsSection } from "@/components/join/teams" +import { CONTAINER } from "@/components/layout/page" import { Hero } from "@/components/ui/hero" export const metadata: Metadata = { @@ -13,8 +14,8 @@ export const metadata: Metadata = { export default function JoinPage() { return ( -
      -
      +
      +
      -
      + a:focus-visible]:ring-2 has-[>a:focus-visible]:ring-blue-secondary", + className + )} + > + {href && } + {href && } + {/* Positioned so the text stays above the hover background */} +
      @@ -44,11 +58,11 @@ export function InfoCard({

{mobileDescription !== undefined && ( -

{mobileDescription}

+

{mobileDescription}

)}

+

-
- Diventa Admin -
+
+

Diventa Admin

+

Un admin è la presenza di PoliNetwork nei gruppi ogni giorno.

Non servono competenze specifiche ma solamente{" "} @@ -33,7 +33,7 @@ export function AdminSection() {

-

+

Ti interessano questi compiti?

    diff --git a/src/components/join/cards.tsx b/src/components/join/cards.tsx index 59c2a953..3e89da3c 100644 --- a/src/components/join/cards.tsx +++ b/src/components/join/cards.tsx @@ -17,9 +17,10 @@ export function JoinCards({ cards, carousel = false }: { cards: InfoCardData[]; return ( - + {cards.map((card) => ( diff --git a/src/components/join/constants.ts b/src/components/join/constants.ts index 469ccd83..c5f262fb 100644 --- a/src/components/join/constants.ts +++ b/src/components/join/constants.ts @@ -60,24 +60,28 @@ export const adminSteps: InfoCardData[] = [ export const teamCards: InfoCardData[] = [ { title: "Sviluppo Tecnico", + href: "/team#it", description: "Realizza prodotti digitali in produzione, usati da migliaia di persone", icon: code, iconClassName, }, { title: "Comunicazione", + href: "/team#design-marketing", description: "Gestisce la comunicazione e l’identità visiva dell’associazione: Instagram, grafiche di eventi…", icon: palette, iconClassName, }, { title: "Gestione HR", + href: "/team#hr", description: "Struttura i processi di recruiting e onboarding e gestisce i volontari del network", icon: hr, iconClassName, }, { title: "Eventi/Partnership", + href: "/team#events-partnership", description: "Organizza eventi e costruisce collaborazioni con aziende e associazioni esterne", icon: events, iconClassName, diff --git a/src/components/join/introduction.tsx b/src/components/join/introduction.tsx index e49057a4..71aa648c 100644 --- a/src/components/join/introduction.tsx +++ b/src/components/join/introduction.tsx @@ -1,23 +1,17 @@ import Image from "next/image" -import { FiMessageCircle, FiUserCheck, FiUsers } from "react-icons/fi" import networkLogo from "@/assets/images/join/network-logo.png" -import { CardIcon } from "@/components/card-icon" -import { Highlight, SectionTitle } from "@/components/collabora/section" -import { SECTION_LEAD } from "@/components/layout/page" +import { Highlight } from "@/components/collabora/section" +import { CONTAINER, INLINE_LINK, SECTION_LEAD, SECTION_TITLE } from "@/components/layout/page" import { cn } from "@/lib/utils" -const paths = [ - { title: "Diventa Admin", icon: FiMessageCircle, href: "#admin" }, - { title: "Entra in un Team", icon: FiUsers, href: "#team" }, - { title: "Diventa Socio", icon: FiUserCheck, href: "#socio" }, -] - export function Introduction() { return ( -
    -
    - Cosa significa far parte di PoliNetwork -
    +
    +
    +

    Cosa significa far parte di PoliNetwork

    +

    PoliNetwork è gestita interamente da studenti del Politecnico di Milano.

    @@ -34,22 +28,23 @@ export function Introduction() {

    - Ci sono tre modi per fare parte di PoliNetwork: diventare admin, entrare in uno dei team operativi o - diventare socio. + Ci sono tre modi per fare parte di PoliNetwork:{" "} + + diventare admin + + ,{" "} + + entrare in uno dei team operativi + {" "} + o{" "} + + diventare socio + + .

    -
      - {paths.map((path) => ( -
    • - -
    • - ))} -
    ) } diff --git a/src/components/join/membership.tsx b/src/components/join/membership.tsx index ecec0b21..0e1a8f25 100644 --- a/src/components/join/membership.tsx +++ b/src/components/join/membership.tsx @@ -1,24 +1,21 @@ import Image from "next/image" import externalLink from "@/assets/icons/join/external-link.svg" import membersPhoto from "@/assets/images/join/members.webp" -import { Highlight, SectionTitle } from "@/components/collabora/section" -import { SECTION_LEAD } from "@/components/layout/page" +import { Highlight } from "@/components/collabora/section" +import { CONTAINER, SECTION_LEAD, SECTION_TITLE } from "@/components/layout/page" import { PhotoShapes } from "@/components/shapes" import { cn } from "@/lib/utils" import { membershipSteps } from "./constants" export function MembershipSection() { return ( -
    -
    - +
    +
    +

    Diventa Socio Diventa Socio di PoliNetwork - -
    +

    +

    Il tesseramento è il modo per fare parte di PoliNetwork in modo formale, con{" "} diritto di voto nelle Assemblee dei Soci, di candidarsi alle elezioni del direttivo e{" "} @@ -44,13 +41,13 @@ export function MembershipSection() {

    -
    - Entra in un Team -
    +
    +
    +

    Entra in un Team

    +

    Entrare in un team significa avere un ruolo operativo con deliverable reali, non essere osservatori di un progetto altrui. diff --git a/src/components/layout/page.ts b/src/components/layout/page.ts index 1070c10c..a725375a 100644 --- a/src/components/layout/page.ts +++ b/src/components/layout/page.ts @@ -6,3 +6,7 @@ export const SECTION_TITLE = "typo-headline-medium sm:typo-display-medium text-balance bg-linear-to-b from-text-primary to-text-secondary bg-clip-text text-transparent" export const SECTION_LEAD = "typo-body-large sm:typo-headline-extrasmall text-pretty" + +/** A link inside running text: always underlined, so it reads as a link without relying on colour. */ +export const INLINE_LINK = + "rounded-sm text-blue-secondary underline decoration-1 decoration-blue-secondary/40 underline-offset-4 outline-none transition-colors hover:decoration-blue-secondary focus-visible:ring-2 focus-visible:ring-blue-secondary" From 87691688deafc11a6cca6e4e246a77ef7899f827 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriele=20Vigan=C3=B2?= Date: Mon, 2 Nov 2026 16:19:24 +0100 Subject: [PATCH 08/22] feat(collabora): hold the page together between the Figma frames; share one page rhythm (#205) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor(layout): share one vertical rhythm across the redesigned pages Storia, Su di noi, Struttura, Team and Unisciti each spaced their sections differently (144 to 288px between sections at 1728, hero titles between y=224 and y=282). They now share PAGE_MAIN and PAGE_HERO from components/layout/page.ts, measured on the Figma frames: - 180px between section blocks from xl (Figma, Su di noi and Storia), 128px on tablets, 96px on phones; the same space before the footer. - Hero title at y=286 on desktop as in every Figma frame, with ~270px of air after the hero instead of the regular gap. Unisciti and Team had stretched their gaps (224-288px) to make up for sections that are shorter than in Figma; that space now follows the shared rule instead. Co-Authored-By: Claude Opus 5.5 * feat(collabora): hold the page together between the Figma frames /collabora matched Figma at 1728px but fell apart elsewhere: everything stacked below 1440px with a 719px column and half the screen empty, the contact steps stayed vertical up to 1615px, and the copy ran in 24px Poppins on tablets. - Shared container, section titles and rhythm (PAGE_MAIN/PAGE_HERO) of the other redesigned pages instead of the page's own 145px-gutter frame and per-section paddings. - Copy steps 16 → 20 → 24px (Figma size from 3xl) and stays left-aligned; the wide "Cosa possiamo fare insieme?" paragraph narrows to ~65 characters below 3xl. Line breaks forced for the 1728px frame are gone. - Figures: one row of four on laptops, two on tablets, Figma's 2×2 beside the copy from xl; cards size to their content. - Photo sections side by side from lg (Figma's 719:611 ratio, centred); stacked below with the photo as wide as the copy. - Contact steps in a row from lg with flexible cards; the e-mail step is a mailto link and the only card that reacts to hover. - InfoCard: optional titleClassName. Co-Authored-By: Claude Opus 5.5 * refactor(collabora): use the shared section tokens and pass card title styles once - SectionTitle and sectionTextClass give way to SECTION_TITLE and SECTION_LEAD, so /collabora's titles and copy step at the same breakpoints as the other pages. - InfoCard takes titleClassName as a prop: /collabora sets it once, and /join no longer reaches into the card with [&_h3] selectors. - PAGE_HERO_TOP lets pages that need no space under the hero use the hero's top padding without cancelling the bottom one. --------- Co-authored-by: Bianca Roberta Ianosel --- src/app/about/page.tsx | 6 +- src/app/chi-siamo/page.tsx | 3 +- src/app/collabora/page.tsx | 8 +- src/app/join/page.tsx | 6 +- src/app/storia/page.tsx | 130 +++++++++--------- src/app/team/page.tsx | 6 +- src/components/chi-siamo/vision-section.tsx | 4 +- src/components/collabora/about-section.tsx | 24 ++-- .../collabora/association-section.tsx | 8 +- .../collabora/collaboration-section.tsx | 12 +- src/components/collabora/company-section.tsx | 1 - src/components/collabora/contact-section.tsx | 20 ++- src/components/collabora/info-card.tsx | 10 +- src/components/collabora/photo-section.tsx | 25 ++-- src/components/collabora/section.tsx | 26 ---- src/components/join/admin.tsx | 3 +- src/components/join/cards.tsx | 7 +- src/components/layout/page.ts | 14 ++ src/components/storia/hero.tsx | 7 +- 19 files changed, 158 insertions(+), 162 deletions(-) diff --git a/src/app/about/page.tsx b/src/app/about/page.tsx index a2851a4e..6c8380ea 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, SECTION_TITLE } from "@/components/layout/page" +import { CONTAINER, PAGE_HERO, PAGE_MAIN, 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" @@ -22,8 +22,8 @@ const titleClass = `${SECTION_TITLE} text-center` export default function StructurePage() { return ( -

    -
    +
    +
    diff --git a/src/app/chi-siamo/page.tsx b/src/app/chi-siamo/page.tsx index d96a6022..88a51feb 100644 --- a/src/app/chi-siamo/page.tsx +++ b/src/app/chi-siamo/page.tsx @@ -6,6 +6,7 @@ import { PathSection } from "@/components/chi-siamo/path-section" import { ProblemSection } from "@/components/chi-siamo/problem-section" import { SupportSection } from "@/components/chi-siamo/support-section" import { VisionSection } from "@/components/chi-siamo/vision-section" +import { PAGE_MAIN } from "@/components/layout/page" export const metadata: Metadata = { title: "Su di noi", @@ -14,7 +15,7 @@ export const metadata: Metadata = { export default function ChiSiamoPage() { return ( -
    +
    diff --git a/src/app/collabora/page.tsx b/src/app/collabora/page.tsx index a2889aaf..16fe2bc0 100644 --- a/src/app/collabora/page.tsx +++ b/src/app/collabora/page.tsx @@ -4,9 +4,8 @@ import { AssociationSection } from "@/components/collabora/association-section" import { CollaborationSection } from "@/components/collabora/collaboration-section" import { CompanySection } from "@/components/collabora/company-section" import { ContactSection } from "@/components/collabora/contact-section" -import { sectionContainerClass } from "@/components/collabora/section" +import { CONTAINER, PAGE_HERO, PAGE_MAIN } from "@/components/layout/page" import { Hero } from "@/components/ui/hero" -import { cn } from "@/lib/utils" export const metadata: Metadata = { title: "Collabora con Noi", @@ -16,9 +15,8 @@ export const metadata: Metadata = { export default function CollaboraPage() { return ( -
    - {/* Title at y=262 (mobile) / 286 (desktop) as in Figma; Hero adds 16px / 56px above it */} -
    +
    +
    -
    +
    +
    +
    -
    - {/* Figma reserves the right column for an archive photo that doesn't exist yet: until then the text stands alone. - The two milestone cards that filled it repeated the timeline right below. */} - -

    Una domanda semplice, una grande idea.

    -

    - Tutto è cominciato nel 2016, quando un futuro studente cercava uno spazio per entrare in contatto con i - propri compagni e trovare risposte alle prime domande sulla vita universitaria. -

    -

    - Da questa esigenza è nato un progetto collettivo, che nel tempo si è trasformato in una{" "} - rete capillare costruita dagli studenti, per gli studenti. -

    - - } - classNames={{ - section: `${CONTAINER} items-start [&>div>h2]:typo-title-large`, - textDiv: "max-w-180 items-start", - title: "text-balance text-start text-blue-secondary", - descriptionDiv: "text-start", - contentDiv: "hidden", - }} - /> - - - - - + +

    Una domanda semplice, una grande idea.

    +

    + Tutto è cominciato nel 2016, quando un futuro studente cercava uno spazio per entrare in contatto con i + propri compagni e trovare risposte alle prime domande sulla vita universitaria. +

    +

    + Da questa esigenza è nato un progetto collettivo, che nel tempo si è trasformato in una{" "} + rete capillare costruita dagli studenti, per gli studenti. +

    + + } + classNames={{ + section: `${CONTAINER} items-start [&>div>h2]:typo-title-large`, + textDiv: "max-w-180 items-start", + title: "text-balance text-start text-blue-secondary", + descriptionDiv: "text-start", + contentDiv: "hidden", + }} + /> + + + + + - -

    Essere lì quando serve.

    -

    - Al di là dei progetti e dei team, PoliNetwork fa una cosa davvero importante: risponde alle domande - degli studenti. -

    -

    - Intorno ai gruppi dei corsi sono nati spazi per tutto ciò che riguarda la vita universitaria: dagli - esami ai piani di studio, dalla ricerca casa alla compravendita dei libri, fino al tempo libero. -

    - - } - classNames={{ - section: `${CONTAINER} gap-12 xl:flex-row xl:items-center xl:gap-16 [&>div>h2]:typo-title-large`, - title: "text-balance text-start text-blue-secondary", - textDiv: "min-w-0 items-start xl:w-1/2", - descriptionDiv: "text-start", - contentDiv: "min-w-0 xl:w-1/2", - }} - > -
    - -

    - L’obiettivo è raggiunto. + +

    Essere lì quando serve.

    +

    + Al di là dei progetti e dei team, PoliNetwork fa una cosa davvero importante: risponde alle domande degli + studenti.

    - Se anche un solo studente ha trovato casa grazie a un nostro gruppo, o si è laureato in tempo perché - qualcuno gli ha risposto. + Intorno ai gruppi dei corsi sono nati spazi per tutto ciò che riguarda la vita universitaria: dagli esami + ai piani di studio, dalla ricerca casa alla compravendita dei libri, fino al tempo libero.

    -
    -
    + + } + classNames={{ + section: `${CONTAINER} gap-12 xl:flex-row xl:items-center xl:gap-16 [&>div>h2]:typo-title-large`, + title: "text-balance text-start text-blue-secondary", + textDiv: "min-w-0 items-start xl:w-1/2", + descriptionDiv: "text-start", + contentDiv: "min-w-0 xl:w-1/2", + }} + > +
    + +

    + L’obiettivo è raggiunto. +

    +

    + Se anche un solo studente ha trovato casa grazie a un nostro gruppo, o si è laureato in tempo perché + qualcuno gli ha risposto. +

    +
    +
    - + - -
    +
    ) } diff --git a/src/app/team/page.tsx b/src/app/team/page.tsx index 5489f290..4f0dcd34 100644 --- a/src/app/team/page.tsx +++ b/src/app/team/page.tsx @@ -1,7 +1,7 @@ import type { Metadata } from "next" import { FiUsers } from "react-icons/fi" import { ClosingCta } from "@/components/layout/closing-cta" -import { CONTAINER } from "@/components/layout/page" +import { CONTAINER, PAGE_HERO, PAGE_MAIN } from "@/components/layout/page" import { applicationHref, teams } from "@/components/team/constants" import { TeamSection } from "@/components/team/team-section" import { Button } from "@/components/ui/button" @@ -14,8 +14,8 @@ export const metadata: Metadata = { export default function TeamPage() { return ( -
    -
    +
    +
    +

    La nostra visione

    Un Politecnico in cui nessuno studente resti indietro per mancanza di{" "} diff --git a/src/components/collabora/about-section.tsx b/src/components/collabora/about-section.tsx index 5ac09718..48202e60 100644 --- a/src/components/collabora/about-section.tsx +++ b/src/components/collabora/about-section.tsx @@ -4,11 +4,12 @@ import chatIcon from "@/assets/icons/collabora/chat.svg" import groupIcon from "@/assets/icons/collabora/group.svg" import handshakeIcon from "@/assets/icons/collabora/handshake.svg" import schoolIcon from "@/assets/icons/collabora/school.svg" +import { CONTAINER, SECTION_LEAD, SECTION_TITLE } from "@/components/layout/page" import { Button } from "@/components/ui/button" import { Carousel, CarouselContent, CarouselDots, CarouselItem } from "@/components/ui/carousel" import { cn } from "@/lib/utils" import { InfoCard, type InfoCardData } from "./info-card" -import { Aside, Highlight, SectionTitle, sectionContainerClass, sectionTextClass } from "./section" +import { Aside, Highlight } from "./section" const cards: InfoCardData[] = [ { @@ -39,9 +40,7 @@ const cards: InfoCardData[] = [ title: "1000+", description: ( <> - Persone nel nostro evento -
    - più grande + Persone nel nostro evento più grande ), mobileDescription: "Persone al nostro evento più grande", @@ -65,12 +64,13 @@ const cards: InfoCardData[] = [ export function AboutSection() { return ( -
    -
    -
    +
    + {/* Copy and figures side by side from xl, as in Figma; below, the figures run as one row (two on tablets) */} +
    +
    - Chi siamo? -
    +

    Chi siamo?

    +

    PoliNetwork è l'infrastruttura di comunicazione studentesca del Politecnico di Milano.

    Gestiamo oltre 500 gruppi Telegram e WhatsApp, una presenza social in crescita e strumenti digitali @@ -92,14 +92,14 @@ export function AboutSection() {

    -
    +
    {cards.map((card) => ( - + ))}
    {/* Bleeds to the screen edges like the Figma carousel, so the next card peeks in */} - + {cards.map((card) => ( diff --git a/src/components/collabora/association-section.tsx b/src/components/collabora/association-section.tsx index 1e2fd866..cc04e76e 100644 --- a/src/components/collabora/association-section.tsx +++ b/src/components/collabora/association-section.tsx @@ -39,7 +39,6 @@ export function AssociationSection() { }} /> } - className="py-30 md:py-37.5" >

    Se fai parte dell'ecosistema universitario del Politecnico di Milano o di un'altra università, possiamo valutare @@ -48,11 +47,8 @@ export function AssociationSection() {

    Le collaborazioni più efficaci che abbiamo avuto nel tempo sono nate da gruppi studenteschi che condividevano -
    una parte degli obiettivi che perseguiamo:{" "} - - rendere la vita universitaria meno complicata e più ricca per chi studia -
    al Politecnico. -
    + una parte degli obiettivi che perseguiamo:{" "} + rendere la vita universitaria meno complicata e più ricca per chi studia al Politecnico.

    ) diff --git a/src/components/collabora/collaboration-section.tsx b/src/components/collabora/collaboration-section.tsx index e615b72f..81832916 100644 --- a/src/components/collabora/collaboration-section.tsx +++ b/src/components/collabora/collaboration-section.tsx @@ -1,13 +1,15 @@ +import { CONTAINER, SECTION_LEAD, SECTION_TITLE } from "@/components/layout/page" import { cn } from "@/lib/utils" -import { Aside, Highlight, SectionTitle, sectionContainerClass, sectionTextClass } from "./section" +import { Aside, Highlight } from "./section" export function CollaborationSection() { return ( -
    +
    - Cosa possiamo fare insieme? -
    -

    +

    Cosa possiamo fare insieme?

    +
    + {/* Figma runs this paragraph 959px wide at 24px; at 20px that is ~90 characters a line, so it narrows below 3xl */} +

    Collaborare con noi significa accedere a questa community in modo diretto. Non siamo un canale pubblicitario e non accettiamo promozioni commerciali generiche: ogni collaborazione viene valutata in termini di valore concreto per gli studenti del Politecnico{" "} diff --git a/src/components/collabora/company-section.tsx b/src/components/collabora/company-section.tsx index d04247be..f4bf6bcc 100644 --- a/src/components/collabora/company-section.tsx +++ b/src/components/collabora/company-section.tsx @@ -33,7 +33,6 @@ export function CompanySection() { /> } photoFirst - className="py-30 md:py-31.25" >

    PoliNetwork è un punto di accesso diretto alla community studentesca del Politecnico di Milano: migliaia di diff --git a/src/components/collabora/contact-section.tsx b/src/components/collabora/contact-section.tsx index cdd23e4e..4e43c90d 100644 --- a/src/components/collabora/contact-section.tsx +++ b/src/components/collabora/contact-section.tsx @@ -2,9 +2,9 @@ import { Fragment } from "react" import favoriteIcon from "@/assets/icons/collabora/favorite.svg" import mailIcon from "@/assets/icons/collabora/mail.svg" import promptSuggestionIcon from "@/assets/icons/collabora/prompt-suggestion.svg" +import { CONTAINER, SECTION_TITLE } from "@/components/layout/page" import { cn } from "@/lib/utils" import { InfoCard, type InfoCardData } from "./info-card" -import { SectionTitle, sectionContainerClass } from "./section" const cards: InfoCardData[] = [ { @@ -14,6 +14,8 @@ const cards: InfoCardData[] = [ icon: mailIcon, iconClassName: "mr-2.5 h-5.5 w-7 md:mr-0 md:h-12 md:w-15", descriptionClassName: "max-w-56", + // The only step that is an action: it opens the mail client + href: "mailto:events@polinetwork.org", }, { title: "Condividi la tua visione", @@ -46,18 +48,24 @@ const connectorClassNames = [ export function ContactSection() { return ( -

    - Come contattarci +
    +

    Come contattarci

    -
    + {/* A row of equal steps from lg (tighter padding until 3xl); stacked below, at Figma's card width */} +
    {cards.map((card, index) => ( - + {index < connectorClassNames.length && (
    diff --git a/src/components/collabora/info-card.tsx b/src/components/collabora/info-card.tsx index e84892a6..b9c4bb7c 100644 --- a/src/components/collabora/info-card.tsx +++ b/src/components/collabora/info-card.tsx @@ -27,10 +27,13 @@ export type InfoCardData = { export function InfoCard({ card, className, + titleClassName, descriptionColorClassName = "text-text-primary", }: { card: InfoCardData className?: string + /** Title size and wrapping, for rows where the default single-line title doesn't fit. */ + titleClassName?: string descriptionColorClassName?: string }) { const { title, description, mobileDescription, icon, iconClassName, descriptionClassName, href } = card @@ -53,7 +56,12 @@ export function InfoCard({
    {/* On the narrowest phones an email title may not fit: let it wrap after the "@" */} -

    +

    {title.replace("@", "@\u200B")}

    diff --git a/src/components/collabora/photo-section.tsx b/src/components/collabora/photo-section.tsx index 52c51847..03528219 100644 --- a/src/components/collabora/photo-section.tsx +++ b/src/components/collabora/photo-section.tsx @@ -1,7 +1,7 @@ import Image, { type StaticImageData } from "next/image" import type { ReactNode } from "react" +import { CONTAINER, SECTION_LEAD, SECTION_TITLE } from "@/components/layout/page" import { cn } from "@/lib/utils" -import { SectionTitle, sectionContainerClass, sectionTextClass } from "./section" type PhotoSectionProps = { title: ReactNode @@ -15,12 +15,12 @@ type PhotoSectionProps = { decorations: ReactNode /** Photo on the left on desktop. */ photoFirst?: boolean - className?: string } /** - * Heading + copy next to a photo. Stacked (copy, then photo) up to `xl`; from `xl` the two columns keep the - * 719:611 ratio of the 1728px Figma frame, bottom-aligned. + * Heading + copy next to a photo. Stacked (copy, then a photo as wide as the copy) on phones and portrait tablets; + * from `lg` the two columns keep the 719:611 ratio of the 1728px Figma frame, centred on each other so a taller + * text column doesn't leave the photo hanging at one edge. */ export function PhotoSection({ title, @@ -30,27 +30,26 @@ export function PhotoSection({ frameClassName, decorations, photoFirst = false, - className, }: PhotoSectionProps) { return ( -
    +
    -
    - {title} -
    {children}
    +
    +

    {title}

    +
    {children}
    -
    +
    diff --git a/src/components/collabora/section.tsx b/src/components/collabora/section.tsx index 03da89db..6c085ee6 100644 --- a/src/components/collabora/section.tsx +++ b/src/components/collabora/section.tsx @@ -1,32 +1,6 @@ import type { ReactNode } from "react" import { cn } from "@/lib/utils" -/** - * Horizontal frame shared by every Collabora section: 24px gutters on mobile (402px Figma frame), growing to the - * 145px gutters of the 1728px desktop frame, then centered so the content column never exceeds 1438px. - */ -export const sectionContainerClass = "mx-auto w-full max-w-432 px-6 md:px-[clamp(1.5rem,8.391%,9.0625rem)]" - -export function SectionTitle({ children, className }: { children: ReactNode; className?: string }) { - return ( -

    - {children} -

    - ) -} - -/** - * Section body copy: Red Hat 16px centered on mobile, Poppins 24px on desktop. Browsers add the letter-spacing after - * the last glyph of a line too, so lines that exactly fill their Figma box would wrap: the box gets 1px of slack. - */ -export const sectionTextClass = - "typo-body-large md:typo-headline-small -mx-[0.5px] text-center text-black md:mr-[-1px] md:ml-0 md:text-start" - export function Highlight({ children }: { children: ReactNode }) { return {children} } diff --git a/src/components/join/admin.tsx b/src/components/join/admin.tsx index 7eff89fa..12eb0332 100644 --- a/src/components/join/admin.tsx +++ b/src/components/join/admin.tsx @@ -41,7 +41,8 @@ export function AdminSection() {
  1. {index < connectors.length && ( {cards.map((card) => ( - + ))}
  2. ) @@ -23,7 +24,7 @@ export function JoinCards({ cards, carousel = false }: { cards: InfoCardData[]; {cards.map((card) => ( - + ))} diff --git a/src/components/layout/page.ts b/src/components/layout/page.ts index a725375a..ca8cc332 100644 --- a/src/components/layout/page.ts +++ b/src/components/layout/page.ts @@ -10,3 +10,17 @@ export const SECTION_LEAD = "typo-body-large sm:typo-headline-extrasmall text-pr /** A link inside running text: always underlined, so it reads as a link without relying on colour. */ export const INLINE_LINK = "rounded-sm text-blue-secondary underline decoration-1 decoration-blue-secondary/40 underline-offset-4 outline-none transition-colors hover:decoration-blue-secondary focus-visible:ring-2 focus-visible:ring-blue-secondary" + +/** + * Vertical rhythm of the redesigned pages. Figma leaves 180px between section blocks at 1728px (Su di noi, Storia): + * 96px on phones, 128px on tablets, 180px from xl. The bottom padding repeats the gap before the footer. + */ +export const PAGE_MAIN = "flex w-full flex-col gap-24 pb-24 md:gap-32 md:pb-32 xl:gap-45 xl:pb-45" + +/** + * The page's opening section. The top padding clears the fixed header: with `Hero`'s own offset the title sits at + * y=286 on desktop, as in every Figma frame. Figma also leaves more air after the hero (~270px at 1728) than between + * sections (180px): the bottom padding adds that difference to the main gap. + */ +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` diff --git a/src/components/storia/hero.tsx b/src/components/storia/hero.tsx index b13235ee..071ac805 100644 --- a/src/components/storia/hero.tsx +++ b/src/components/storia/hero.tsx @@ -1,18 +1,17 @@ import Link from "next/link" import { FiArrowDown } from "react-icons/fi" -import { CONTAINER } from "@/components/layout/page" +import { CONTAINER, PAGE_HERO } from "@/components/layout/page" import { Button } from "@/components/ui/button" import { Hero } from "@/components/ui/hero" -// Figma: a centred XL title and nothing else. The board portraits appear further down, under "I nostri direttivi". export function HistoryHero() { return ( -
    +
    -
    - Foto di gruppo della community PoliNetwork -
    + Foto di gruppo della community PoliNetwork
    ) } From 3dd3cfb366e2fc6e9d3b22ed59d9919195f3f573 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriele=20Vigan=C3=B2?= Date: Mon, 2 Nov 2026 16:33:48 +0100 Subject: [PATCH 10/22] feat(projects): content-sized project cards and a working collection search (#207) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(contact): align Contattaci with the shared layout and fix the card grid - 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 * 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. * fix(contact): let the group photo fill its column, centre the Collabora block * fix(contact): centre the groups text when the photo wraps below it * 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 * 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. * 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. --------- Co-authored-by: Claude Opus 5.5 Co-authored-by: Bianca Roberta Ianosel --- src/app/about/page.tsx | 9 +- src/app/projects/[id]/page.tsx | 43 ++++--- src/app/projects/page.tsx | 11 +- src/components/card-caption.tsx | 23 +++- src/components/home/projects.tsx | 25 ++-- src/components/layout/page.ts | 3 + src/components/peek-carousel.tsx | 34 ++++++ src/components/projects/collection.tsx | 131 +++++++++------------ src/components/projects/community-news.tsx | 81 ++++--------- src/components/projects/deprecated.tsx | 97 +++------------ src/components/projects/project-card.tsx | 32 +++++ src/components/projects/project-list.tsx | 74 ++++++++++++ src/components/projects/resources.ts | 20 +++- src/components/projects/upload.tsx | 48 ++++---- 14 files changed, 337 insertions(+), 294 deletions(-) create mode 100644 src/components/peek-carousel.tsx create mode 100644 src/components/projects/project-card.tsx create mode 100644 src/components/projects/project-list.tsx 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 8d1c623e..a721908d 100644 --- a/src/app/projects/[id]/page.tsx +++ b/src/app/projects/[id]/page.tsx @@ -2,10 +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 { SECTION_LEAD, SECTION_TITLE } from "@/components/layout/page" +import { ClosingCta } from "@/components/layout/closing-cta" +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" @@ -39,16 +40,21 @@ export default async function ProjectPage({ params }: Props) { const resources = getProjectResources(project) return ( -
    -
    +
    +
    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 && ( @@ -74,10 +76,7 @@ export default async function ProjectPage({ params }: Props) {
    {resources.length > 0 && ( -
    +

    Come usarlo @@ -135,19 +134,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..25e85bd6 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,9 +21,12 @@ export default async function Home() { const deprecated = projects.filter((project) => project.category === "deprecated") return ( -
    +
    +
    + +
    - +
    diff --git a/src/components/card-caption.tsx b/src/components/card-caption.tsx index 43fe8e3b..70468f5c 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,19 @@ export function CardCaption({ - {title} - {icon && } + + {/* Gradient per line (box-decoration-clone): a two-line title doesn't fade into a paler second line */} + + {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 61345010..e320f5b2 100644 --- a/src/components/projects/collection.tsx +++ b/src/components/projects/collection.tsx @@ -1,94 +1,75 @@ "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 { 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" -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) +/** 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 + ? searchable.filter((project) => { + const text = normalize(`${project.title} ${project.descriptionIt}`) + return words.every((word) => text.includes(word)) + }) + : projects return ( -
    -
    -

    +
    +
    +

    Esplora la raccolta completa dei progetti

    -
    + {searchable.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 && `${matches.length} progetti trovati`} +

    + + {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..98a8fb75 100644 --- a/src/components/projects/community-news.tsx +++ b/src/components/projects/community-news.tsx @@ -1,68 +1,27 @@ -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 { INLINE_LINK, SECTION_LEAD, SECTION_STACK, SECTION_TITLE } from "../layout/page" +import { ProjectList, ProjectsEmpty } from "./project-list" +import type { Project } from "./resources" 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..96b23070 100644 --- a/src/components/projects/deprecated.tsx +++ b/src/components/projects/deprecated.tsx @@ -1,87 +1,24 @@ -"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_STACK, SECTION_TITLE } from "../layout/page" +import { ProjectList, ProjectsEmpty } from "./project-list" +import type { Project } from "./resources" 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..291485d4 --- /dev/null +++ b/src/components/projects/project-card.tsx @@ -0,0 +1,32 @@ +import { FiArrowRight } from "react-icons/fi" +import { CardCaption } from "../card-caption" +import { getProjectResources, type Project, resourceDetails } from "./resources" + +/** 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 ( + + {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..72ee5c4a --- /dev/null +++ b/src/components/projects/project-list.tsx @@ -0,0 +1,74 @@ +"use client" + +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 { 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 && ( + + )} +
    + + project.id} + render={(project) => } + className="sm:hidden" + /> + + ) +} + +/** 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/upload.tsx b/src/components/projects/upload.tsx index 5bcd90bf..b057c9e2 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,32 @@ const cards = [ export function Upload() { return ( -
      -
      -
      -

      Hai un'idea?

      -

      Proponila qui!

      -
      -
      -

      +

      +
      +
      +
      +

      + 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 f1417224cfbce053681388825b4c94f811261c2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriele=20Vigan=C3=B2?= Date: Mon, 2 Nov 2026 16:52:20 +0100 Subject: [PATCH 11/22] feat(groups): fix the results layout and give every link a hover state (#208) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(contact): align Contattaci with the shared layout and fix the card grid - 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 * 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. * fix(contact): let the group photo fill its column, centre the Collabora block * fix(contact): centre the groups text when the photo wraps below it * 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 * 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. * 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. * 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 * 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. --------- Co-authored-by: Bianca Roberta Ianosel --- 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 | 87 ++++++++++--------------- 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 +- src/queries/groups.ts | 9 ++- 10 files changed, 158 insertions(+), 117 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..558a8de3 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} - -
    - - + + {/* In a card narrower than 20rem the name gets its own line, so it doesn't wrap 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..b0ff9c99 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,50 +99,33 @@ 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 site, school, level and course groups */} + {generalGroups.length > 0 && ( +
    +

    + Gruppi generali +

    + {generalGroups.map(({ group, secondary }) => ( + ))} -
    +
    )} {cohorts.map((cohort) => ( @@ -156,7 +137,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 (
    - -
    + +
    { - 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 f24f8c74a2e66e70fe655d3283b477047131a775 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriele=20Vigan=C3=B2?= Date: Mon, 2 Nov 2026 16:53:57 +0100 Subject: [PATCH 12/22] feat(matricole): align Matricole with the shared layout; fix CardIcon responsive sizes (#209) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(contact): align Contattaci with the shared layout and fix the card grid - 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 * 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. * fix(contact): let the group photo fill its column, centre the Collabora block * fix(contact): centre the groups text when the photo wraps below it * 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. * 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. * 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. * 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 * 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. * fix(card-icon): generate the responsive size classes 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 * 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 * 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. --------- Co-authored-by: Bianca Roberta Ianosel --- src/app/matricole/page.tsx | 5 ++- src/components/card-group/index.tsx | 8 ++-- 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 | 10 +++++ src/components/matricole/faqs.tsx | 10 ++--- src/components/matricole/guides.tsx | 53 ++++++++++++++------------- src/components/matricole/intro.tsx | 41 +++++++++------------ src/components/wizard/school-step.tsx | 2 +- src/styles/globals.css | 7 ++++ tests/card-icon-classes.test.mjs | 18 +++++++++ 12 files changed, 95 insertions(+), 63 deletions(-) create mode 100644 tests/card-icon-classes.test.mjs 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/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 20889aea..dbd1e6b7 100644 --- a/src/components/matricole/cards.ts +++ b/src/components/matricole/cards.ts @@ -43,3 +43,13 @@ export function freshmanCardProps(card: FreshmanCard): CardIconProps { href: card.href || undefined, } } + +/** + * 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..77b047dd 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,54 @@ 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 */} +
    + {/* 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 mobile design */} {guides.slice(0, 1).map((guide) => ( ))} -
    +
    {guides.slice(1).map((guide) => ( - + ))}
    - -
    + +
    {content.telegram.map((card) => ( ))}
    -
    - {/* hidden in mobile */} +
    + {/* Hidden on phones, as in the mobile design */} {materials.map((material) => ( ))} - -
    + +
    {tools.map((card) => ( ))} diff --git a/src/components/matricole/intro.tsx b/src/components/matricole/intro.tsx index 6152cdbb..f1d4ccb7 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_TOP } 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 it takes only the hero's top padding +
    + -
    - {resources - .filter((resource) => resource.group === "featured") - .map((resource) => ( - - ))} -
    + {/* Hidden on phones, as in the mobile design */} +
    + {resources + .filter((resource) => resource.group === "featured") + .map((resource) => ( + + ))}
    ) 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" }} /> ))}
    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 320e93ab1aaac36e3f74e089777f15fad6e2fbbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriele=20Vigan=C3=B2?= Date: Mon, 2 Nov 2026 16:57:57 +0100 Subject: [PATCH 13/22] feat(associations): honest logo fallback and a steadier accordion (#210) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(contact): align Contattaci with the shared layout and fix the card grid - 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 * 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. * fix(contact): let the group photo fill its column, centre the Collabora block * fix(contact): centre the groups text when the photo wraps below it * 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 * 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. * 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. * 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 * 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. * fix(card-icon): generate the responsive size classes 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 * 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 * 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. * 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 * 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. --------- Co-authored-by: Bianca Roberta Ianosel --- 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 ++-- src/components/storia/hero.tsx | 2 +- .../structure/association-bodies.tsx | 6 +- src/components/ui/hero.tsx | 22 +++-- 12 files changed, 118 insertions(+), 80 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/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" />
    ) } diff --git a/src/components/associations/associations-list.tsx b/src/components/associations/associations-list.tsx index 65dd312a..d22670e6 100644 --- a/src/components/associations/associations-list.tsx +++ b/src/components/associations/associations-list.tsx @@ -12,6 +12,7 @@ import { } from "react-icons/fi" import AccordionAssociation from "@/components/accordion-association" import type { GradientIconType } from "@/components/gradient-icon" +import { EMPTY_STATE } from "@/components/layout/page" import type { ApiOutput } from "@/types" type Association = ApiOutput["web"]["associations"]["getAllAssociations"][number] @@ -33,11 +34,7 @@ const LINK_CONFIG: { key: keyof AssociationLinks; label: string; icon: GradientI export function AssociationsList({ associations }: { associations: Association[] }) { if (associations.length === 0) { // 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. -

    - ) + return

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

    } const accordionItems = associations.map((association) => ({ diff --git a/src/components/faqs/faqs-tabs.tsx b/src/components/faqs/faqs-tabs.tsx index c1080bd6..632cd4a8 100644 --- a/src/components/faqs/faqs-tabs.tsx +++ b/src/components/faqs/faqs-tabs.tsx @@ -4,6 +4,7 @@ import type { IconType } from "react-icons" import { FiBookOpen, FiDollarSign, FiEdit, FiFileText, FiHelpCircle, FiSend } from "react-icons/fi" import AccordionList from "@/components/accordion-list" import CalloutItem from "@/components/callout-item" +import { EMPTY_STATE } from "@/components/layout/page" import TabsNavigation from "@/components/tabs" import { Tabs } from "@/components/tabs/tabs" import { TabsContent } from "@/components/tabs/tabs-content" @@ -19,9 +20,6 @@ const ICONS: Record = { send: FiSend, } -const EMPTY_STATE = - "typo-body-large rounded-rectangles border border-slate-300 border-dashed px-6 py-10 text-center text-slate-600" - export function FaqsTabs({ categories }: { categories: FaqCategory[] }) { const tabs = categories.map((category) => ({ value: String(category.categoryId), diff --git a/src/components/groups/extra-groups.tsx b/src/components/groups/extra-groups.tsx index f0b25c96..a381cb5c 100644 --- a/src/components/groups/extra-groups.tsx +++ b/src/components/groups/extra-groups.tsx @@ -1,5 +1,6 @@ import { CardCourseGroup } from "@/components/card-course-group" -import { GROUPS_COLUMN, GroupsPageHeader } from "@/components/groups/page-header" +import { FLOW_MAIN, FlowHeader } from "@/components/layout/flow-header" +import { EMPTY_STATE } from "@/components/layout/page" import { getVisibleGroups } from "@/queries/groups" import { EXTRA_LABEL, matchesLabelBranch } from "@/utils/labels" import { mergeGroupsByTitle } from "@/utils/merge-groups" @@ -11,8 +12,8 @@ export async function ExtraGroups() { ) return ( -
    - +
    + {extraGroups.length > 0 ? (
    @@ -28,7 +29,7 @@ export async function ExtraGroups() { ))}
    ) : ( -

    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 b0ff9c99..591b4ddd 100644 --- a/src/components/groups/groups-result.tsx +++ b/src/components/groups/groups-result.tsx @@ -1,6 +1,7 @@ import { notFound } from "next/navigation" import { CardCourseGroup } from "@/components/card-course-group" -import { GROUPS_COLUMN, GroupsPageHeader } from "@/components/groups/page-header" +import { FLOW_MAIN, FlowHeader } from "@/components/layout/flow-header" +import { EMPTY_STATE } from "@/components/layout/page" import { getLevel, getSchool } from "@/components/wizard/constants" import { getVisibleGroups } from "@/queries/groups" import type { VisibleGroup } from "@/queries/types" @@ -107,8 +108,8 @@ export async function GroupsResult({ ] return ( -
    - + ))} - {!hasAnyGroup && ( -

    - Nessun gruppo disponibile al momento per questo corso. -

    - )} + {!hasAnyGroup &&

    Nessun gruppo disponibile al momento per questo corso.

    }
    ) } diff --git a/src/components/guides/constants.ts b/src/components/guides/constants.ts index 742951ef..e53f618c 100644 --- a/src/components/guides/constants.ts +++ b/src/components/guides/constants.ts @@ -1,26 +1,7 @@ -export const LOREM_GUIDES = [ - { - value: "lorem-ipsum-dolor-sit-amet", - trigger: "Lorem ipsum dolor sit amet", - content: - "Consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.", - }, - { - value: "ut-enim-ad-minim-veniam", - trigger: "Ut enim ad minim veniam", - content: - "Quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium.", - }, - { - value: "duis-aute-irure-dolor", - trigger: "Duis aute irure dolor", - content: - "In reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt.", - }, - { - value: "excepteur-sint-occaecat", - trigger: "Excepteur sint occaecat", - content: - "Cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt.", - }, -] +import type { AccordionListItem } from "@/components/accordion-list/types" + +// The guides have no content yet, and how they will be organised is still open: until then the pages show their empty state. +export const GENERAL_GUIDES: AccordionListItem[] = [] + +/** Guides for a given course, by course slug. */ +export const COURSE_GUIDES: Record = {} diff --git a/src/components/guides/general-guides.tsx b/src/components/guides/general-guides.tsx index 48b62904..321f7169 100644 --- a/src/components/guides/general-guides.tsx +++ b/src/components/guides/general-guides.tsx @@ -1,38 +1,17 @@ -import Link from "next/link" -import { FiArrowLeft, FiX } from "react-icons/fi" import AccordionList from "@/components/accordion-list" -import { LOREM_GUIDES } from "@/components/guides/constants" +import { GENERAL_GUIDES } from "@/components/guides/constants" +import { FLOW_MAIN, FlowHeader } from "@/components/layout/flow-header" +import { EMPTY_STATE } from "@/components/layout/page" export function GeneralGuides() { return ( -
    -
    - - - -
    -

    Guide Generali

    -
    - - - -
    +
    + - {LOREM_GUIDES.length > 0 ? ( - + {GENERAL_GUIDES.length > 0 ? ( + ) : ( -

    - Nessuna guida disponibile al momento. -

    +

    Nessuna guida disponibile al momento.

    )}
    ) diff --git a/src/components/guides/guides-result.tsx b/src/components/guides/guides-result.tsx index bf98f20f..32f2b5ee 100644 --- a/src/components/guides/guides-result.tsx +++ b/src/components/guides/guides-result.tsx @@ -1,8 +1,8 @@ -import Link from "next/link" import { notFound } from "next/navigation" -import { FiArrowLeft, FiX } from "react-icons/fi" import AccordionList from "@/components/accordion-list" -import { LOREM_GUIDES } from "@/components/guides/constants" +import { COURSE_GUIDES } from "@/components/guides/constants" +import { FLOW_MAIN, FlowHeader } from "@/components/layout/flow-header" +import { EMPTY_STATE } from "@/components/layout/page" import { getLevel, getSchool } from "@/components/wizard/constants" import { getVisibleGroups } from "@/queries/groups" import { courseFacetsForLevel, humanizeSlug } from "@/utils/labels" @@ -28,38 +28,22 @@ export async function GuidesResult({ const courseName = humanizeSlug(course) + const guides = COURSE_GUIDES[course] ?? [] + return ( -
    -
    - - - -
    -

    {courseName}

    -

    - Laurea {currentLevel.name} -

    -
    - - - -
    +
    + - {LOREM_GUIDES.length > 0 ? ( - + {guides.length > 0 ? ( + ) : ( -

    - Nessuna guida disponibile al momento per questo corso. -

    +

    Nessuna guida disponibile al momento per questo corso.

    )}
    ) diff --git a/src/components/groups/page-header.tsx b/src/components/layout/flow-header.tsx similarity index 74% rename from src/components/groups/page-header.tsx rename to src/components/layout/flow-header.tsx index ef06eb8c..6f86ccc6 100644 --- a/src/components/groups/page-header.tsx +++ b/src/components/layout/flow-header.tsx @@ -1,8 +1,13 @@ import Link from "next/link" import { FiArrowLeft, FiX } from "react-icons/fi" +import { CONTAINER } from "@/components/layout/page" +import { cn } from "@/lib/utils" -/** 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" +/** The narrower column of the pages a wizard leads to (group results, guides), with the site's gutters. */ +export const FLOW_COLUMN = cn(CONTAINER, "min-w-0 max-w-5xl") + +/** Page shell for those pages: the top padding leaves the fixed header ~48px of air on phones. */ +export const FLOW_MAIN = `${FLOW_COLUMN} flex min-h-svh flex-col gap-12 pt-44 pb-32 md:gap-16 md:pt-52` 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" @@ -11,7 +16,7 @@ const iconLinkClass = * 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({ +export function FlowHeader({ title, subtitle, backHref, diff --git a/src/components/layout/page.ts b/src/components/layout/page.ts index 495b9d2a..fcde2497 100644 --- a/src/components/layout/page.ts +++ b/src/components/layout/page.ts @@ -27,3 +27,7 @@ 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` + +/** Placeholder for a list with nothing to show yet: dashed, so it reads as "empty" rather than as a card. */ +export const EMPTY_STATE = + "typo-body-large wrap-anywhere rounded-rectangles border border-slate-300 border-dashed px-6 py-10 text-center text-slate-600" diff --git a/src/components/projects/project-list.tsx b/src/components/projects/project-list.tsx index 72ee5c4a..6429d5f8 100644 --- a/src/components/projects/project-list.tsx +++ b/src/components/projects/project-list.tsx @@ -2,6 +2,7 @@ import { type ReactNode, useId, useState } from "react" import { FiArrowDown, FiArrowUp } from "react-icons/fi" +import { EMPTY_STATE } from "@/components/layout/page" import { PeekCarousel } from "../peek-carousel" import { Button } from "../ui/button" import { ProjectCard } from "./project-card" @@ -66,9 +67,5 @@ 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} -
    - ) + return
    {children}
    } diff --git a/src/components/wizard/course-step.tsx b/src/components/wizard/course-step.tsx index a3b570ac..c2872829 100644 --- a/src/components/wizard/course-step.tsx +++ b/src/components/wizard/course-step.tsx @@ -2,6 +2,7 @@ import Link from "next/link" import { notFound } from "next/navigation" import { FiChevronRight } from "react-icons/fi" import { CardCourse } from "@/components/card-course" +import { EMPTY_STATE } from "@/components/layout/page" import { getLevel, getSchool } from "@/components/wizard/constants" import { CourseFilters, type FilterOption } from "@/components/wizard/course-filters" import { WizardShell } from "@/components/wizard/wizard-shell" @@ -76,9 +77,7 @@ export async function CourseStep({ } >
    - {courseSlugs.length === 0 && ( -

    Nessun corso disponibile al momento.

    - )} + {courseSlugs.length === 0 &&

    Nessun corso disponibile al momento.

    } {courseSlugs.map((course) => { const facets = facetsByCourse.get(course) ?? [] const courseCampus = campusFacet(facets) From dc46b29cd3fa7fa34b4fc676c737c645883beea6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriele=20Vigan=C3=B2?= Date: Mon, 2 Nov 2026 17:05:54 +0100 Subject: [PATCH 16/22] feat(matricole): align the Guida della Matricola page with the shared layout (#213) * feat(matricole): align the Guida della Matricola page with the shared layout The standalone download page kept its own title steps (45px up to 1023px, then 120px) and 48px side margins. It now uses the fluid display scale and the shared container gutters, stays centered with padding for short screens, and its title is the page's h1 (header and footer are hidden here). CardSplit, used only by this page, tightens its stacked phone layout. Co-Authored-By: Claude Opus 5.5 * refactor(matricole): the Guida della Matricola title uses Hero It was the third page-title implementation, with its own gradient and sizes. Hero already fits long words to the width. * fix(matricole): restore the Guida della Matricola's own title Hero wrapped the title onto three lines at 320px and pushed the download button below a 320x568 screen. This standalone page has no header, so it keeps its fluid title; a comment now says why. --------- Co-authored-by: Bianca Roberta Ianosel --- src/app/matricole/guida/page.tsx | 15 ++++++++++----- src/components/card-split/index.tsx | 4 ++-- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/app/matricole/guida/page.tsx b/src/app/matricole/guida/page.tsx index 91334bfb..c959f7a6 100644 --- a/src/app/matricole/guida/page.tsx +++ b/src/app/matricole/guida/page.tsx @@ -1,7 +1,9 @@ import type { Metadata } from "next" import { FiDownload } from "react-icons/fi" import { CardSplit } from "@/components/card-split" +import { CONTAINER } from "@/components/layout/page" import { Button } from "@/components/ui/button" +import { cn } from "@/lib/utils" import { getLatestGuidaMatricola } from "@/queries/guida-matricola" export const metadata: Metadata = { @@ -22,12 +24,15 @@ export default async function GuidaMatricolaPage() { const formattedDate = new Intl.DateTimeFormat("it-IT", { dateStyle: "long" }).format(new Date(guida.date)) return ( -
    -
    -

    + // Standalone page (no header or footer, see `HIDDEN_ROUTES`): the content stays centered on screen, with the + // shared gutters. The vertical padding keeps it off the edges on short landscape screens. +
    + {/* Not Hero: this page has no header, and Hero's spacing would push the download below the fold on phones */} +
    +

    Guida della Matricola -

    -

    +

    +

    Controlla l'ultima versione disponibile e scarica il PDF aggiornato.

    diff --git a/src/components/card-split/index.tsx b/src/components/card-split/index.tsx index 33049da0..38451fc9 100644 --- a/src/components/card-split/index.tsx +++ b/src/components/card-split/index.tsx @@ -15,8 +15,8 @@ export function CardSplit({ textPrimary, textSecondary, textSecondarySmall, acti className )} > -
    -
    +
    +
    {textPrimary ? : null} {hasSecondaryContent && ( From 38abe68e3ac2219d05dd40a9d61a03a6cddf71f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriele=20Vigan=C3=B2?= Date: Mon, 2 Nov 2026 17:08:21 +0100 Subject: [PATCH 17/22] feat(header): readable glass menus, mobile menu fixes and a single h1 (#214) * feat(matricole): align the Guida della Matricola page with the shared layout The standalone download page kept its own title steps (45px up to 1023px, then 120px) and 48px side margins. It now uses the fluid display scale and the shared container gutters, stays centered with padding for short screens, and its title is the page's h1 (header and footer are hidden here). CardSplit, used only by this page, tightens its stacked phone layout. Co-Authored-By: Claude Opus 5.5 * refactor(matricole): the Guida della Matricola title uses Hero It was the third page-title implementation, with its own gradient and sizes. Hero already fits long words to the width. * fix(matricole): restore the Guida della Matricola's own title Hero wrapped the title onto three lines at 320px and pushed the download button below a 320x568 screen. This standalone page has no header, so it keeps its fluid title; a comment now says why. * feat(header): readable glass menus, mobile menu fixes and a single h1 The header sat inside the page wrapper (isolate + overflow-clip), where Chromium skipped its backdrop: the glass never blurred, so the page titles read sharp through the open menus. It now renders outside the wrapper and the open panels use a stronger blur, keeping the glass. The mobile menu closes on navigation and on Escape, exposes aria-expanded and uses 40px rows without the blue gradient labels. The current page and its section are marked, "Appunti" gets the external arrow, and the language and theme buttons, which only logged to the console, are removed. The logo is no longer an h1: the page Hero is, so every route has exactly one. Co-Authored-By: Claude Opus 5.5 * fix(header): close the mobile menu on every link and keep focus in sight - The menu closes when any link in it is followed, the current page and the logo included, when tapping outside it and when focus leaves it. Before, Back reopened it and the current-page link left it open. - Escape returns focus to the toggle instead of dropping it on . - Menu links get the site's focus ring: the active item was already underlined, so focusing it changed nothing. The desktop dropdown links get an outline for the same reason. - The open menu's scroll no longer moves the page underneath. - Glass forwards a ref. --------- Co-authored-by: Claude Opus 5.5 Co-authored-by: Bianca Roberta Ianosel --- src/app/layout.tsx | 3 +- src/components/glass.tsx | 2 +- src/components/header/constants.ts | 10 +- src/components/header/desktop-layout.tsx | 87 +++++++++--------- src/components/header/icon-buttons.tsx | 30 ------ src/components/header/logo.tsx | 10 +- src/components/header/mobile-layout.tsx | 111 ++++++++++++++++------- src/components/ui/hero.tsx | 6 +- 8 files changed, 141 insertions(+), 118 deletions(-) delete mode 100644 src/components/header/icon-buttons.tsx diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 62e5cb9c..be1ce1fd 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -74,9 +74,10 @@ export default function RootLayout({ children }: Readonly<{ children: React.Reac // storageKey="polinetwork_darkmode_temp" // TODO: enable when dark mode design is ready disableTransitionOnChange > + {/* Outside the page wrapper: inside it, Chromium did not blur the content behind the fixed header's glass */} +
    -
    {children}
    diff --git a/src/components/glass.tsx b/src/components/glass.tsx index 6be30a1b..dfae2089 100644 --- a/src/components/glass.tsx +++ b/src/components/glass.tsx @@ -1,6 +1,6 @@ import { cn } from "@/lib/utils" -type GlassProps = React.HTMLAttributes +type GlassProps = React.ComponentProps<"div"> export function Glass({ children, ...props }: GlassProps) { return ( diff --git a/src/components/header/constants.ts b/src/components/header/constants.ts index 7cb314e9..e664983c 100644 --- a/src/components/header/constants.ts +++ b/src/components/header/constants.ts @@ -1,4 +1,4 @@ -import { FiChevronRight } from "react-icons/fi" +import { FiArrowUpRight, FiChevronRight } from "react-icons/fi" import type { HeaderMenuItem } from "./types" export const headerMenuItems: HeaderMenuItem[] = [ @@ -6,7 +6,8 @@ export const headerMenuItems: HeaderMenuItem[] = [ title: "Resources", menu: [ { title: "Progetti", href: "/projects", icon: FiChevronRight }, - { title: "Appunti", href: "https://polinetwork.org/it/materials/view/", icon: FiChevronRight }, + // Leaves the site: ↗ instead of ›, as in the rest of the site + { title: "Appunti", href: "https://polinetwork.org/it/materials/view/", icon: FiArrowUpRight }, { title: "Guide", href: "/guides", icon: FiChevronRight }, { title: "FAQs", href: "/faqs", icon: FiChevronRight }, ], @@ -36,3 +37,8 @@ export const headerMenuItems: HeaderMenuItem[] = [ ], }, ] + +/** The page is the item itself or one of its subpages (e.g. a project under `/projects`). */ +export function isActiveHref(pathname: string, href: string) { + return href.startsWith("/") && (pathname === href || pathname.startsWith(`${href}/`)) +} diff --git a/src/components/header/desktop-layout.tsx b/src/components/header/desktop-layout.tsx index f4a80071..379e5d8c 100644 --- a/src/components/header/desktop-layout.tsx +++ b/src/components/header/desktop-layout.tsx @@ -1,6 +1,7 @@ "use client" import Link from "next/link" +import { usePathname } from "next/navigation" import { FiChevronDown } from "react-icons/fi" import { Glass } from "@/components/glass" import { @@ -12,13 +13,16 @@ import { NavigationMenuTrigger, } from "@/components/ui/navigation-menu" import { cn } from "@/lib/utils" -import { headerMenuItems } from "./constants" -import { IconButtonsDesktop } from "./icon-buttons" +import { headerMenuItems, isActiveHref } from "./constants" import { Logo } from "./logo" export const DesktopLayout = ({ logoOnly = false }: { logoOnly?: boolean }) => { + const pathname = usePathname() const removeDefaultHoverEffectClass = "hover:bg-transparent focus:bg-transparent bg-transparent!" - const customHoverEffectClass = "hover:underline focus-visible:underline decoration-1 decoration-blue-secondary " + const customHoverEffectClass = + "hover:underline decoration-1 decoration-blue-secondary focus-visible:outline-2 focus-visible:outline-solid focus-visible:outline-blue-secondary" + // The current page, and the section that contains it, keep the hover underline + const activeClass = "underline decoration-1 decoration-blue-secondary underline-offset-4" return (
    @@ -40,13 +44,14 @@ export const DesktopLayout = ({ logoOnly = false }: { logoOnly?: boolean }) => { {!logoOnly && headerMenuItems.map((item) => ( - {"menu" in item && item.menu ? ( + {"menu" in item ? ( <> isActiveHref(pathname, subItem.href)) && activeClass )} hideChevron onPointerEnter={(e) => e.preventDefault()} @@ -63,45 +68,47 @@ export const DesktopLayout = ({ logoOnly = false }: { logoOnly?: boolean }) => { -
      - - {"menu" in item && - item.menu.map((subItem) => { - const Icon = subItem.icon - return ( -
    • - - -
      - - {subItem.title} - -
      - - -
      -
    • - ) - })} -
      -
    + {/* Stronger blur than the header pill: the panel opens over the page titles */} + +
      + {item.menu.map((subItem) => { + const Icon = subItem.icon + const active = isActiveHref(pathname, subItem.href) + return ( +
    • + + + + {subItem.title} + + + + +
    • + ) + })} +
    +
    ) : ( - + {item.title} @@ -110,12 +117,6 @@ export const DesktopLayout = ({ logoOnly = false }: { logoOnly?: boolean }) => { )}
    ))} - - {!logoOnly && ( - - - - )}
    diff --git a/src/components/header/icon-buttons.tsx b/src/components/header/icon-buttons.tsx deleted file mode 100644 index ed28131e..00000000 --- a/src/components/header/icon-buttons.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { FiGlobe, FiMoon } from "react-icons/fi" -import { NavigationMenuLink } from "@/components/ui/navigation-menu" -import { cn } from "@/lib/utils" - -const iconConfigs = [ - { key: "globe", label: "Language", Icon: FiGlobe, onClick: () => console.log("Globe icon clicked") }, - { key: "moon", label: "Theme", Icon: FiMoon, onClick: () => console.log("Moon icon clicked") }, -] - -export const IconButtonsMobile = () => ( - <> - {iconConfigs.map(({ key, label, Icon, onClick }) => ( - - ))} - -) - -export const IconButtonsDesktop = ({ removeHoverClass }: { removeHoverClass: string }) => ( - <> - {iconConfigs.map(({ key, label, Icon, onClick }) => ( - - - - ))} - -) diff --git a/src/components/header/logo.tsx b/src/components/header/logo.tsx index 83cd41c8..e89dba06 100644 --- a/src/components/header/logo.tsx +++ b/src/components/header/logo.tsx @@ -4,13 +4,17 @@ import Link from "next/link" export const Logo = ({ href = "/" }: { href?: string }) => { const content = ( <> - PoliNetwork Logo -

    PoliNetwork

    + + {/* Not a heading: every page has its own h1, the logo repeats on all of them */} + PoliNetwork ) return ( - + {content} ) diff --git a/src/components/header/mobile-layout.tsx b/src/components/header/mobile-layout.tsx index ff9fe72a..15455419 100644 --- a/src/components/header/mobile-layout.tsx +++ b/src/components/header/mobile-layout.tsx @@ -1,67 +1,108 @@ "use client" import Link from "next/link" -import { Fragment, useEffect, useState } from "react" +import { usePathname } from "next/navigation" +import { useEffect, useRef, useState } from "react" import { FiMenu, FiX } from "react-icons/fi" import { Glass } from "@/components/glass" import { cn } from "@/lib/utils" -import { headerMenuItems } from "./constants" -import { IconButtonsMobile } from "./icon-buttons" +import { headerMenuItems, isActiveHref } from "./constants" import { Logo } from "./logo" export const MobileLayout = ({ logoOnly = false }: { logoOnly?: boolean }) => { - const [mobileMenuOpen, setMobileMenuOpen] = useState(false) + const pathname = usePathname() + const [openOn, setOpenOn] = useState(null) + const panelRef = useRef(null) + const toggleRef = useRef(null) + // Tied to the page it was opened on, so any navigation closes it: this component persists across pages + const mobileMenuOpen = !logoOnly && openOn === pathname + const close = () => setOpenOn(null) - // logoOnly routes hide the toggle/nav entirely; close the menu so it doesn't reappear open - // when navigating back to a normal route (this component instance persists across navigation). useEffect(() => { - if (logoOnly) setMobileMenuOpen(false) - }, [logoOnly]) + if (!mobileMenuOpen) return + const onKeyDown = (event: KeyboardEvent) => { + if (event.key !== "Escape") return + setOpenOn(null) + toggleRef.current?.focus() + } + const onPointerDown = (event: PointerEvent) => { + if (!panelRef.current?.contains(event.target as Node)) setOpenOn(null) + } + document.addEventListener("keydown", onKeyDown) + document.addEventListener("pointerdown", onPointerDown) + return () => { + document.removeEventListener("keydown", onKeyDown) + document.removeEventListener("pointerdown", onPointerDown) + } + }, [mobileMenuOpen]) return ( - + (event.target as HTMLElement).closest("a") && close()} + onBlur={(event) => !event.currentTarget.contains(event.relatedTarget) && close()} + >
    {!logoOnly && ( )}
    - {!logoOnly && mobileMenuOpen && ( - )}
    diff --git a/src/components/ui/hero.tsx b/src/components/ui/hero.tsx index 1059c361..6b972cf6 100644 --- a/src/components/ui/hero.tsx +++ b/src/components/ui/hero.tsx @@ -36,8 +36,8 @@ export function Hero({ }) { return (
    - {/* Gradient drawn per line (box-decoration-clone), so a wrapped title keeps the full effect on each line */} -

    + {/* The page's h1: the header logo is not a heading. The gradient is drawn per line (box-decoration-clone) */} +

    {/* Capped so the longest word fits the width, since a word can't wrap (DM Sans is about 0.55em per letter) */} {title} -

    +

    Date: Sat, 31 Oct 2026 11:11:51 +0000 Subject: [PATCH 18/22] feat(footer): rebuild the footer on the designers' Figma exploration The footer was the official component's bare layout: a 57px blue gradient title, 24px icon targets, two link columns ~320px apart and three "#" policy links. It now follows "footer-francesca" (593:2123), the version used across the Figma pages: "Keep in touch!" with the social pill, a site map with the association's documents and the 5x1000 card, the contact addresses, the join and report-a-bug calls to action, and a hairline legal row. Only existing content is used: no TikTok, no language selector, and the Statuto and Regolamento interno in place of policy pages that don't exist. The Su di noi support section gets an anchor for the 5x1000 card. Co-Authored-By: Claude Opus 5.5 --- src/components/chi-siamo/support-section.tsx | 3 +- src/components/footer/index.tsx | 240 +++++++++++++------ 2 files changed, 168 insertions(+), 75 deletions(-) diff --git a/src/components/chi-siamo/support-section.tsx b/src/components/chi-siamo/support-section.tsx index 506a90fd..78d5560c 100644 --- a/src/components/chi-siamo/support-section.tsx +++ b/src/components/chi-siamo/support-section.tsx @@ -2,7 +2,8 @@ import { CONTAINER, SECTION_TITLE } from "@/components/layout/page" export function SupportSection() { return ( -

    + // `id`: the footer's 5x1000 card links here +

    Sostieni PoliNetwork

    diff --git a/src/components/footer/index.tsx b/src/components/footer/index.tsx index 58568b40..e04233e3 100644 --- a/src/components/footer/index.tsx +++ b/src/components/footer/index.tsx @@ -1,96 +1,188 @@ import Image from "next/image" import Link from "next/link" -import { FiFacebook, FiGithub, FiInstagram, FiLinkedin } from "react-icons/fi" +import type { ReactNode } from "react" +import { FiArrowUpRight, FiFacebook, FiGithub, FiInstagram, FiLinkedin } from "react-icons/fi" import discord from "@/assets/icons/discord.svg" import telegram from "@/assets/icons/telegram.svg" -import { CardMultipleIcons } from "../card-multiple-icons" +import { CardMultipleIcons } from "@/components/card-multiple-icons" +import { Glass } from "@/components/glass" +import { CONTAINER } from "@/components/layout/page" +import { Button } from "@/components/ui/button" +import { cn } from "@/lib/utils" + +/* + Built on the designers' footer exploration used across the Figma pages ("footer-francesca", 593:2123): the + "Keep in touch!" header of the official component (5107:12889), then a site map with the 5x1000 card, the contact + addresses and the two calls to action. Only content that exists is shown: no TikTok, no language selector (the site + has no translations), and the association's documents instead of policy pages that don't exist yet. +*/ + +const socials: { label: string; href: string; icon: ReactNode }[] = [ + { label: "Telegram", href: "https://t.me/polinetwork", icon: }, + { label: "Instagram", href: "https://instagram.com/polinetwork_", icon: }, + { label: "LinkedIn", href: "https://www.linkedin.com/company/polinetwork", icon: }, + { label: "Facebook", href: "https://www.facebook.com/PolitecnicoDiMilanoNetwork", icon: }, + // The Discord glyph has a 34px canvas with its own padding: at 28px it matches the optical size of the others + { label: "Discord", href: "https://discord.gg/Fp7ZHcC", icon: }, + { label: "GitHub", href: "https://github.com/PoliNetworkOrg", icon: }, +] + +const siteLinks = [ + { title: "Gruppi", href: "/groups" }, + { title: "Matricole", href: "/matricole" }, + { title: "Progetti", href: "/projects" }, + { title: "Appunti", href: "https://polinetwork.org/it/materials/view/" }, + { title: "Su di noi", href: "/chi-siamo" }, + { title: "Contatti", href: "/contact" }, +] + +// Already linked from /join. They stand in for "Terms", "Privacy" and "Cookie policy", which pointed to "#" +const documents = [ + { title: "Statuto", href: "https://github.com/PoliNetworkOrg/Statuto/blob/main/full.pdf" }, + { + title: "Regolamento interno", + href: "https://github.com/PoliNetworkOrg/RegolamentoInterno/blob/main/RaccoltaRegolamentiInterni.pdf", + }, +] + +// The addresses of the /contact page, with the Figma labels +const emails = [ + { label: "Per collaborazioni ed eventi", email: "events@polinetwork.org" }, + { label: "Per domande e richieste", email: "info@polinetwork.org" }, +] + +const HEADING = "typo-headline-extrasmall text-text-primary" +const LINK = + "rounded-sm outline-none decoration-1 decoration-blue-secondary underline-offset-4 transition-colors hover:text-blue-secondary hover:underline focus-visible:underline focus-visible:ring-2 focus-visible:ring-blue-secondary focus-visible:ring-offset-2" + +function FooterLink({ href, children, className }: { href: string; children: ReactNode; className?: string }) { + const external = !href.startsWith("/") + return external ? ( + // Inline arrow: when the label wraps, it follows the last word instead of floating beside the block + + {children} + + + ) : ( + + {children} + + ) +} export function Footer() { return ( -