From 7cf55e62acc1732cb5db131a349b6ef16b91aaad Mon Sep 17 00:00:00 2001 From: Bianca Roberta Ianosel Date: Fri, 16 Oct 2026 08:28:34 +0000 Subject: [PATCH 1/5] feat(projects): add project detail page Add /projects/[id] with the project header and one card per way to use it (web app, platform downloads, source code). Project cards now open this page. The backend only exposes the project link for now, so getProjectResources maps it to a web or source resource until per-platform downloads are available. --- src/app/projects/[id]/page.tsx | 94 ++++++++++++++++++++++ src/components/projects/collection.tsx | 4 +- src/components/projects/community-news.tsx | 4 +- src/components/projects/deprecated.tsx | 4 +- src/components/projects/resources.ts | 49 +++++++++++ src/queries/projects.ts | 5 ++ 6 files changed, 154 insertions(+), 6 deletions(-) create mode 100644 src/app/projects/[id]/page.tsx create mode 100644 src/components/projects/resources.ts diff --git a/src/app/projects/[id]/page.tsx b/src/app/projects/[id]/page.tsx new file mode 100644 index 0000000..f1f414f --- /dev/null +++ b/src/app/projects/[id]/page.tsx @@ -0,0 +1,94 @@ +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 { Glass } from "@/components/glass" +import { GradientIcon } from "@/components/gradient-icon" +import { getProjectResources, resourceDetails } from "@/components/projects/resources" +import { Button } from "@/components/ui/button" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { getProject } from "@/queries/projects" + +export const dynamic = "force-dynamic" + +type Props = { params: Promise<{ id: string }> } + +async function loadProject(params: Props["params"]) { + const { id } = await params + const projectId = Number(id) + return Number.isInteger(projectId) ? getProject(projectId) : null +} + +export async function generateMetadata({ params }: Props): Promise { + const project = await loadProject(params) + return project ? { title: project.title, description: project.descriptionIt } : { title: "Progetto non trovato" } +} + +export default async function ProjectPage({ params }: Props) { + const project = await loadProject(params) + if (!project) notFound() + const resources = getProjectResources(project) + + return ( +
+ + + +
+ {project.logo ? ( + + ) : ( + + )} +
+
+

+ {project.title} +

+

{project.descriptionIt}

+
+
+ + {resources.length > 0 && ( +
+

+ Come usarlo +

+
    + {resources.map((resource) => { + const details = resourceDetails[resource.kind] + const external = resource.href.startsWith("http") + return ( +
  • + + + + {details.title} + + {details.description} + + +
  • + ) + })} +
+
+ )} +
+ ) +} diff --git a/src/components/projects/collection.tsx b/src/components/projects/collection.tsx index 8eb420c..6134501 100644 --- a/src/components/projects/collection.tsx +++ b/src/components/projects/collection.tsx @@ -47,7 +47,7 @@ export function Collection({ projects }: { projects: Project[] }) { title={project.title} caption={project.descriptionIt} icon={FiUploadCloud} - href={project.link ?? undefined} + href={`/projects/${project.id}`} /> ))} @@ -78,7 +78,7 @@ export function Collection({ projects }: { projects: Project[] }) { title={project.title} caption={project.descriptionIt} icon={FiUploadCloud} - href={project.link ?? undefined} + href={`/projects/${project.id}`} /> diff --git a/src/components/projects/community-news.tsx b/src/components/projects/community-news.tsx index 7b65cb5..ee7440e 100644 --- a/src/components/projects/community-news.tsx +++ b/src/components/projects/community-news.tsx @@ -34,7 +34,7 @@ export function CommunityNews({ projects }: { projects: Project[] }) { caption={project.descriptionIt} icon={FiCrop} iconPosition="right" - href={project.link ?? undefined} + href={`/projects/${project.id}`} /> ))} @@ -51,7 +51,7 @@ export function CommunityNews({ projects }: { projects: Project[] }) { caption={project.descriptionIt} icon={FiCrop} iconPosition="right" - href={project.link ?? undefined} + href={`/projects/${project.id}`} /> diff --git a/src/components/projects/deprecated.tsx b/src/components/projects/deprecated.tsx index b8ce5bb..34a0fd0 100644 --- a/src/components/projects/deprecated.tsx +++ b/src/components/projects/deprecated.tsx @@ -39,7 +39,7 @@ export function Deprecated({ projects }: { projects: Project[] }) { title={project.title} caption={project.descriptionIt} icon={FiUploadCloud} - href={project.link ?? undefined} + href={`/projects/${project.id}`} /> ))} @@ -70,7 +70,7 @@ export function Deprecated({ projects }: { projects: Project[] }) { title={project.title} caption={project.descriptionIt} icon={FiUploadCloud} - href={project.link ?? undefined} + href={`/projects/${project.id}`} /> diff --git a/src/components/projects/resources.ts b/src/components/projects/resources.ts new file mode 100644 index 0000000..09227e0 --- /dev/null +++ b/src/components/projects/resources.ts @@ -0,0 +1,49 @@ +import type { IconType } from "react-icons" +import { FaAndroid, FaApple, FaGithub, FaLinux, 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" + +/** 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 } + +export const resourceDetails: Record< + ProjectResourceKind, + { title: string; description: string; cta: string; icon: IconType } +> = { + web: { + title: "Web", + description: "Usalo direttamente dal browser, senza installare nulla.", + cta: "Apri il sito", + icon: FiGlobe, + }, + windows: { + title: "Windows", + description: "Scarica l’installer per Windows.", + cta: "Scarica per Windows", + icon: FaWindows, + }, + macos: { title: "macOS", description: "Scarica l’app per macOS.", cta: "Scarica per macOS", icon: FaApple }, + linux: { title: "Linux", description: "Scarica il pacchetto per Linux.", cta: "Scarica per Linux", icon: FaLinux }, + android: { title: "Android", description: "Installa l’app su Android.", cta: "Scarica per Android", icon: FaAndroid }, + ios: { title: "iOS", description: "Installa l’app su iPhone e iPad.", cta: "Scarica per iOS", icon: FaApple }, + source: { + title: "Codice sorgente", + description: "Consulta il codice, segui il README e contribuisci al progetto.", + cta: "Vai alla repository", + icon: FaGithub, + }, +} + +/** + * 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. + */ +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 }] +} diff --git a/src/queries/projects.ts b/src/queries/projects.ts index 39bbcd7..92e0cf6 100644 --- a/src/queries/projects.ts +++ b/src/queries/projects.ts @@ -10,3 +10,8 @@ export async function getAllProjects() { return [] } } + +export async function getProject(id: number) { + const projects = await getAllProjects() + return projects.find((project) => project.id === id) ?? null +} From 1ee8b5c280163bbe98adcf106e0afe1261a32f3b Mon Sep 17 00:00:00 2001 From: Bianca Roberta Ianosel Date: Fri, 16 Oct 2026 08:35:19 +0000 Subject: [PATCH 2/5] style(projects): align project detail page with site layout Use the Hero component with the projects page shapes, a gradient section title, linked CardIcon cards for each resource and a closing Glass panel to browse the other projects, with the shared section spacing. --- src/app/projects/[id]/page.tsx | 114 +++++++++++++++++++-------------- 1 file changed, 65 insertions(+), 49 deletions(-) diff --git a/src/app/projects/[id]/page.tsx b/src/app/projects/[id]/page.tsx index f1f414f..d97131b 100644 --- a/src/app/projects/[id]/page.tsx +++ b/src/app/projects/[id]/page.tsx @@ -2,13 +2,15 @@ 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 { FiArrowUpRight, FiUploadCloud } from "react-icons/fi" +import { CardIcon } from "@/components/card-icon" import { Glass } from "@/components/glass" import { GradientIcon } from "@/components/gradient-icon" import { getProjectResources, resourceDetails } from "@/components/projects/resources" import { Button } from "@/components/ui/button" -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Hero } from "@/components/ui/hero" import { getProject } from "@/queries/projects" +import { CommunityNewsShapes } from "../shapes" export const dynamic = "force-dynamic" @@ -31,64 +33,78 @@ export default async function ProjectPage({ params }: Props) { const resources = getProjectResources(project) return ( -
- - - -
- {project.logo ? ( - - ) : ( - - )} -
-
-

- {project.title} -

-

{project.descriptionIt}

-
-
+
+
+ +
+ + {project.logo ? ( + + ) : ( + + )} + + +
+
{resources.length > 0 && ( -
-

- Come usarlo -

-
    +
    +
    +

    + Come usarlo +

    +

    + Scegli la versione più adatta a te. +

    +
    +
      {resources.map((resource) => { const details = resourceDetails[resource.kind] - const external = resource.href.startsWith("http") return ( -
    • - - - - {details.title} - - {details.description} - - +
    • +
    • ) })}
    )} + +
    + +

    + Scopri gli altri progetti +

    +

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

    + +
    +
) } From 6e5141c14b513a653aeb4439e74fa077654bf281 Mon Sep 17 00:00:00 2001 From: Bianca Roberta Ianosel Date: Fri, 16 Oct 2026 09:06:21 +0000 Subject: [PATCH 3/5] fix(projects): allow Webeep logo and preserve background proportions --- next.config.js | 7 +++++++ src/app/projects/shapes.tsx | 9 +++------ tests/project-images.test.mjs | 29 +++++++++++++++++++++++++++++ 3 files changed, 39 insertions(+), 6 deletions(-) create mode 100644 tests/project-images.test.mjs diff --git a/next.config.js b/next.config.js index 4ac987b..d14d83e 100644 --- a/next.config.js +++ b/next.config.js @@ -20,6 +20,13 @@ const config = { protocol: "https", hostname: "*.blob.core.windows.net", }, + { + protocol: "https", + hostname: "raw.githubusercontent.com", + port: "", + pathname: "/toto04/webeep-sync/**", + search: "", + }, ], }, } diff --git a/src/app/projects/shapes.tsx b/src/app/projects/shapes.tsx index 90ec5bb..297b464 100644 --- a/src/app/projects/shapes.tsx +++ b/src/app/projects/shapes.tsx @@ -53,7 +53,7 @@ export function CollectionShapes() { ) @@ -69,10 +69,7 @@ export function UploadShapes() { {/* Desktop Shapes */} - + ) } @@ -90,7 +87,7 @@ export function DeprecatedShapes() { ) diff --git a/tests/project-images.test.mjs b/tests/project-images.test.mjs new file mode 100644 index 0000000..2903e1b --- /dev/null +++ b/tests/project-images.test.mjs @@ -0,0 +1,29 @@ +import assert from "node:assert/strict" +import { createRequire } from "node:module" +import test from "node:test" + +// Load the image configuration without requiring unrelated application credentials. +process.env.SKIP_ENV_VALIDATION = "true" +const { default: nextConfig } = await import("../next.config.js") +const require = createRequire(import.meta.url) +const { default: imageLoader } = require("next/dist/shared/lib/image-loader.js") +const { imageConfigDefault } = require("next/dist/shared/lib/image-config.js") + +function loadImage(src) { + return imageLoader({ config: { ...imageConfigDefault, ...nextConfig.images }, src, width: 128, quality: 75 }) +} + +test("the reported Webeep Sync logo can be rendered by next/image", () => { + const src = "https://raw.githubusercontent.com/toto04/webeep-sync/main/static/icons/icon-256x256.png" + assert.equal(new URL(loadImage(src), "https://polinetwork.org").searchParams.get("url"), src) +}) + +test("the new image permission is limited to the intended HTTPS repository", () => { + for (const src of [ + "https://unconfigured.example/logo.png", + "https://raw.githubusercontent.com/other-owner/other-repo/main/logo.png", + "http://raw.githubusercontent.com/toto04/webeep-sync/main/static/icons/icon-256x256.png", + ]) { + assert.throws(() => loadImage(src), /not configured/) + } +}) From d941c5a97186bbcab0ba37eb45494e9733c2d260 Mon Sep 17 00:00:00 2001 From: Bianca Roberta Ianosel Date: Fri, 16 Oct 2026 09:13:25 +0000 Subject: [PATCH 4/5] style(projects): balance detail layout across screen sizes --- src/app/projects/[id]/page.tsx | 143 +++++++++++++++++++++------------ 1 file changed, 91 insertions(+), 52 deletions(-) diff --git a/src/app/projects/[id]/page.tsx b/src/app/projects/[id]/page.tsx index d97131b..b512948 100644 --- a/src/app/projects/[id]/page.tsx +++ b/src/app/projects/[id]/page.tsx @@ -2,15 +2,15 @@ import type { Metadata } from "next" import Image from "next/image" import Link from "next/link" import { notFound } from "next/navigation" -import { FiArrowUpRight, FiUploadCloud } from "react-icons/fi" -import { CardIcon } from "@/components/card-icon" +import { FiArrowLeft, FiArrowUpRight, FiUploadCloud } from "react-icons/fi" import { Glass } from "@/components/glass" import { GradientIcon } from "@/components/gradient-icon" import { getProjectResources, resourceDetails } from "@/components/projects/resources" +import { Shape, ShapeLayer } from "@/components/shapes" import { Button } from "@/components/ui/button" -import { Hero } from "@/components/ui/hero" +import { Card, CardContent, CardTitle } from "@/components/ui/card" +import { cn } from "@/lib/utils" import { getProject } from "@/queries/projects" -import { CommunityNewsShapes } from "../shapes" export const dynamic = "force-dynamic" @@ -33,56 +33,95 @@ export default async function ProjectPage({ params }: Props) { const resources = getProjectResources(project) return ( -
+
- -
- - {project.logo ? ( - - ) : ( - - )} - - + + + +
+ + Tutti i progetti + +
+ + {project.logo ? ( + + ) : ( + + )} + +
+

+ {project.title} +

+

+ {project.descriptionIt} +

+
+
{resources.length > 0 && ( -
-
-

+
+
+

Come usarlo

-

- Scegli la versione più adatta a te. +

+ {resources.length > 1 + ? "Scegli la versione più adatta a te." + : "Apri il progetto e segui le istruzioni per iniziare."}

-
    +
      1 && "md:grid-cols-2", + resources.length > 2 && "xl:grid-cols-3" + )} + > {resources.map((resource) => { const details = resourceDetails[resource.kind] return ( -
    • - +
    • + +
      + +
      + {details.title} + {details.description} +
      +
      + +
    • ) })} @@ -90,17 +129,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. -

-
From e52a4ab7920951f25762919de4b03fc157aadfaf Mon Sep 17 00:00:00 2001 From: Bianca Roberta Ianosel Date: Fri, 16 Oct 2026 09:44:28 +0000 Subject: [PATCH 5/5] style(projects): rework detail hero and section spacing Put the logo, category, title and description in a gradient Glass panel like the history page, show app icon logos without an extra frame, use the shared page top padding, max-w-330 column and section gaps, gradient section titles, and open external resource links in a new tab. --- src/app/projects/[id]/page.tsx | 100 +++++++++++++++++++-------------- 1 file changed, 59 insertions(+), 41 deletions(-) diff --git a/src/app/projects/[id]/page.tsx b/src/app/projects/[id]/page.tsx index b512948..fed6fa4 100644 --- a/src/app/projects/[id]/page.tsx +++ b/src/app/projects/[id]/page.tsx @@ -14,6 +14,12 @@ import { getProject } from "@/queries/projects" export const dynamic = "force-dynamic" +const categoryLabels = { + news: "Novità della community", + general: "Progetto della community", + deprecated: "Progetto non più mantenuto", +} as const + type Props = { params: Promise<{ id: string }> } async function loadProject(params: Props["params"]) { @@ -33,52 +39,62 @@ export default async function ProjectPage({ params }: Props) { const resources = getProjectResources(project) return ( -
+
-
+
Tutti i progetti -
- - {project.logo ? ( - - ) : ( - + + {project.logo ? ( + // Project logos are app icons with their own background and corners: shown as they are, not framed + + ) : ( + + + + )} +
+ {project.category in categoryLabels && ( +

{categoryLabels[project.category]}

)} - -
-

+

{project.title}

{project.descriptionIt}

-
+
{resources.length > 0 && ( -
-
-

+
+
+

Come usarlo

-

+

{resources.length > 1 ? "Scegli la versione più adatta a te." : "Apri il progetto e segui le istruzioni per iniziare."} @@ -93,17 +109,18 @@ export default async function ProjectPage({ params }: Props) { > {resources.map((resource) => { const details = resourceDetails[resource.kind] + const external = resource.href.startsWith("http") return (

  • -
    +
    -
    +
    {details.title} {details.description}
    @@ -111,12 +128,13 @@ export default async function ProjectPage({ params }: Props) {
  • )} -
    - -
    -

    Scopri gli altri progetti

    -

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

    -
    -