diff --git a/.gitignore b/.gitignore index c2e19a5b..dd586ac7 100644 --- a/.gitignore +++ b/.gitignore @@ -50,4 +50,7 @@ yarn-error.log* .idea # codex -.codex \ No newline at end of file +.codex + +# PDF.js assets copied from the installed dependency during dev/build +/public/pdfjs/ diff --git a/AGENTS.md b/AGENTS.md index abd1342a..96a620fd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,7 +14,7 @@ breaking changes with `!` or a `BREAKING CHANGE:` footer. -# This is NOT the Next.js you know +## This is NOT the Next.js you know This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices. diff --git a/package.json b/package.json index 0aeb6177..9aec8a0d 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,8 @@ "type": "module", "scripts": { "preinstall": "npx -y only-allow pnpm", - "build": "cross-env SKIP_ENV_VALIDATION=true next build", - "dev": "cross-env PORT=3002 next dev", + "build": "node scripts/copy-pdf-assets.mjs && cross-env SKIP_ENV_VALIDATION=true next build", + "dev": "node scripts/copy-pdf-assets.mjs && cross-env PORT=3002 next dev", "preview": "pnpm build && pnpm start", "start": "node .next/standalone/server.js", "typecheck": "tsc --noEmit", @@ -40,6 +40,7 @@ "react-dom": "19.3.0", "react-hook-form": "^7.68.0", "react-icons": "^5.6.0", + "react-pdf": "^11.0.0", "superjson": "^2.2.6", "tailwind-merge": "^3.4.0", "tailwindcss-animate": "^1.0.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 94f4f76b..82a821d4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -80,6 +80,9 @@ importers: react-icons: specifier: ^5.6.0 version: 5.6.0(react@19.3.0) + react-pdf: + specifier: ^11.0.0 + version: 11.0.0(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) superjson: specifier: ^2.2.6 version: 2.2.6 @@ -401,6 +404,81 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@napi-rs/canvas-android-arm64@1.1.0': + resolution: {integrity: sha512-mKrSmRyhWK4q699Q1xthbZIZi7JBvLtsRNnOZAeFaLPOFT1yo8y82aRC2nNZbXTGJXU7fVDONgGQPr0vo3kerw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@napi-rs/canvas-darwin-arm64@1.1.0': + resolution: {integrity: sha512-iqwST4g0oH+x8VCpBIYh+MYokv/mVvkM+v9oJGB0d9A3P2qBh42ogJlVyWq9SpPCrAjS7tplMb1D3OIfxl+utw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@napi-rs/canvas-darwin-x64@1.1.0': + resolution: {integrity: sha512-jkeA8eVTxDum03LTvc32YJ28F6KB1U0gYwnWMfOAUQZvWWEh/kvONby4iWGJx3+Tw45UqMGMXjtw2GKNVZDhVg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@napi-rs/canvas-linux-arm-gnueabihf@1.1.0': + resolution: {integrity: sha512-Y4YkKG1ijoqkgUdTEEBNG4k+2BK5bR5jGL6S56YxCXYPwGfUd5nED8/74xyc7z+bpixPasCjSYoP3xYPOIGNxw==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@napi-rs/canvas-linux-arm64-gnu@1.1.0': + resolution: {integrity: sha512-dE/XthqYkiWcBVtKFlZbUXwAYfDuQGqDWixznipJNmevLGpprNnTVQZyftm1RZJudkIOn2EkzYZDMMETM08Row==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@napi-rs/canvas-linux-arm64-musl@1.1.0': + resolution: {integrity: sha512-DqZ/CMcHlhU56pPjv9AP0h3U7MFQ5LzC1HzMyRMotL+PFsJmZJA/ziApPYVyyz78DaVLhI8t3So3ztqtijbYpg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@napi-rs/canvas-linux-riscv64-gnu@1.1.0': + resolution: {integrity: sha512-kJ7m5n2VffxWXM5QGa5lfq8ZN4LdOJEoz3aAJAmIqYvKiZw1gRdXqpucnRXsF6D9nLYiiIlevaAtAclR7c5ilg==} + engines: {node: '>= 10'} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@napi-rs/canvas-linux-x64-gnu@1.1.0': + resolution: {integrity: sha512-uQ9JuJlRhQdHyttcMWOiwLkfIqkqaB0YFUVe835LUe12THt2b51HXkTItq0fxWT1jdhUPX2ylWZ1walkJyD6GQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@napi-rs/canvas-linux-x64-musl@1.1.0': + resolution: {integrity: sha512-has4CPPAkkCac/LZ13VAw5p+MJLdaKVoyrhumN/A7uJd6960xYkJNDZgUebyxiha82AFzXx6RV+QJg5qGG079g==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@napi-rs/canvas-win32-arm64-msvc@1.1.0': + resolution: {integrity: sha512-MJnktLKNijmXqdjTughHDZ9TDLZ9uUm2hvFXqaOqn0+w7lphXFnft6bpSgQAc9U1exc20UGIBlzGu9+kiCj9Ag==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@napi-rs/canvas-win32-x64-msvc@1.1.0': + resolution: {integrity: sha512-08575bFk7iiadxap4h+JxgGut9xDCHAGum6wh0eInN53zEMImkmEHi5MushX2FOmNO7qRNX7BPiDC5ZYBwHeEg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@napi-rs/canvas@1.1.0': + resolution: {integrity: sha512-k+ph/D8H9GS8ZZ48okvHrKeJynkqAA1kMUZRuzRWNlQUDHXngq7oRaWMZBdmSfd9ZhVy9bwNKei/7wMboI/uMg==} + engines: {node: '>= 10'} + '@next/env@16.4.1': resolution: {integrity: sha512-5sUUrrSvs/2/2LnLoesybMGx12EtPB46fdZ6RhAw0XDGSPxU2LFyQBB7yuEiOFFJPKmBY6+q2jTr1/AMbnp0/g==} @@ -1559,6 +1637,10 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} @@ -1583,6 +1665,9 @@ packages: resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==} engines: {node: '>=10.13.0'} + es-toolkit@1.53.1: + resolution: {integrity: sha512-VhMc6IXmexu8iYFfL2aaAww6IvJTdHlqBLruLoEUs33YD4IcP5DdZcLOybMsMmmLsiy1c8PsiLsrM0DdIuuRUA==} + geist@1.5.1: resolution: {integrity: sha512-mAHZxIsL2o3ZITFaBVFBnwyDOw+zNLYum6A6nIjpzCGIO8QtC3V76XF2RnZTyLx1wlDTmMDy8jg3Ib52MIjGvQ==} peerDependencies: @@ -1609,6 +1694,9 @@ packages: jose@6.2.12: resolution: {integrity: sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw==} + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + lightningcss-android-arm64@1.30.2: resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} engines: {node: '>= 12.0.0'} @@ -1683,9 +1771,27 @@ packages: resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} engines: {node: '>= 12.0.0'} + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + make-cancellable-promise@2.0.0: + resolution: {integrity: sha512-3SEQqTpV9oqVsIWqAcmDuaNeo7yBO3tqPtqGRcKkEo0lrzD3wqbKG9mkxO65KoOgXqj+zH2phJ2LiAsdzlogSw==} + + make-event-props@2.0.0: + resolution: {integrity: sha512-G/hncXrl4Qt7mauJEXSg3AcdYzmpkIITTNl5I+rH9sog5Yw0kK6vseJjCaPfOXqOqQuPUP89Rkhfz5kPS8ijtw==} + + merge-refs@2.0.0: + resolution: {integrity: sha512-3+B21mYK2IqUWnd2EivABLT7ueDhb0b8/dGK8LoFQPrU61YITeCMn14F7y7qZafWNZhUEKb24cJdiT5Wxs3prg==} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + nanoid@3.3.18: resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -1722,6 +1828,10 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + pdfjs-dist@6.3.289: + resolution: {integrity: sha512-ZHjSVpDa3D6izMq8/04lvkhkATUmL9px6ChPaXc1k6nU2Mrhlg1/7F0bdUqCwUjw3NsPTfPZsMDUU6ZIcRaeQw==} + engines: {node: '>=22.13.0 || >=24'} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -1758,6 +1868,16 @@ packages: peerDependencies: react: '*' + react-pdf@11.0.0: + resolution: {integrity: sha512-x4Ierg3Sl+gOoqd0pVOQndiuFKeGcXQmUNtaSH0D/cs2nUW1HBDlFBT2Z8jDsDJv0Kh//jlOJT21ERyQtlRe7A==} + peerDependencies: + '@types/react': ^19.0.0 + react: ^19.0.0 + react-dom: ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + react-remove-scroll-bar@2.3.8: resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} engines: {node: '>=10'} @@ -1853,6 +1973,9 @@ packages: resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} engines: {node: '>=6'} + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -1892,6 +2015,9 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + warning@4.0.3: + resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -2101,6 +2227,54 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@napi-rs/canvas-android-arm64@1.1.0': + optional: true + + '@napi-rs/canvas-darwin-arm64@1.1.0': + optional: true + + '@napi-rs/canvas-darwin-x64@1.1.0': + optional: true + + '@napi-rs/canvas-linux-arm-gnueabihf@1.1.0': + optional: true + + '@napi-rs/canvas-linux-arm64-gnu@1.1.0': + optional: true + + '@napi-rs/canvas-linux-arm64-musl@1.1.0': + optional: true + + '@napi-rs/canvas-linux-riscv64-gnu@1.1.0': + optional: true + + '@napi-rs/canvas-linux-x64-gnu@1.1.0': + optional: true + + '@napi-rs/canvas-linux-x64-musl@1.1.0': + optional: true + + '@napi-rs/canvas-win32-arm64-msvc@1.1.0': + optional: true + + '@napi-rs/canvas-win32-x64-msvc@1.1.0': + optional: true + + '@napi-rs/canvas@1.1.0': + optionalDependencies: + '@napi-rs/canvas-android-arm64': 1.1.0 + '@napi-rs/canvas-darwin-arm64': 1.1.0 + '@napi-rs/canvas-darwin-x64': 1.1.0 + '@napi-rs/canvas-linux-arm-gnueabihf': 1.1.0 + '@napi-rs/canvas-linux-arm64-gnu': 1.1.0 + '@napi-rs/canvas-linux-arm64-musl': 1.1.0 + '@napi-rs/canvas-linux-riscv64-gnu': 1.1.0 + '@napi-rs/canvas-linux-x64-gnu': 1.1.0 + '@napi-rs/canvas-linux-x64-musl': 1.1.0 + '@napi-rs/canvas-win32-arm64-msvc': 1.1.0 + '@napi-rs/canvas-win32-x64-msvc': 1.1.0 + optional: true + '@next/env@16.4.1': {} '@next/swc-darwin-arm64@16.4.1': @@ -3162,6 +3336,8 @@ snapshots: csstype@3.2.3: {} + dequal@2.0.3: {} + detect-libc@2.1.2: {} detect-node-es@1.1.0: {} @@ -3183,6 +3359,8 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.3.0 + es-toolkit@1.53.1: {} + geist@1.5.1(next@16.4.1(@types/node@24.10.4)(babel-plugin-react-compiler@1.0.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)): dependencies: next: 16.4.1(@types/node@24.10.4)(babel-plugin-react-compiler@1.0.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) @@ -3199,6 +3377,8 @@ snapshots: jose@6.2.12: {} + js-tokens@4.0.0: {} + lightningcss-android-arm64@1.30.2: optional: true @@ -3248,10 +3428,22 @@ snapshots: lightningcss-win32-arm64-msvc: 1.30.2 lightningcss-win32-x64-msvc: 1.30.2 + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + make-cancellable-promise@2.0.0: {} + + make-event-props@2.0.0: {} + + merge-refs@2.0.0(@types/react@19.3.0): + optionalDependencies: + '@types/react': 19.3.0 + nanoid@3.3.18: {} next-themes@0.4.6(react-dom@19.3.0(react@19.3.0))(react@19.3.0): @@ -3287,6 +3479,10 @@ snapshots: path-key@3.1.1: {} + pdfjs-dist@6.3.289: + optionalDependencies: + '@napi-rs/canvas': 1.1.0 + picocolors@1.1.1: {} postcss@8.5.23: @@ -3371,6 +3567,22 @@ snapshots: dependencies: react: 19.3.0 + react-pdf@11.0.0(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0): + dependencies: + clsx: 2.1.1 + dequal: 2.0.3 + es-toolkit: 1.53.1 + make-cancellable-promise: 2.0.0 + make-event-props: 2.0.0 + merge-refs: 2.0.0(@types/react@19.3.0) + pdfjs-dist: 6.3.289 + react: 19.3.0 + react-dom: 19.3.0(react@19.3.0) + tiny-invariant: 1.3.3 + warning: 4.0.3 + optionalDependencies: + '@types/react': 19.3.0 + react-remove-scroll-bar@2.3.8(@types/react@19.3.0)(react@19.3.0): dependencies: react: 19.3.0 @@ -3466,6 +3678,8 @@ snapshots: tapable@2.3.0: {} + tiny-invariant@1.3.3: {} + tslib@2.8.1: {} tw-animate-css@1.4.0: {} @@ -3514,6 +3728,10 @@ snapshots: dependencies: react: 19.3.0 + warning@4.0.3: + dependencies: + loose-envify: 1.4.0 + which@2.0.2: dependencies: isexe: 2.0.0 diff --git a/public/documents/mock/bilancio-2022.pdf b/public/documents/mock/bilancio-2022.pdf new file mode 100644 index 00000000..bb3166c6 Binary files /dev/null and b/public/documents/mock/bilancio-2022.pdf differ diff --git a/public/documents/mock/bilancio-2023.pdf b/public/documents/mock/bilancio-2023.pdf new file mode 100644 index 00000000..113b7021 Binary files /dev/null and b/public/documents/mock/bilancio-2023.pdf differ diff --git a/public/documents/mock/bilancio-2024.pdf b/public/documents/mock/bilancio-2024.pdf new file mode 100644 index 00000000..de40a7ed Binary files /dev/null and b/public/documents/mock/bilancio-2024.pdf differ diff --git a/public/documents/mock/polinetwork-in-breve.pdf b/public/documents/mock/polinetwork-in-breve.pdf new file mode 100644 index 00000000..bc2a9009 Binary files /dev/null and b/public/documents/mock/polinetwork-in-breve.pdf differ diff --git a/public/documents/mock/privacy-app.pdf b/public/documents/mock/privacy-app.pdf new file mode 100644 index 00000000..883d43e2 Binary files /dev/null and b/public/documents/mock/privacy-app.pdf differ diff --git a/public/documents/mock/privacy-ccs.pdf b/public/documents/mock/privacy-ccs.pdf new file mode 100644 index 00000000..f4407050 Binary files /dev/null and b/public/documents/mock/privacy-ccs.pdf differ diff --git a/public/documents/mock/privacy-soci.pdf b/public/documents/mock/privacy-soci.pdf new file mode 100644 index 00000000..6a4d5d9c Binary files /dev/null and b/public/documents/mock/privacy-soci.pdf differ diff --git a/public/documents/mock/regolamento-interno.pdf b/public/documents/mock/regolamento-interno.pdf new file mode 100644 index 00000000..acba1b5d Binary files /dev/null and b/public/documents/mock/regolamento-interno.pdf differ diff --git a/public/documents/mock/regolamento-utenti.pdf b/public/documents/mock/regolamento-utenti.pdf new file mode 100644 index 00000000..f2a5a32f Binary files /dev/null and b/public/documents/mock/regolamento-utenti.pdf differ diff --git a/public/documents/mock/statuto.pdf b/public/documents/mock/statuto.pdf new file mode 100644 index 00000000..75181ec0 Binary files /dev/null and b/public/documents/mock/statuto.pdf differ diff --git a/scripts/copy-pdf-assets.mjs b/scripts/copy-pdf-assets.mjs new file mode 100644 index 00000000..63219ccf --- /dev/null +++ b/scripts/copy-pdf-assets.mjs @@ -0,0 +1,14 @@ +import { cp, mkdir } from "node:fs/promises" +import { createRequire } from "node:module" +import { dirname, join } from "node:path" + +// Resolve PDF.js from React-PDF so the renderer and worker always use the same version. +const require = createRequire(import.meta.url) +const reactPdfRequire = createRequire(require.resolve("react-pdf")) +const source = dirname(reactPdfRequire.resolve("pdfjs-dist/package.json")) +const destination = new URL("../public/pdfjs/", import.meta.url) +await mkdir(destination, { recursive: true }) +for (const name of ["cmaps", "standard_fonts", "wasm"]) { + await cp(join(source, name), new URL(name, destination), { recursive: true }) +} +await cp(join(source, "build/pdf.worker.min.mjs"), new URL("pdf.worker.min.mjs", destination)) diff --git a/src/app/documents/page.tsx b/src/app/documents/page.tsx new file mode 100644 index 00000000..d55a34a7 --- /dev/null +++ b/src/app/documents/page.tsx @@ -0,0 +1,43 @@ +import type { Metadata } from "next" +import { DocumentCategoryCard } from "@/components/documents/document-category-card" +import { mockDocumentCategories } from "@/components/documents/mock-data" +import { Hero } from "@/components/ui/hero" + +export const metadata: Metadata = { + title: "Documenti", + description: "Tutti i documenti che cerchi, in un unico posto.", +} + +export default function DocumentsPage() { + return ( +
+ +
+ +

+ Anteprima dimostrativa: tutti i PDF presenti sono esempi e non sono documenti ufficiali. +

+ {mockDocumentCategories.map((category) => ( + + ))} +
+
+ ) +} diff --git a/src/app/documents/view/[slug]/page.tsx b/src/app/documents/view/[slug]/page.tsx new file mode 100644 index 00000000..f45ed88c --- /dev/null +++ b/src/app/documents/view/[slug]/page.tsx @@ -0,0 +1,33 @@ +import type { Metadata } from "next" +import { notFound } from "next/navigation" +import { mockDocumentCategories } from "@/components/documents/mock-data" +import { PdfViewerLoader } from "@/components/documents/pdf-viewer-loader" +import { getDocumentSlug } from "@/components/documents/utils" + +const documents = mockDocumentCategories.flatMap((category) => category.documents) + +type Props = { params: Promise<{ slug: string }> } + +function findDocument(slug: string) { + return documents.find((document) => getDocumentSlug(document) === slug) +} + +export function generateStaticParams() { + return documents.map((document) => ({ slug: getDocumentSlug(document) })) +} + +export async function generateMetadata({ params }: Props): Promise { + const document = findDocument((await params).slug) + return { title: document?.title ?? "Documento non trovato" } +} + +export default async function DocumentViewerPage({ params }: Props) { + const document = findDocument((await params).slug) + if (!document) notFound() + + return ( +
+ +
+ ) +} diff --git a/src/components/conditional-footer.tsx b/src/components/conditional-footer.tsx index 35ce8eb5..8d52c38e 100644 --- a/src/components/conditional-footer.tsx +++ b/src/components/conditional-footer.tsx @@ -5,7 +5,7 @@ import type { ReactNode } from "react" import { matchesAnyRoutePrefix } from "@/utils/route-match" const HIDDEN_EXACT = ["/matricole/guida"] -const HIDDEN_PREFIXES = ["/groups"] +const HIDDEN_PREFIXES = ["/groups", "/documents/view"] /** * Hides the footer on full-screen routes. The footer itself is passed as `children` so it stays a server component diff --git a/src/components/documents/document-category-card.tsx b/src/components/documents/document-category-card.tsx new file mode 100644 index 00000000..8d6406b7 --- /dev/null +++ b/src/components/documents/document-category-card.tsx @@ -0,0 +1,61 @@ +import Link from "next/link" +import { FiDownload, FiEye, FiFile } from "react-icons/fi" +import { Glass } from "@/components/glass" +import { Button } from "@/components/ui/button" +import type { DocumentCategory } from "./types" +import { getDocumentViewerHref } from "./utils" + +export function DocumentCategoryCard({ category }: { category: DocumentCategory }) { + return ( +
+ +
+

+ {category.title} +

+

{category.description}

+
+
    + {category.documents.map((document) => ( +
  • +
    +
    +
    + + +
    +
  • + ))} +
+
+
+ ) +} diff --git a/src/components/documents/mock-data.ts b/src/components/documents/mock-data.ts new file mode 100644 index 00000000..86e66f6d --- /dev/null +++ b/src/components/documents/mock-data.ts @@ -0,0 +1,71 @@ +import type { DocumentCategory } from "./types" + +// Demonstration content only. Replace this catalog when official documents are available. +export const mockDocumentCategories: DocumentCategory[] = [ + { + id: "associazione", + title: "Associazione", + description: "Chi siamo e su quali principi ci fondiamo.", + documents: [ + { + title: "PoliNetwork in breve", + description: "Una panoramica dell’associazione e delle sue attività.", + href: "/documents/mock/polinetwork-in-breve.pdf", + }, + { + title: "Statuto dell’associazione", + description: "Finalità, organizzazione e principi associativi.", + href: "/documents/mock/statuto.pdf", + }, + ], + }, + { + id: "regolamenti", + title: "Regolamenti", + description: "Le regole dell’associazione e della community.", + documents: [ + { + title: "Regolamento interno dell’associazione", + description: "Le regole di organizzazione e funzionamento dell’associazione.", + href: "/documents/mock/regolamento-interno.pdf", + }, + { + title: "Regolamento per gli utenti", + description: "Le regole di partecipazione ai gruppi e alle piattaforme PoliNetwork.", + href: "/documents/mock/regolamento-utenti.pdf", + }, + ], + }, + { + id: "privacy", + title: "Privacy e Informative", + description: "Come trattiamo e proteggiamo i dati personali.", + documents: [ + { + title: "Associati e candidati soci", + description: "Il trattamento dei dati personali di chi è socio o richiede di diventarlo.", + href: "/documents/mock/privacy-soci.pdf", + }, + { + title: "App PoliNetwork", + description: "Il trattamento dei dati personali nell’app PoliNetwork.", + href: "/documents/mock/privacy-app.pdf", + }, + { + title: "Candidati CCS – elezioni 2023", + description: "Il trattamento dei dati personali dei candidati alle elezioni CCS di maggio 2023.", + href: "/documents/mock/privacy-ccs.pdf", + }, + ], + }, + { + id: "bilanci", + title: "Bilanci", + description: "Come gestiamo le risorse dell’associazione.", + documents: [ + { title: "Bilancio 2024", href: "/documents/mock/bilancio-2024.pdf" }, + { title: "Bilancio 2023", href: "/documents/mock/bilancio-2023.pdf" }, + { title: "Bilancio 2022", href: "/documents/mock/bilancio-2022.pdf" }, + ], + }, +] diff --git a/src/components/documents/pdf-viewer-loader.tsx b/src/components/documents/pdf-viewer-loader.tsx new file mode 100644 index 00000000..abeb7a61 --- /dev/null +++ b/src/components/documents/pdf-viewer-loader.tsx @@ -0,0 +1,13 @@ +"use client" + +import dynamic from "next/dynamic" +import type { DocumentItem } from "./types" + +const PdfViewer = dynamic(() => import("./pdf-viewer"), { + ssr: false, + loading: () => Caricamento del visualizzatore…, +}) + +export function PdfViewerLoader({ document }: { document: DocumentItem }) { + return +} diff --git a/src/components/documents/pdf-viewer.tsx b/src/components/documents/pdf-viewer.tsx new file mode 100644 index 00000000..5c7738dd --- /dev/null +++ b/src/components/documents/pdf-viewer.tsx @@ -0,0 +1,159 @@ +"use client" + +import Link from "next/link" +import { useEffect, useRef, useState } from "react" +import { FiArrowLeft, FiChevronLeft, FiChevronRight, FiDownload, FiZoomIn, FiZoomOut } from "react-icons/fi" +import { Document, Page, pdfjs } from "react-pdf" +import "react-pdf/dist/Page/TextLayer.css" +import { Glass } from "@/components/glass" +import { Button } from "@/components/ui/button" +import type { DocumentItem } from "./types" + +pdfjs.GlobalWorkerOptions.workerSrc = "/pdfjs/pdf.worker.min.mjs" +const pdfOptions = { cMapUrl: "/pdfjs/cmaps/", standardFontDataUrl: "/pdfjs/standard_fonts/", wasmUrl: "/pdfjs/wasm/" } + +export default function PdfViewer({ document }: { document: DocumentItem }) { + const [zoom, setZoom] = useState(1) + const [page, setPage] = useState(1) + const [pageCount, setPageCount] = useState(0) + const [width, setWidth] = useState(0) + const [failed, setFailed] = useState(false) + const viewport = useRef(null) + + useEffect(() => { + const element = viewport.current + if (!element) return + const observer = new ResizeObserver(([entry]) => { + if (entry) setWidth(Math.max(1, entry.contentRect.width - 32)) + }) + observer.observe(element) + return () => observer.disconnect() + }, []) + + return ( + +
+
+ +

{document.title}

+
+
+ + + + +
+
+

PDF dimostrativo, non ufficiale.

+
+ {width > 0 && ( + { + setPageCount(numPages) + setFailed(false) + }} + onLoadError={() => setFailed(true)} + loading={Caricamento del PDF…} + error={ +

+ Impossibile visualizzare il PDF. Puoi scaricarlo con il pulsante in alto. +

+ } + className="flex min-w-full justify-start p-4" + > + setFailed(true)} + loading={Caricamento della pagina…} + error={ +

+ Impossibile visualizzare questa pagina. Puoi scaricare il PDF con il pulsante in alto. +

+ } + className="mx-auto shrink-0 shadow-sm" + /> +
+ )} +
+ {pageCount > 0 && !failed && ( +
+ + + Pagina {page} di {pageCount} + + +
+ )} +
+ ) +} diff --git a/src/components/documents/types.ts b/src/components/documents/types.ts new file mode 100644 index 00000000..d265e037 --- /dev/null +++ b/src/components/documents/types.ts @@ -0,0 +1,12 @@ +export type DocumentItem = { + title: string + description?: string + href: string +} + +export type DocumentCategory = { + id: string + title: string + description: string + documents: DocumentItem[] +} diff --git a/src/components/documents/utils.ts b/src/components/documents/utils.ts new file mode 100644 index 00000000..77dd3e10 --- /dev/null +++ b/src/components/documents/utils.ts @@ -0,0 +1,9 @@ +import type { DocumentItem } from "./types" + +export function getDocumentSlug(document: DocumentItem) { + return document.href.slice(document.href.lastIndexOf("/") + 1).replace(/\.pdf$/, "") +} + +export function getDocumentViewerHref(document: DocumentItem) { + return `/documents/view/${getDocumentSlug(document)}` +} diff --git a/src/components/header/constants.ts b/src/components/header/constants.ts index 7cb314e9..c97f7672 100644 --- a/src/components/header/constants.ts +++ b/src/components/header/constants.ts @@ -8,6 +8,7 @@ export const headerMenuItems: HeaderMenuItem[] = [ { title: "Progetti", href: "/projects", icon: FiChevronRight }, { title: "Appunti", href: "https://polinetwork.org/it/materials/view/", icon: FiChevronRight }, { title: "Guide", href: "/guides", icon: FiChevronRight }, + { title: "Documenti", href: "/documents", icon: FiChevronRight }, { title: "FAQs", href: "/faqs", icon: FiChevronRight }, ], }, diff --git a/src/components/ui/hero.tsx b/src/components/ui/hero.tsx index bcd563e3..a254ceef 100644 --- a/src/components/ui/hero.tsx +++ b/src/components/ui/hero.tsx @@ -22,17 +22,24 @@ export function Hero({ description, gradientDescription = false, className, + titleClassName, }: { title: string description: string gradientDescription?: boolean className?: string + titleClassName?: string }) { return (
{/* Figma "lightheme-XLarge text effect": #020618 → #6e6e6e at 82.69% */} {/* The gradient is drawn per line (box-decoration-clone), so a title that wraps keeps the full effect on each line */} -

+

{title} diff --git a/tsconfig.json b/tsconfig.json index ca124295..326e81a0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -46,6 +46,7 @@ ".next/dev/types/**/*.ts" ], "exclude": [ - "node_modules" + "node_modules", + "public/pdfjs" ] }