-
+
Come usarlo
-
+
{resources.length > 1
? "Scegli la versione più adatta a te."
: "Apri il progetto e segui le istruzioni per iniziare."}
@@ -141,21 +134,17 @@ export default async function ProjectPage({ params }: Props) {
)}
-
-
-
- Scopri gli altri progetti
-
-
- Strumenti e app realizzati dagli studenti del Politecnico, per gli studenti del Politecnico.
-
+
Tutti i progetti
-
-
+ }
+ />
)
}
diff --git a/src/app/projects/page.tsx b/src/app/projects/page.tsx
index fbfb0d74..25e85bd6 100644
--- a/src/app/projects/page.tsx
+++ b/src/app/projects/page.tsx
@@ -1,7 +1,9 @@
+import { CONTAINER, PAGE_HERO, PAGE_MAIN } from "@/components/layout/page"
import { Collection } from "@/components/projects/collection"
import { CommunityNews } from "@/components/projects/community-news"
import { Deprecated } from "@/components/projects/deprecated"
import { Upload } from "@/components/projects/upload"
+import { Hero } from "@/components/ui/hero"
import { getAllProjects } from "@/queries/projects"
export const metadata = {
@@ -11,7 +13,7 @@ export const metadata = {
export const dynamic = "force-dynamic"
-export default async function Home() {
+export default async function ProjectsPage() {
const projects = await getAllProjects()
const news = projects.filter((project) => project.category === "news")
@@ -19,9 +21,12 @@ export default async function Home() {
const deprecated = projects.filter((project) => project.category === "deprecated")
return (
-
+
+
-
+
diff --git a/src/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}
-
+
+
+ {/* 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 */}
+
)
@@ -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}
>
)}
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}
-
+
{block.cta}
-
+
))}
)
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.
-
-
-
- Scopri cosa facciamo
-
-
-
-
- Entra nel network
-
-
-
)
}
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}
+
-
+
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!
-
+
Collabora con noi
diff --git a/src/components/contact/email-section.tsx b/src/components/contact/email-section.tsx
index dfa71c18..2e67be97 100644
--- a/src/components/contact/email-section.tsx
+++ b/src/components/contact/email-section.tsx
@@ -1,5 +1,6 @@
import Link from "next/link"
import { ContactCard, type ContactCardData } from "../contact-card"
+import { CONTAINER, INLINE_LINK, SECTION_LEAD, SECTION_TITLE } from "../layout/page"
const contactCards: ContactCardData[] = [
{
@@ -18,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
.
>
@@ -31,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.
@@ -43,13 +44,11 @@ const contactCards: ContactCardData[] = [
export function EmailSection() {
return (
-
-
- Hai una richiesta specifica?
-
-
+
+ Hai una richiesta specifica?
+
Per tutto il resto,{" "}
-
+
puoi scriverci direttamente
.
@@ -57,7 +56,8 @@ export function EmailSection() {
Qui sotto trovi i contatti in base al tipo di richiesta.
-
+ {/* 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 37440fcd..f8cf963a 100644
--- a/src/components/contact/groups-section.tsx
+++ b/src/components/contact/groups-section.tsx
@@ -1,40 +1,44 @@
import Image from "next/image"
import Link from "next/link"
import { FiArrowUpRight } from "react-icons/fi"
+import { CONTAINER, INLINE_LINK, SECTION_LEAD, SECTION_TITLE } from "@/components/layout/page"
import { Button } from "@/components/ui/button"
export function GroupsSection() {
return (
-
-
-
+
+
+
Hai mille dubbi sul Poli?
Siamo pronti ad aiutarti!
-
+
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.
-
- Trova il tuo gruppo
-
+
+
+
+ Trova il tuo gruppo
+
+
-
-
-
+
)
}
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}
+
+ ))}
+ />
+
-
+
+
+
+
+
+
+ {siteLinks.map((link) => (
+
+
+ {link.title}
+
+
+ ))}
+
+
+ {documents.map((document) => (
+
+ {document.title}
+
+ ))}
+
+
+
-
-
-
-
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 }) => (
+
+ ))}
+
+
+
+
+
+
Sei interessato?
+
+ Unisciti a noi!
+
+
+
+
+
-
-
-
PoliNetwork 2016-{new Date().getFullYear()} - All rights reserved
-
+ {/* 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 (
-
-
-
- {(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 (
-
-
+
+
- {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
+
+ {/* 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 && (
setMobileMenuOpen(!mobileMenuOpen)}
- className="ml-auto rounded-md focus-visible:ring-2 focus-visible:ring-text-primary"
- aria-label={mobileMenuOpen ? "Close menu" : "Open menu"}
+ onClick={() => setOpenOn(mobileMenuOpen ? null : pathname)}
+ className="-m-2 ml-auto rounded-full p-2 outline-none focus-visible:ring-2 focus-visible:ring-blue-secondary"
+ aria-label={mobileMenuOpen ? "Chiudi il menu" : "Apri il menu"}
+ aria-expanded={mobileMenuOpen}
+ aria-controls="mobile-menu"
>
- {mobileMenuOpen ? : }
+ {mobileMenuOpen ? (
+
+ ) : (
+
+ )}
)}
- {!logoOnly && mobileMenuOpen && (
-
-
- {headerMenuItems.map((item) => (
-
-
- {item.title}
-
- {"menu" in item && item.menu && (
-
- {item.menu.map((subItem) => (
-
+ {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.
-
+
-
+
About us
diff --git a/src/components/home/hero.tsx b/src/components/home/hero.tsx
index 6f28b27a..32d580ac 100644
--- a/src/components/home/hero.tsx
+++ b/src/components/home/hero.tsx
@@ -5,29 +5,30 @@ import { GroupSearch } from "./group-search"
export function Hero() {
return (
-
-
-
+
+
+
Trova gruppi, risorse e supporto tra gli studenti del Polimi
-
+
+
-
-
- More groups
-
-
-
-
-
-
-
-
-
- Sei una matricola?
-
-
+
+
+
+ More groups
+
+
+
+
+
+
+ Sei una matricola?
+
+
+
+
)
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.
-
+
Scopri di più
+
+
+
+ {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.
-
+
Esplora la raccolta
-
-
- {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() {
{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 && (
-
- setExpanded((previous) => !previous)}
- >
- {expanded ? "Mostra di meno" : "Mostra di più"}
- {expanded ? : }
-
-
+
+ {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 && (
-
- setExpanded((previous) => !previous)}
- >
- {expanded ? "Mostra di meno" : "Mostra di più"}
- {expanded ? : }
-
-
- )}
-
-
-
-
-
- {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 && (
+
setExpanded((previous) => !previous)}
+ >
+ {expanded ? "Mostra di meno" : `Mostra tutti (${projects.length})`}
+ {expanded ? : }
+
+ )}
+
+
+ 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.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.
-
-
-
- Unisciti a noi
-
-
-
-
-
- {["2016", "2022", "2026", "?"].map((year) => (
-
-
- {year}
-
-
- ))}
-
-
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) => (
+ 0 && "flex-1")}>
+ {index > 0 && }
+
+ {year}
+
+
+ ))}
+
+
+ Il prossimo capitolo lo scriviamo insieme.
+
+
+
+ Unisciti a noi
+
+
+
)
}
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 (
-
-
-
-
-
-
- Leggi la nostra storia
-
-
-
-
- Conosci le persone
-
-
-
-
-
-
- {portraits.map(({ member, position }) => (
-
-
-
-
- {member.role}
-
-
- ))}
-
-
-
-
-
200+
-
admin volontari, oggi
-
-
+
+
+
+
+
+ Leggi la nostra storia
+
+
+
+
+ Conosci le persone
+
+
)
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.
@@ -190,15 +21,8 @@ export function Organization() {
-
-
-
-
+ {/* 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",
}}
>
-
-
+ {/* 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.
-
-
+
+
+ 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.
+
Scopri tutti i progetti
-
+
)
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) => (
-
-
+
+ {/* Nodes on a line: vertical below lg, horizontal from lg (Figma) */}
+
+ {milestones.map((milestone) => (
+
+
+
+ {milestone.year}
+
+
+ {milestone.description}
+
))}
+
-
+
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) => (
+
+ {index > 0 && }
+
+ {item.icon && }
+ {item.value && {item.value} }
+ {item.label}
+
+
+ ))}
+
+ )
+}
+
+/** 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) => (
-
-
- {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]}
))}
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.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.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) */}
+
+