+
Groups
-
+
+
Le risorse per le matricole non sono disponibili al momento. Riprova più tardi.
)} diff --git a/src/app/projects/[id]/page.tsx b/src/app/projects/[id]/page.tsx index 8d1c623e..a721908d 100644 --- a/src/app/projects/[id]/page.tsx +++ b/src/app/projects/[id]/page.tsx @@ -2,10 +2,11 @@ import type { Metadata } from "next" import Image from "next/image" import Link from "next/link" import { notFound } from "next/navigation" -import { FiArrowLeft, FiArrowUpRight, FiUploadCloud } from "react-icons/fi" +import { FiArrowLeft, FiArrowUpRight } from "react-icons/fi" import { Glass } from "@/components/glass" import { GradientIcon } from "@/components/gradient-icon" -import { SECTION_LEAD, SECTION_TITLE } from "@/components/layout/page" +import { ClosingCta } from "@/components/layout/closing-cta" +import { CONTAINER, PAGE_HERO, PAGE_MAIN, SECTION_LEAD, SECTION_STACK, SECTION_TITLE } from "@/components/layout/page" import { getProjectResources, resourceDetails } from "@/components/projects/resources" import { Button } from "@/components/ui/button" import { Card, CardContent, CardTitle } from "@/components/ui/card" @@ -39,16 +40,21 @@ export default async function ProjectPage({ params }: Props) { const resources = getProjectResources(project) return ( -
Come usarlo
@@ -135,19 +134,17 @@ export default async function ProjectPage({ params }: Props) {
)}
-
-
- Scopri gli altri progetti
-
- Strumenti e app realizzati dagli studenti del Politecnico, per gli studenti del Politecnico.
-
+
Tutti i progetti
-
-
+ }
+ />
)
}
diff --git a/src/app/projects/page.tsx b/src/app/projects/page.tsx
index fbfb0d74..25e85bd6 100644
--- a/src/app/projects/page.tsx
+++ b/src/app/projects/page.tsx
@@ -1,7 +1,9 @@
+import { CONTAINER, PAGE_HERO, PAGE_MAIN } from "@/components/layout/page"
import { Collection } from "@/components/projects/collection"
import { CommunityNews } from "@/components/projects/community-news"
import { Deprecated } from "@/components/projects/deprecated"
import { Upload } from "@/components/projects/upload"
+import { Hero } from "@/components/ui/hero"
import { getAllProjects } from "@/queries/projects"
export const metadata = {
@@ -11,7 +13,7 @@ export const metadata = {
export const dynamic = "force-dynamic"
-export default async function Home() {
+export default async function ProjectsPage() {
const projects = await getAllProjects()
const news = projects.filter((project) => project.category === "news")
@@ -19,9 +21,12 @@ export default async function Home() {
const deprecated = projects.filter((project) => project.category === "deprecated")
return (
-
+
+
+
+
-
+
diff --git a/src/components/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/associations/associations-list.tsx b/src/components/associations/associations-list.tsx
index 847a39a3..65dd312a 100644
--- a/src/components/associations/associations-list.tsx
+++ b/src/components/associations/associations-list.tsx
@@ -13,7 +13,6 @@ import {
import AccordionAssociation from "@/components/accordion-association"
import type { GradientIconType } from "@/components/gradient-icon"
import type { ApiOutput } from "@/types"
-import esnLogo from "../../../public/logos/esn.svg"
type Association = ApiOutput["web"]["associations"]["getAllAssociations"][number]
type AssociationLinks = Association["links"]
@@ -33,13 +32,19 @@ const LINK_CONFIG: { key: keyof AssociationLinks; label: string; icon: GradientI
export function AssociationsList({ associations }: { associations: Association[] }) {
if (associations.length === 0) {
- return Nessuna associazione disponibile al momento.
+ // The query returns an empty list on backend errors too, so the message covers both cases
+ return (
+
+ Le associazioni non sono disponibili al momento. Riprova più tardi.
+
+ )
}
const accordionItems = associations.map((association) => ({
value: String(association.id),
name: association.name,
- logo: association.logo ?? esnLogo,
+ // No placeholder logo: an association without one gets its initials (see AccordionAssociation)
+ logo: association.logo,
content: association.descriptionIt,
links: LINK_CONFIG.filter(({ key }) => association.links[key]).map(({ key, label, icon, mailto }) => {
const value = association.links[key] as string
@@ -47,9 +52,5 @@ export function AssociationsList({ associations }: { associations: Association[]
}),
}))
- return (
-
-
-
- )
+ return
}
diff --git a/src/components/card-caption.tsx b/src/components/card-caption.tsx
index 43fe8e3b..70468f5c 100644
--- a/src/components/card-caption.tsx
+++ b/src/components/card-caption.tsx
@@ -1,3 +1,4 @@
+import Image from "next/image"
import Link from "next/link"
import type { ReactNode } from "react"
import type { IconType } from "react-icons"
@@ -9,18 +10,25 @@ export function CardCaption({
caption,
icon,
iconPosition = "right",
+ image,
href,
footer,
className,
+ titleClassName,
+ captionClassName,
}: {
title: string
caption: ReactNode
icon?: IconType
iconPosition?: "top" | "right"
+ /** An image shown in place of the icon, e.g. a project's logo. */
+ image?: string
href?: string
/** Pinned to the bottom of the card, e.g. where a link leads to. */
footer?: ReactNode
className?: string
+ titleClassName?: string
+ captionClassName?: string
}) {
return (
// Only cards that lead somewhere react to hover
@@ -32,10 +40,19 @@ export function CardCaption({
- {title}
- {icon && }
+
+ {/* Gradient per line (box-decoration-clone): a two-line title doesn't fade into a paler second line */}
+
+ {title}
+
+
+ {image ? (
+
+ ) : (
+ icon &&
+ )}
-
+
{typeof caption === "string" ? {caption}
: caption}
{footer && {footer}}
diff --git a/src/components/card-course-group.tsx b/src/components/card-course-group.tsx
index bfd21a3a..558a8de3 100644
--- a/src/components/card-course-group.tsx
+++ b/src/components/card-course-group.tsx
@@ -10,7 +10,7 @@ import { cn } from "@/lib/utils"
import { Card, CardAction, CardTitle } from "./ui/card"
export const cardCourseGroupVariants = cva(
- "flex h-fit w-full min-w-0 flex-1 flex-row items-center gap-3 px-5 py-4 font-normal leading-6 tracking-[0.03125rem] sm:gap-5 sm:px-7.5 sm:py-6.25",
+ "@container h-auto w-full min-w-0 px-4 py-3 font-normal leading-6 tracking-[0.03125rem] sm:px-6 sm:py-4",
{
variants: {
secondary: {
@@ -33,7 +33,6 @@ export function CardCourseGroup({
tgGroupId,
iconTelegram: IconTelegram = LiaTelegramPlane,
secondary = false,
- stacked = false,
}: {
groupName: string
waLink?: string
@@ -42,38 +41,42 @@ export function CardCourseGroup({
tgLink?: string
tgGroupId?: number
iconTelegram?: IconType
- stacked?: boolean
} & VariantProps) {
- const actionClassName = cn("rounded-full p-2 sm:p-3.75", secondary ? "bg-[#51A2FF]" : "bg-[#74D4FF]")
+ // The platform buttons are the only links in the row, so they carry the hover state, not the card
+ const actionClassName = cn(
+ "rounded-full p-2 transition-[filter] group-hover/action:brightness-90 sm:p-3",
+ secondary ? "bg-[#51A2FF]" : "bg-[#74D4FF]"
+ )
return (
-
-
- {groupName}
-
-
-
-
+
+ {/* In a card narrower than 20rem the name gets its own line, so it doesn't wrap word by word */}
+
+
+ {groupName}
+
+
+
+
+
)
@@ -100,7 +103,13 @@ function PlatformAction({
return (
<>
-
+
{groupId !== undefined && (
@@ -108,8 +117,12 @@ function PlatformAction({
selectedReport={{ groupId, type: platform, reportType: "broken_link", reportedLink: link }}
selectedGroupName={groupName}
trigger={
-
Scopri gli altri progetti
-- Strumenti e app realizzati dagli studenti del Politecnico, per gli studenti del Politecnico. -
+{item.content}
} + {item.links.length > 0 && ( +-
+ {item.links.map((link) => (
+
-
+
+
+ + +
+ ))}
+
Nessuna associazione disponibile al momento.
+ // The query returns an empty list on backend errors too, so the message covers both cases + return ( ++ Le associazioni non sono disponibili al momento. Riprova più tardi. +
+ ) } const accordionItems = associations.map((association) => ({ value: String(association.id), name: association.name, - logo: association.logo ?? esnLogo, + // No placeholder logo: an association without one gets its initials (see AccordionAssociation) + logo: association.logo, content: association.descriptionIt, links: LINK_CONFIG.filter(({ key }) => association.links[key]).map(({ key, label, icon, mailto }) => { const value = association.links[key] as string @@ -47,9 +52,5 @@ export function AssociationsList({ associations }: { associations: Association[] }), })) - return ( -{caption}
: caption}