From f1c4e896b6a2d92561e21bc737abe504a7bc00be Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Thu, 25 Jun 2026 22:15:06 +0300 Subject: [PATCH 01/10] test pdf viewer --- .../cypress/e2e/admin-element.cy.ts | 14 ++++---- frontend/rabbi_gerzi/cypress/e2e/media.cy.ts | 36 ++++++++++++++----- 2 files changed, 34 insertions(+), 16 deletions(-) diff --git a/frontend/rabbi_gerzi/cypress/e2e/admin-element.cy.ts b/frontend/rabbi_gerzi/cypress/e2e/admin-element.cy.ts index ab70619..bc4a7ae 100644 --- a/frontend/rabbi_gerzi/cypress/e2e/admin-element.cy.ts +++ b/frontend/rabbi_gerzi/cypress/e2e/admin-element.cy.ts @@ -281,14 +281,12 @@ describe('admin element editing', () => { .should('be.visible') .and('have.attr', 'src') .and('include', '/storage/element-icons/') - cy.get('[data-cy="element-short-pdf-link"]') + cy.get('[data-cy="element-short-pdf-viewer-button"]') .should('be.visible') - .and('have.attr', 'href') - .and('include', '/storage/element-pdfs/short/') - cy.get('[data-cy="element-long-pdf-link"]') + .and('contain.text', 'View Short PDF') + cy.get('[data-cy="element-long-pdf-viewer-button"]') .should('be.visible') - .and('have.attr', 'href') - .and('include', '/storage/element-pdfs/long/') + .and('contain.text', 'View Long PDF') cy.resetDb() }) @@ -341,8 +339,8 @@ describe('admin element editing', () => { cy.contains('header.site-header a', 'View Element').click() cy.get('[data-cy="element-icon"]').should('not.exist') - cy.get('[data-cy="element-short-pdf-link"]').should('not.exist') - cy.get('[data-cy="element-long-pdf-link"]').should('not.exist') + cy.get('[data-cy="element-short-pdf-viewer-button"]').should('not.exist') + cy.get('[data-cy="element-long-pdf-viewer-button"]').should('not.exist') cy.resetDb() }) diff --git a/frontend/rabbi_gerzi/cypress/e2e/media.cy.ts b/frontend/rabbi_gerzi/cypress/e2e/media.cy.ts index 805be28..0233789 100644 --- a/frontend/rabbi_gerzi/cypress/e2e/media.cy.ts +++ b/frontend/rabbi_gerzi/cypress/e2e/media.cy.ts @@ -51,8 +51,8 @@ describe('media page sets', () => { cy.get('[data-cy="element-rich-text"]').should('not.exist') cy.get('[data-cy="element-youtube-embed"]') .should('not.exist') - cy.get('[data-cy="element-short-pdf-link"]').should('not.exist') - cy.get('[data-cy="element-long-pdf-link"]').should('not.exist') + cy.get('[data-cy="element-short-pdf-viewer-button"]').should('not.exist') + cy.get('[data-cy="element-long-pdf-viewer-button"]').should('not.exist') cy.get('[data-cy="child-element-list"]').should('be.visible') cy.get('[data-cy="child-element-list"]') .should( @@ -117,14 +117,34 @@ describe('media page sets', () => { ) .should('have.css', 'text-align', 'center') cy.get('[data-cy="element-youtube-embed"]').should('not.exist') - cy.contains('[data-cy="element-short-pdf-link"]', 'View Short PDF') - .as('shortPdfLink') + cy.contains( + '[data-cy="element-short-pdf-viewer-button"]', + 'View Short PDF', + ) + .as('shortPdfButton') + .should('be.visible') + cy.get('[data-cy="element-long-pdf-viewer-button"]').should('not.exist') + cy.get('@shortPdfButton').click() + cy.get('[data-cy="pdf-viewer-modal"]').should('be.visible') + cy.contains('[data-cy="pdf-viewer-title"]', 'Short PDF').should( + 'be.visible', + ) + cy.get('[data-cy="pdf-viewer-document"]').should('be.visible') + cy.get('[data-cy="pdf-viewer-page-status"]').should('be.visible') + cy.get('[data-cy="pdf-viewer-previous-page"]').should('be.visible') + cy.get('[data-cy="pdf-viewer-next-page"]').should('be.visible') + cy.get('[data-cy="pdf-viewer-zoom-out"]').should('be.visible') + cy.get('[data-cy="pdf-viewer-zoom-in"]').should('be.visible') + cy.get('[data-cy="pdf-viewer-search-input"]').should('be.visible') + cy.get('[data-cy="pdf-viewer-download"]').should('be.visible') + cy.get('[data-cy="pdf-viewer-print"]').should('be.visible') + cy.get('[data-cy="pdf-viewer-open-link"]') .should('be.visible') .and('have.attr', 'target', '_blank') - cy.get('@shortPdfLink') .and('have.attr', 'href') .and('include', '/storage/element-pdfs/short/baderech.pdf') - cy.get('[data-cy="element-long-pdf-link"]').should('not.exist') + cy.get('[data-cy="pdf-viewer-close"]').click() + cy.get('[data-cy="pdf-viewer-modal"]').should('not.exist') cy.contains('[data-cy="child-element-link"]', introductionAudioTitle) .as('introductionAudioLink') .should('have.attr', 'href', '/element/8') @@ -134,8 +154,8 @@ describe('media page sets', () => { cy.contains('h1', introductionAudioTitle).should('be.visible') cy.get('[data-cy="element-icon"]').should('not.exist') cy.get('[data-cy="element-rich-text"]').should('not.exist') - cy.get('[data-cy="element-short-pdf-link"]').should('not.exist') - cy.get('[data-cy="element-long-pdf-link"]').should('not.exist') + cy.get('[data-cy="element-short-pdf-viewer-button"]').should('not.exist') + cy.get('[data-cy="element-long-pdf-viewer-button"]').should('not.exist') cy.get('[data-cy="child-element-list"]').should('not.exist') cy.get('[data-cy="element-youtube-embed"]') .should('be.visible') From 208c683dacb464927987451ea16d694583a70b91 Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Thu, 25 Jun 2026 22:39:21 +0300 Subject: [PATCH 02/10] add pdf viewer --- backend/app/Controllers/ElementController.php | 42 ++ backend/routes/api.php | 4 + frontend/rabbi_gerzi/cypress.config.ts | 18 + frontend/rabbi_gerzi/package-lock.json | 290 ++++++++ frontend/rabbi_gerzi/package.json | 1 + .../src/components/PdfViewerModal.vue | 662 ++++++++++++++++++ .../rabbi_gerzi/src/views/ElementPage.vue | 110 ++- 7 files changed, 1114 insertions(+), 13 deletions(-) create mode 100644 frontend/rabbi_gerzi/src/components/PdfViewerModal.vue diff --git a/backend/app/Controllers/ElementController.php b/backend/app/Controllers/ElementController.php index 8665880..e086df6 100644 --- a/backend/app/Controllers/ElementController.php +++ b/backend/app/Controllers/ElementController.php @@ -18,10 +18,14 @@ use App\Exceptions\NotFoundException; use App\Shared\Files\FileUploader; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; +use Illuminate\Http\Response; use Illuminate\Http\UploadedFile; +use Illuminate\Support\Facades\Storage; class ElementController { + private const PDF_FOLDERS = ['short', 'long']; + public function __construct( private GetElement $getElement, private CreateChildElement $createChildElement, @@ -64,6 +68,39 @@ class ElementController ], 200); } + public function showPdf( + string $folder, + string $fileName, + ): Response|JsonResponse { + if (!$this->isAllowedPdfFolder($folder)) { + return new JsonResponse([ + 'error' => 'PDF not found', + ], 404); + } + + if (!preg_match('/^[A-Za-z0-9._-]+\.pdf$/', $fileName)) { + return new JsonResponse([ + 'error' => 'PDF not found', + ], 404); + } + + $path = "element-pdfs/$folder/$fileName"; + if (!Storage::disk('public')->exists($path)) { + return new JsonResponse([ + 'error' => 'PDF not found', + ], 404); + } + + return new Response( + Storage::disk('public')->get($path), + 200, + [ + 'Content-Type' => 'application/pdf', + 'Content-Disposition' => "inline; filename=\"$fileName\"", + ], + ); + } + public function createChild(Request $request, ?int $parentId): JsonResponse { try { @@ -372,4 +409,9 @@ class ElementController return $path; } + + private function isAllowedPdfFolder(string $folder): bool + { + return in_array($folder, self::PDF_FOLDERS, true); + } } diff --git a/backend/routes/api.php b/backend/routes/api.php index f084592..82a3af4 100644 --- a/backend/routes/api.php +++ b/backend/routes/api.php @@ -12,6 +12,10 @@ Route::get('/me', [AuthController::class, 'me']) ->middleware(AuthMiddleware::class); Route::get('/sets', [SetController::class, 'index']); Route::get('/elements/{id}', [ElementController::class, 'show']); +Route::get('/element-pdfs/{folder}/{fileName}', [ + ElementController::class, + 'showPdf', +]); Route::post('/elements/{parentId}/children', [ ElementController::class, 'createChild', diff --git a/frontend/rabbi_gerzi/cypress.config.ts b/frontend/rabbi_gerzi/cypress.config.ts index 82a9470..1254b43 100644 --- a/frontend/rabbi_gerzi/cypress.config.ts +++ b/frontend/rabbi_gerzi/cypress.config.ts @@ -1,4 +1,5 @@ import { execSync } from 'node:child_process' +import { cpSync, mkdirSync, rmSync } from 'node:fs' import { dirname, resolve } from 'node:path' import { fileURLToPath } from 'node:url' import { defineConfig } from 'cypress' @@ -7,7 +8,9 @@ import { Client } from 'pg' const configDirectory = dirname(fileURLToPath(import.meta.url)) const backendDirectory = resolve(configDirectory, '../../backend') +const seedAssetDirectory = resolve(backendDirectory, 'database/seeders/assets') const socketDirectory = resolve(configDirectory, '../../.postgres') +const storagePublicDirectory = resolve(backendDirectory, 'storage/app/public') const appDatabase = 'postgres' const templateDatabase = 'postgres_cypress_template' @@ -47,6 +50,7 @@ async function buildTemplate(): Promise { stdio: 'pipe', env: seedEnvironment, }) + resetSeedStorage() const markClient = controlClient() await markClient.connect() @@ -72,10 +76,24 @@ async function resetFromTemplate(): Promise { TEMPLATE ${templateDatabase}`, ) await client.end() + resetSeedStorage() return null } +function resetSeedStorage(): void { + rmSync(resolve(storagePublicDirectory, 'element-icons'), { + recursive: true, + force: true, + }) + rmSync(resolve(storagePublicDirectory, 'element-pdfs'), { + recursive: true, + force: true, + }) + mkdirSync(storagePublicDirectory, { recursive: true }) + cpSync(seedAssetDirectory, storagePublicDirectory, { recursive: true }) +} + export default defineConfig({ e2e: { baseUrl: 'http://localhost:5173', diff --git a/frontend/rabbi_gerzi/package-lock.json b/frontend/rabbi_gerzi/package-lock.json index 6a19774..707bb2c 100644 --- a/frontend/rabbi_gerzi/package-lock.json +++ b/frontend/rabbi_gerzi/package-lock.json @@ -19,6 +19,7 @@ "lucide-vue-next": "^1.0.0", "pinia": "^3.0.4", "vue": "beta", + "vue-pdf-embed": "^2.1.5", "vue-router": "^5.0.4" }, "devDependencies": { @@ -1275,6 +1276,271 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@napi-rs/canvas": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.100.tgz", + "integrity": "sha512-xglYA6q3XO5P3BNJYxVZ1IV7DLVjp1Py6nwag88YntrS+3vKHyYcMqXVS4ZztJmwz2uGvz1FWhI/4LgbR5uQDA==", + "license": "MIT", + "optional": true, + "workspaces": [ + "e2e/*" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@napi-rs/canvas-android-arm64": "0.1.100", + "@napi-rs/canvas-darwin-arm64": "0.1.100", + "@napi-rs/canvas-darwin-x64": "0.1.100", + "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.100", + "@napi-rs/canvas-linux-arm64-gnu": "0.1.100", + "@napi-rs/canvas-linux-arm64-musl": "0.1.100", + "@napi-rs/canvas-linux-riscv64-gnu": "0.1.100", + "@napi-rs/canvas-linux-x64-gnu": "0.1.100", + "@napi-rs/canvas-linux-x64-musl": "0.1.100", + "@napi-rs/canvas-win32-arm64-msvc": "0.1.100", + "@napi-rs/canvas-win32-x64-msvc": "0.1.100" + } + }, + "node_modules/@napi-rs/canvas-android-arm64": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.100.tgz", + "integrity": "sha512-hjhCKhntPv9+t4ckHymdx0phYNcVW+GKQR6Lzw2zE+pOVjOplSmtx9nNNknTjbEDLcuLZqA1y8ufKg1XfgftzQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-darwin-arm64": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.100.tgz", + "integrity": "sha512-2PcswRaC7Ly645DGt88///zuFDhJxJYdKAs1uU3mfk1atYkXufgcgLfBpk6Tm12nCQBaNt1wpybuPZ4qOhTo8A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-darwin-x64": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.100.tgz", + "integrity": "sha512-ePNZtj7pNIva/siZMg+HmbeozkIjqUIYdoymH8HaA3qK7LfzFN4WMBM8G6HQ9ZC+H3+Dnn5pqtiXpgLykaPOhw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.100.tgz", + "integrity": "sha512-d5cDB48oWFGU8/XPhUOFAlySgb/VAu7D+s8fi55K1Pcfg8aPplHWqMgibhVLU8ky7Pyg/fuiVLz4Nf3JrSTuUA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-arm64-gnu": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.100.tgz", + "integrity": "sha512-rDxgxRu69RvDlX/bh9o22DxLsGr8EqsNgotL9+RwQE1S0b0cqeatqsw6aW45mukm0B42DIAaAacKaYQ8cqS1nw==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-arm64-musl": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.100.tgz", + "integrity": "sha512-K3mDW66N+xT2/V439u1alFANiBUjdEx2gLiNYnCmUsva5jZMxWTjafBYwTzYK+EMFMHrUoabuU+T1BIP5CgbYQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-riscv64-gnu": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.100.tgz", + "integrity": "sha512-mooqUBTIsccZpnoQC4NgrC1v6C1vof39etLNMnBwCY+p0gajWJvAHLGQ6g/gGyS5YrpDW+GefSN4+Cvcr08UWw==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-x64-gnu": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.100.tgz", + "integrity": "sha512-1eCvkDCazm7FFhsT7DfGOdSaHgZVK3bt/dSBl5EWHOWmnz+I7j8tPseJqqD81NF+MH21jKUK4wQSDjN0mdhnTg==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-x64-musl": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.100.tgz", + "integrity": "sha512-20arT6lnI19S68qNlii73TSEDbECNgzMz2EpldC1V3mZFuRkeujXkcebRk0LRJe9SEUAooYiLokfMViY8IX7yA==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-win32-arm64-msvc": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-0.1.100.tgz", + "integrity": "sha512-DZFFT1wIAg37LJw37yhMRFfjATd3vTQzjZ1Yki8u2vhO6Hi5VE6BVaGQ1aaDu7xb4iMErz+9EOwjpS7xcxFeBw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-win32-x64-msvc": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.100.tgz", + "integrity": "sha512-MyT1j3mHC2+Lu4pBi9mKyMJhtP6U7k7EldY7sj/uS5gJA65gTXt8MefJQXLJo5d/vZbuWmfxzkEUNc/urV3pHA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", @@ -7007,6 +7273,18 @@ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "license": "MIT" }, + "node_modules/pdfjs-dist": { + "version": "5.7.284", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.7.284.tgz", + "integrity": "sha512-h4EdYQczmGhbOlqc3PPZwxevn7ApdWPbovAuWXOB/DjIyigSnwfy2oze7c6mRcSr9XgLp3eN3EeL4DyySTPMFw==", + "license": "Apache-2.0", + "engines": { + "node": ">=22.13.0 || >=24" + }, + "optionalDependencies": { + "@napi-rs/canvas": "^0.1.100" + } + }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -8784,6 +9062,18 @@ "node": ">=10" } }, + "node_modules/vue-pdf-embed": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/vue-pdf-embed/-/vue-pdf-embed-2.1.5.tgz", + "integrity": "sha512-IGFVBYlnOz2zSql1zk4YJyBu584EZa6RUykk5f8wkHF/AR31khCa+ruJoRag+Ff2UyntkWu0brENIKoikQ7F8g==", + "license": "MIT", + "dependencies": { + "pdfjs-dist": "^5.7.284" + }, + "peerDependencies": { + "vue": "^3.3.0" + } + }, "node_modules/vue-router": { "version": "5.0.7", "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.0.7.tgz", diff --git a/frontend/rabbi_gerzi/package.json b/frontend/rabbi_gerzi/package.json index 50ba0ca..c8ca08b 100644 --- a/frontend/rabbi_gerzi/package.json +++ b/frontend/rabbi_gerzi/package.json @@ -28,6 +28,7 @@ "lucide-vue-next": "^1.0.0", "pinia": "^3.0.4", "vue": "beta", + "vue-pdf-embed": "^2.1.5", "vue-router": "^5.0.4" }, "devDependencies": { diff --git a/frontend/rabbi_gerzi/src/components/PdfViewerModal.vue b/frontend/rabbi_gerzi/src/components/PdfViewerModal.vue new file mode 100644 index 0000000..ebfbbaf --- /dev/null +++ b/frontend/rabbi_gerzi/src/components/PdfViewerModal.vue @@ -0,0 +1,662 @@ + + + + + diff --git a/frontend/rabbi_gerzi/src/views/ElementPage.vue b/frontend/rabbi_gerzi/src/views/ElementPage.vue index c2ed707..b0aa452 100644 --- a/frontend/rabbi_gerzi/src/views/ElementPage.vue +++ b/frontend/rabbi_gerzi/src/views/ElementPage.vue @@ -1,16 +1,26 @@