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/[id]/page.tsx b/src/app/projects/[id]/page.tsx new file mode 100644 index 0000000..fed6fa4 --- /dev/null +++ b/src/app/projects/[id]/page.tsx @@ -0,0 +1,167 @@ +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 { Shape, ShapeLayer } from "@/components/shapes" +import { Button } from "@/components/ui/button" +import { Card, CardContent, CardTitle } from "@/components/ui/card" +import { cn } from "@/lib/utils" +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"]) { + 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 ( +
+
+ + + +
+ + Tutti i progetti + + + {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."} +

+
+
    1 && "md:grid-cols-2", + resources.length > 2 && "xl:grid-cols-3" + )} + > + {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. +

+ +
+
+
+ ) +} 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/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 +} 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/) + } +})