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/about/page.tsx b/src/app/about/page.tsx index 8cab8cbf..34afdc94 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, 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" @@ -15,18 +17,16 @@ 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 titleClass = `${SECTION_TITLE} text-center` export default function StructurePage() { return ( -
-
+
+
-
+

Il Direttivo in carica

@@ -37,33 +37,31 @@ export default function StructurePage() {

-
+

Come funziona l’associazione

-
+

Trova il tuo posto in PoliNetwork

-
-

- 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/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/app/chi-siamo/page.tsx b/src/app/chi-siamo/page.tsx index 0fdae581..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 */} -
+
+
- +
+
+ +
diff --git a/src/app/faqs/page.tsx b/src/app/faqs/page.tsx index 61037c16..8666c0c4 100644 --- a/src/app/faqs/page.tsx +++ b/src/app/faqs/page.tsx @@ -1,6 +1,8 @@ import type { Metadata } from "next" import { FaqsTabs } from "@/components/faqs/faqs-tabs" +import { CONTAINER, PAGE_HERO_TOP, PAGE_MAIN } from "@/components/layout/page" import { Hero } from "@/components/ui/hero" +import { cn } from "@/lib/utils" import { getAllFaqs } from "@/queries/faqs" export const metadata: Metadata = { @@ -14,9 +16,12 @@ export default async function FAQsPage() { const categories = await getAllFaqs() return ( -
- - +
+ {/* The tabs belong to the hero, so they follow it without the usual break after the hero */} +
+ + +
) } 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/app/guides/page.tsx b/src/app/guides/page.tsx index e9d039f3..20742968 100644 --- a/src/app/guides/page.tsx +++ b/src/app/guides/page.tsx @@ -1,6 +1,8 @@ import type { Metadata } from "next" import { FiBook, FiFileText } from "react-icons/fi" import { CardIcon } from "@/components/card-icon" +import { CONTAINER, PAGE_HERO, PAGE_MAIN } from "@/components/layout/page" +import { Hero } from "@/components/ui/hero" const guides = [ { @@ -26,15 +28,14 @@ export const metadata: Metadata = { export default function GuidesHome() { return ( -
-
-
-

- Guide -

-
+
+
+ +
-
+ {/* Two equal paths: same height, side by side from sm, and the whole card is the link */} +
+
{guides.map((guide) => ( ))}
-
+
) } diff --git a/src/app/join/page.tsx b/src/app/join/page.tsx index c22fc5c4..dd56b8da 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, PAGE_HERO, PAGE_MAIN } 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 ( -
-
+
+
) { return ( @@ -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/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/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/app/not-found.tsx b/src/app/not-found.tsx index 532b7291..f56d292b 100644 --- a/src/app/not-found.tsx +++ b/src/app/not-found.tsx @@ -12,15 +12,15 @@ export default function NotFound() {
-
+

404

-

+

Non trovo quello che cerchi.
- Controlla l'URL o visita un'altra sezione del sito. + Controlla l'URL o visita un'altra sezione del sito.

- -
+ } + />
) } 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/app/storia/page.tsx b/src/app/storia/page.tsx index dc4fc1cf..4d6edfc0 100644 --- a/src/app/storia/page.tsx +++ b/src/app/storia/page.tsx @@ -1,8 +1,7 @@ import type { Metadata } from "next" -import { FiMessageCircle, FiUserCheck } 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 { FiMessageCircle } from "react-icons/fi" +import { GradientIcon } from "@/components/gradient-icon" +import { CONTAINER, PAGE_MAIN, SECTION_LEAD, SECTION_TITLE } from "@/components/layout/page" import TextOnlyLayout from "@/components/layout/TextOnlySection" import { Boards } from "@/components/storia/boards" import { Continuation } from "@/components/storia/continuation" @@ -11,6 +10,7 @@ import { Organization } from "@/components/storia/organization" import { HistoryProjects } from "@/components/storia/projects" import { Thanks } from "@/components/storia/thanks" import { Chapters, Timeline } from "@/components/storia/timeline" +import { cn } from "@/lib/utils" export const metadata: Metadata = { title: "La nostra storia", @@ -20,103 +20,76 @@ export const metadata: Metadata = { export default function HistoryPage() { return ( -
+
-
- -

- 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: "mx-auto max-w-330 gap-12 px-6 md:px-6 xl:flex-row xl:items-center [&>div>h2]:typo-title-large", - textDiv: "min-w-0 xl:w-1/2 xl:items-start", - title: "text-balance text-blue-secondary xl:text-start", - descriptionDiv: "xl:text-start", - contentDiv: "min-w-0 xl:w-1/2", - }} - > -
- - -
-
- - - - - + +

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: "mx-auto max-w-330 gap-12 px-6 md:px-6 xl:flex-row xl:items-center [&>div>h2]:typo-title-large", - title: "text-balance text-blue-secondary xl:text-start", - textDiv: "min-w-0 xl:w-1/2 xl:items-start", - descriptionDiv: "xl:text-start", - contentDiv: "min-w-0 xl:w-1/2", - }} - > - - Se anche un solo studente ha trovato casa grazie a un nostro gruppo, o si è laureato in tempo perché - qualcuno gli ha risposto. -

- } - className="h-auto w-full gap-6 p-6 sm:p-10" - /> -
+ +

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. +

+

+ 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 df966574..4f0dcd34 100644 --- a/src/app/team/page.tsx +++ b/src/app/team/page.tsx @@ -1,6 +1,7 @@ import type { Metadata } from "next" import { FiUsers } from "react-icons/fi" -import { Glass } from "@/components/glass" +import { ClosingCta } from "@/components/layout/closing-cta" +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" @@ -13,34 +14,28 @@ export const metadata: Metadata = { export default function TeamPage() { return ( -
-
+
+
- {teams.map((team) => ( - + {teams.map((team, index) => ( + ))} -
- -

- Vuoi fare la differenza? -

-

- Le porte di PoliNetwork sono aperte a tutti gli studenti del Politecnico di Milano. Segui i nostri social - per non perdere i periodi di recruiting ufficiali… ma se hai talento e motivazione, non aspettare: valutiamo - le candidature spontanee tutto l’anno. -

+ Unisciti a noi -
-
+ } + />
) } 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/accordion-list/accordion-content.tsx b/src/components/accordion-list/accordion-content.tsx index 7db25ef2..03d7793a 100644 --- a/src/components/accordion-list/accordion-content.tsx +++ b/src/components/accordion-list/accordion-content.tsx @@ -13,7 +13,8 @@ export function AccordionContent({ className="overflow-hidden data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down" {...props} > -
{children}
+ {/* Answers stop at a readable line length (~70 characters) however wide the list is */} +
{children}
) } diff --git a/src/components/accordion-list/accordion-item.tsx b/src/components/accordion-list/accordion-item.tsx index 12c68034..2d9da03e 100644 --- a/src/components/accordion-list/accordion-item.tsx +++ b/src/components/accordion-list/accordion-item.tsx @@ -6,7 +6,12 @@ import { cn } from "@/lib/utils" export function AccordionItem({ className, children, ...props }: React.ComponentProps) { return ( - + {children} diff --git a/src/components/accordion-list/accordion-trigger.tsx b/src/components/accordion-list/accordion-trigger.tsx index 061d6749..0b191b17 100644 --- a/src/components/accordion-list/accordion-trigger.tsx +++ b/src/components/accordion-list/accordion-trigger.tsx @@ -14,7 +14,9 @@ export function AccordionTrigger({ data-slot="accordion-trigger" className={cn( "flex w-full items-center justify-between gap-4 p-6 text-left", - "typo-title-medium text-blue-secondary outline-none", + "typo-title-medium text-blue-secondary outline-none transition-colors", + // The header is the only clickable part: it alone reacts to hover and keyboard focus + "hover:bg-white/40 focus-visible:ring-2 focus-visible:ring-blue-secondary focus-visible:ring-inset", "[&[data-state=open]>svg]:rotate-90", className )} diff --git a/src/components/associations/associations-list.tsx b/src/components/associations/associations-list.tsx index 847a39a3..d22670e6 100644 --- a/src/components/associations/associations-list.tsx +++ b/src/components/associations/associations-list.tsx @@ -12,8 +12,8 @@ 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" -import esnLogo from "../../../public/logos/esn.svg" type Association = ApiOutput["web"]["associations"]["getAllAssociations"][number] type AssociationLinks = Association["links"] @@ -33,13 +33,15 @@ 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 +49,5 @@ export function AssociationsList({ associations }: { associations: Association[] }), })) - return ( -
- -
- ) + return } diff --git a/src/components/card-caption.tsx b/src/components/card-caption.tsx index 0f04b101..70468f5c 100644 --- a/src/components/card-caption.tsx +++ b/src/components/card-caption.tsx @@ -1,6 +1,8 @@ +import Image from "next/image" 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({ @@ -8,26 +10,52 @@ 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 ( - - {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 && } + + {/* 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}
+ + {typeof caption === "string" ?

{caption}

: caption} +
+ {footer &&
{footer}
}
) } 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/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 ea197258..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" @@ -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/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/card-multiple-icons/index.tsx b/src/components/card-multiple-icons/index.tsx index 3c05e7f5..56e04def 100644 --- a/src/components/card-multiple-icons/index.tsx +++ b/src/components/card-multiple-icons/index.tsx @@ -1,5 +1,4 @@ import * as React from "react" -import { Glass } from "@/components/glass" import { cn } from "@/lib/utils" import type { CardMultipleIconsProps } from "./types" @@ -7,14 +6,14 @@ export function CardMultipleIcons({ icons, className }: CardMultipleIconsProps) const iconItems = React.Children.toArray(icons) return ( - - {/* Figma "glass-icone": 48px tall, icons 34px from the edges and evenly spread */} -
+
{iconItems.map((icon) => ( ))}
- +
) } 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 && ( 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..78d5560c 100644 --- a/src/components/chi-siamo/support-section.tsx +++ b/src/components/chi-siamo/support-section.tsx @@ -1,31 +1,26 @@ -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 -

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

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..51fb6be4 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, PAGE_HERO, 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/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 5c413a61..b9c4bb7c 100644 --- a/src/components/collabora/info-card.tsx +++ b/src/components/collabora/info-card.tsx @@ -1,5 +1,7 @@ import Image, { type StaticImageData } from "next/image" +import Link from "next/link" import type { ReactNode } from "react" +import { CardHoverBackground } from "@/components/card-icon/hover-background" import { Glass } from "@/components/glass" import { cn } from "@/lib/utils" @@ -14,6 +16,8 @@ export type InfoCardData = { iconClassName: string /** Desktop description width: Figma wraps these copies at fixed widths (+1px, as Figma rounds text widths down). */ descriptionClassName?: string + /** Makes the whole card a link: only then does it react to hover. */ + href?: string } /** @@ -23,32 +27,50 @@ 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 } = card + const { title, description, mobileDescription, icon, iconClassName, descriptionClassName, href } = card return ( // Glass draws a 1px border inside the Figma box: padding is 1px less than in Figma (25px / 40px) - -
+ a:focus-visible]:ring-2 has-[>a:focus-visible]:ring-blue-secondary", + className + )} + > + {href && } + {href && } + {/* Positioned so the text stays above the hover background */} +
{/* On the narrowest phones an email title may not fit: let it wrap after the "@" */} -

+

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

{mobileDescription !== undefined && ( -

{mobileDescription}

+

{mobileDescription}

)}

+

-
- {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/contact-card.tsx b/src/components/contact-card.tsx index 742a8160..716db936 100644 --- a/src/components/contact-card.tsx +++ b/src/components/contact-card.tsx @@ -9,12 +9,12 @@ export type ContactCardData = { export function ContactCard({ title, description, email }: ContactCardData) { return ( - + - {title} + {title} {description} - + {email} diff --git a/src/components/contact/collabora-section.tsx b/src/components/contact/collabora-section.tsx index e035b39e..12dd95c3 100644 --- a/src/components/contact/collabora-section.tsx +++ b/src/components/contact/collabora-section.tsx @@ -1,27 +1,24 @@ import Link from "next/link" import { FiArrowUpRight } from "react-icons/fi" +import { CONTAINER, SECTION_LEAD, SECTION_TITLE } from "@/components/layout/page" import { Button } from "@/components/ui/button" export function CollaboraSection() { return ( -
-
-

- Vuoi sviluppare un progetto insieme a noi? -

+
+
+

Vuoi sviluppare un progetto insieme a noi?

-
-

+

+

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

-

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

+

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

-
-
- Foto di gruppo della community PoliNetwork -
+ Foto di gruppo della community PoliNetwork
) } diff --git a/src/components/faqs/faqs-tabs.tsx b/src/components/faqs/faqs-tabs.tsx index ea36a7e4..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" @@ -27,20 +28,33 @@ export function FaqsTabs({ categories }: { categories: FaqCategory[] }) { accordionItems: category.faqs.map((faq) => ({ value: String(faq.faqId), trigger: faq.titleIt, - content: faq.descriptionIt, + // Answers are plain text: keep the line breaks the editors typed + content:

{faq.descriptionIt}

, })), })) + // Open on a category that has answers + const defaultTab = tabs.find((tab) => tab.accordionItems.length > 0) ?? tabs[0] return ( -
+
{tabs.length === 0 ? ( -

Nessuna FAQ disponibile al momento.

+

Nessuna FAQ disponibile al momento.

) : ( - - + + {/* With several categories the strip scrolls: on phones it runs to the screen edges */} + {tabs.length > 1 && ( + + )} {tabs.map((tab) => ( - + {tab.accordionItems.length > 0 ? ( + + ) : ( +

Non ci sono ancora domande in questa categoria.

+ )}
))}
diff --git a/src/components/footer/index.tsx b/src/components/footer/index.tsx index 58568b40..b71ff3e9 100644 --- a/src/components/footer/index.tsx +++ b/src/components/footer/index.tsx @@ -1,97 +1,178 @@ 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 { MATERIALS_URL } from "@/components/header/constants" +import { CONTAINER, SECTION_TITLE } from "@/components/layout/page" +import { regulationsHref, statuteHref } from "@/components/structure/constants" +import { Button } from "@/components/ui/button" +import { cn } from "@/lib/utils" + +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 its own padding: at 28px it matches 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: MATERIALS_URL }, + { title: "Su di noi", href: "/chi-siamo" }, + { title: "Contatti", href: "/contact" }, +] + +const documents = [ + { title: "Statuto", href: statuteHref }, + { title: "Regolamento interno", href: regulationsHref }, +] + +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 + + {children} + + + ) : ( + + {children} + + ) +} export function Footer() { return ( -
-

- Keep in touch! -

+
+ +
+

+ Keep in touch! +

+ ( + // 36px targets below sm keep the row whole at 320px + + {icon} + + ))} + /> +
-
- - - , - - - , - - - , - - - , - - - , - - - , - ]} - /> -
+
+
+ -
-
- Unisciti a noi! - Contattaci - - Qualche problema?
Segnalalo! -
-
-
- Terms & conditions - Privacy policy - Cookie policy + {/* A lighter tile, not a second glass layer inside the panel */} +
+

+ 5x1000 +

+
+ + Sostienici! + +

C.F. 97927490157

+
+
+
+ +
+
+ +
+ {emails.map(({ label, email }) => ( +
+
{label}
+
+ + {email} + +
+
+ ))} +
+
+ +
+
+

Sei interessato?

+ +
+
+

Qualche problema?

+ +
+
+
-
-
- -
+ {/* slate-600 rather than text-secondary (slate-500, 4.3:1 on the page background) so the small print passes AA */} +
+

PoliNetwork APS 2016–{new Date().getFullYear()}

+

All rights reserved

+
+
) } 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/groups/extra-groups.tsx b/src/components/groups/extra-groups.tsx index 6140f2f2..a381cb5c 100644 --- a/src/components/groups/extra-groups.tsx +++ b/src/components/groups/extra-groups.tsx @@ -1,6 +1,6 @@ -import Link from "next/link" -import { FiArrowLeft } from "react-icons/fi" import { CardCourseGroup } from "@/components/card-course-group" +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" @@ -12,22 +12,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..591b4ddd 100644 --- a/src/components/groups/groups-result.tsx +++ b/src/components/groups/groups-result.tsx @@ -1,7 +1,7 @@ -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 { 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" @@ -21,7 +21,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 +40,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 +100,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) => ( @@ -155,11 +137,7 @@ export async function GroupsResult({ /> ))} - {!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/header/constants.ts b/src/components/header/constants.ts index 7cb314e9..b92aac9a 100644 --- a/src/components/header/constants.ts +++ b/src/components/header/constants.ts @@ -1,12 +1,15 @@ -import { FiChevronRight } from "react-icons/fi" +import { FiArrowUpRight, FiChevronRight } from "react-icons/fi" import type { HeaderMenuItem } from "./types" +/** The materials archive, on the old site. */ +export const MATERIALS_URL = "https://polinetwork.org/it/materials/view/" 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: MATERIALS_URL, icon: FiArrowUpRight }, { title: "Guide", href: "/guides", icon: FiChevronRight }, { title: "FAQs", href: "/faqs", icon: FiChevronRight }, ], @@ -36,3 +39,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/home/about-us.tsx b/src/components/home/about-us.tsx index f879f913..9011e323 100644 --- a/src/components/home/about-us.tsx +++ b/src/components/home/about-us.tsx @@ -4,13 +4,13 @@ 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/materials.tsx b/src/components/home/materials.tsx index 102a8a10..146f16d7 100644 --- a/src/components/home/materials.tsx +++ b/src/components/home/materials.tsx @@ -1,83 +1,96 @@ 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 { MATERIALS_URL } from "@/components/header/constants" +import { CONTAINER } from "@/components/layout/page" import { Button } from "@/components/ui/button" +import { cn } from "@/lib/utils" import { CardCaption } from "../card-caption" +// "Carica" has no destination yet, so it stays a plain card 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 +] -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 +// What the archive holds. Not links: the archive has no per-type pages to open +const archiveTypes = [ + { 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) => ( - - ))} -
- -
- {quickLinks.map((card) => ( - - ))} -
-
+
+

+ Materials +

-
-

- 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.

+ +
+
+ {featuredCards.map((card) => ( + + + + + ))} +
+ +
+ {archiveTypes.map((card) => ( + + ))} +
+
) } diff --git a/src/components/home/projects.tsx b/src/components/home/projects.tsx index b66fafcc..99ec6578 100644 --- a/src/components/home/projects.tsx +++ b/src/components/home/projects.tsx @@ -1,8 +1,10 @@ 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 { Carousel, CarouselContent, CarouselDots, CarouselItem } from "../ui/carousel" +import { cn } from "@/lib/utils" const featuredCards = [ { @@ -29,14 +31,36 @@ const featuredCards = [ }, ] as const +function FeaturedProjectCard({ 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,35 +70,25 @@ export function Projects() { l’innovazione. Hai un’idea? Proponila, trova collaboratori e realizza la tua soluzione.

-
-
- {featuredCards.map((card) => ( - - ))} -
+
+ {featuredCards.map((card) => ( + + ))}
-
- - - {featuredCards.map((card) => ( - -
- -
-
- ))} -
- -
-
+ card.title} + render={(card) => } + className="pt-6 sm:hidden" + />
) } diff --git a/src/components/join/admin.tsx b/src/components/join/admin.tsx index a41f66c4..12eb0332 100644 --- a/src/components/join/admin.tsx +++ b/src/components/join/admin.tsx @@ -2,7 +2,9 @@ import Image from "next/image" 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 { Highlight } from "@/components/collabora/section" +import { CONTAINER, SECTION_LEAD, SECTION_TITLE } from "@/components/layout/page" +import { cn } from "@/lib/utils" import { JoinCards } from "./cards" import { adminCards, adminSteps } from "./constants" @@ -10,11 +12,11 @@ const connectors = [connectorFirst, connectorSecond] export function AdminSection() { return ( -
+
-
- Diventa Admin -
+
+

Diventa Admin

+

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

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

-

+

Ti interessano questi compiti?

    @@ -39,7 +41,8 @@ export function AdminSection() {
  1. {index < connectors.length && ( {cards.map((card) => ( - + ))}
) @@ -17,12 +18,13 @@ 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 c464bcec..71aa648c 100644 --- a/src/components/join/introduction.tsx +++ b/src/components/join/introduction.tsx @@ -1,21 +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" - -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" }, -] +import { Highlight } from "@/components/collabora/section" +import { CONTAINER, INLINE_LINK, SECTION_LEAD, SECTION_TITLE } from "@/components/layout/page" +import { cn } from "@/lib/utils" 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.

@@ -32,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 0d7a82ee..0e1a8f25 100644 --- a/src/components/join/membership.tsx +++ b/src/components/join/membership.tsx @@ -1,23 +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 { 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{" "} @@ -43,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/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/flow-header.tsx b/src/components/layout/flow-header.tsx new file mode 100644 index 00000000..6f86ccc6 --- /dev/null +++ b/src/components/layout/flow-header.tsx @@ -0,0 +1,49 @@ +import Link from "next/link" +import { FiArrowLeft, FiX } from "react-icons/fi" +import { CONTAINER } from "@/components/layout/page" +import { cn } from "@/lib/utils" + +/** 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" + +/** + * 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 FlowHeader({ + 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/layout/page.ts b/src/components/layout/page.ts new file mode 100644 index 00000000..fcde2497 --- /dev/null +++ b/src/components/layout/page.ts @@ -0,0 +1,33 @@ +/** 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" + +/** 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` + +/** 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/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/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..6429d5f8 --- /dev/null +++ b/src/components/projects/project-list.tsx @@ -0,0 +1,71 @@ +"use client" + +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" +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) => ( + + ))} +
    ) 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..172cd87e 100644 --- a/src/components/storia/hero.tsx +++ b/src/components/storia/hero.tsx @@ -1,76 +1,29 @@ -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, PAGE_HERO } 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" }, -] 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..2ea436e6 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" @@ -14,21 +13,20 @@ export function AssociationBodies() { {associationBodies.map((body) => ( - + {body.title} - - {body.description} - + {body.description} ))}
    {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/tabs/tabs-trigger.tsx b/src/components/tabs/tabs-trigger.tsx index ba7cd23c..57921cd5 100644 --- a/src/components/tabs/tabs-trigger.tsx +++ b/src/components/tabs/tabs-trigger.tsx @@ -8,17 +8,13 @@ export function TabsTrigger({ className, children, ...props }: React.ComponentPr - ) } 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) => ( -
    • - -
    • - ))} -
    +
    ) } 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} diff --git a/src/components/ui/carousel.tsx b/src/components/ui/carousel.tsx index e6bc6262..b2cce1a9 100644 --- a/src/components/ui/carousel.tsx +++ b/src/components/ui/carousel.tsx @@ -177,8 +177,10 @@ 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", + selectedIndex === index + ? "size-4 border-blue-primary bg-blue-primary" + : "size-3 border-slate-500 bg-background-blur" )} /> ))} diff --git a/src/components/ui/hero.tsx b/src/components/ui/hero.tsx index bcd563e3..6b972cf6 100644 --- a/src/components/ui/hero.tsx +++ b/src/components/ui/hero.tsx @@ -17,32 +17,42 @@ const descriptionGradientStyle = { "--hero-gradient-desktop": DESCRIPTION_GRADIENT_DESKTOP, } as CSSProperties +function longestWord(text: string) { + return Math.max(...text.split(/\s+/).map((word) => word.length)) +} + export function Hero({ title, description, gradientDescription = false, className, + descriptionClassName, }: { title: string description: string gradientDescription?: boolean className?: string + descriptionClassName?: string }) { return (
    - {/* Figma "lightheme-XLarge text effect": #020618 → #6e6e6e at 82.69% */} - {/* The gradient is drawn per line (box-decoration-clone), so a title that wraps keeps the full effect on each line */} -

    - + {/* 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} -

    +

    {description} diff --git a/src/components/ui/item.tsx b/src/components/ui/item.tsx index 6e9fe15d..89c6a3c7 100644 --- a/src/components/ui/item.tsx +++ b/src/components/ui/item.tsx @@ -30,7 +30,7 @@ function ItemTitle({ className, ...props }: React.ComponentProps<"p">) { return (

    ) diff --git a/src/components/wizard/course-step.tsx b/src/components/wizard/course-step.tsx index 5f187649..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,15 +77,17 @@ 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) const courseLanguage = languageFacet(facets) return ( - + 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/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) { 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/src/styles/typography.css b/src/styles/typography.css index 8daa9847..e85c5d0d 100644 --- a/src/styles/typography.css +++ b/src/styles/typography.css @@ -21,6 +21,21 @@ @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. + 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.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.2] tracking-[-0.02em]; +} + /* Headline */ @utility typo-headline-large { @apply font-dm-sans font-normal text-[2rem]/[40px] tracking-normal; 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(", ")}`) +})