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

- Vuoi sviluppare un progetto insieme a noi? -

+
+
+

Vuoi sviluppare un progetto insieme a noi?

-
-

+

+

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

-

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

+

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

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

Hai una richiesta specifica?

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

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

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

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

Vuoi sviluppare un progetto insieme a noi?

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

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

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

)} -
- -

Scopri gli altri progetti

-

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

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

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

Esplora la raccolta completa dei progetti

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

Nessun progetto disponibile al momento.

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

- Le novità della community -

-

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

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

Nessuna novità disponibile al momento.

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

+ Le novità della community +

+

Rimani aggiornato sulle idee appena condivise dagli studenti del Politecnico.

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

Progetti deprecati

-

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

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

Nessun progetto deprecato al momento.

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

+ Progetti deprecati +

+

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

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

    Hai un'idea?

    -

    Proponila qui!

    -
    -
    -

    +

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

    + Hai un'idea? +

    +

    Proponila qui!

    +
    +

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

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

    -
    -
    -

    +

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

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

    Il Direttivo in carica

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

-
+

Come funziona l’associazione

-
+

Trova il tuo posto in PoliNetwork

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

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

- +

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

{caption}

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

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

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

+

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

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

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

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

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

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

Esplora la raccolta completa dei progetti

- {projects.length > 0 && ( + {searchable.length > 0 && ( } type="search"