diff --git a/app/Http/Controllers/TrainingRoadmapPdfController.php b/app/Http/Controllers/TrainingRoadmapPdfController.php new file mode 100644 index 000000000..72899ccd0 --- /dev/null +++ b/app/Http/Controllers/TrainingRoadmapPdfController.php @@ -0,0 +1,51 @@ +get(self::ROADMAP_SOURCE); + + if (! $response->successful()) { + abort(502, 'Unable to load roadmap PDF.'); + } + + return $response->body(); + } + ); + + return response($bytes, 200, [ + 'Content-Type' => 'application/pdf', + 'Content-Disposition' => 'inline; filename="DDP_toolkit_roadmap.pdf"', + 'Cache-Control' => 'public, max-age=3600', + ]); + } + + /** + * Minimal PDF.js viewer (no site chrome) for iframe embedding on training pages. + */ + public function viewer() + { + return response() + ->view('training.roadmap-pdfjs') + ->header('X-Frame-Options', 'SAMEORIGIN'); + } +} diff --git a/app/Nova/TrainingResource.php b/app/Nova/TrainingResource.php index 93c404fe6..d907c53b5 100644 --- a/app/Nova/TrainingResource.php +++ b/app/Nova/TrainingResource.php @@ -6,8 +6,10 @@ use Illuminate\Http\Request; use Illuminate\Support\Facades\URL; use Laravel\Nova\Fields\Boolean; +use Laravel\Nova\Fields\FormData; use Laravel\Nova\Fields\ID; use Laravel\Nova\Fields\Number; +use Laravel\Nova\Fields\Select; use Laravel\Nova\Fields\Text; use Laravel\Nova\Fields\Textarea; use Laravel\Nova\Fields\Trix; @@ -153,10 +155,38 @@ public function fields(Request $request): array ->nullable() ->help('Optional scroll offset in pixels for in-page anchor links (useful with sticky headers).'), + Select::make('Roadmap embed format', 'roadmap_embed_kind') + ->options([ + 'pdf' => 'PDF (inline viewer)', + 'svg' => 'SVG (paste markup below)', + 'none' => 'None (remove placeholder output)', + ]) + ->default('pdf') + ->help('Use [[embed_roadmap_pdf]] or [[embed_roadmap]] in Content where the roadmap should appear.'), + Text::make('Roadmap PDF embed URL', 'roadmap_pdf_embed_url') ->nullable() ->rules('nullable', 'url') - ->help('Optional HTTPS URL to a PDF shown inline in the Roadmap section. Put the literal text [[embed_roadmap_pdf]] in Content where the embed should appear (avoids Nova stripping iframes).'), + ->dependsOn(['roadmap_embed_kind'], function (Text $field, NovaRequest $request, FormData $formData) { + if (($formData->roadmap_embed_kind ?? 'pdf') === 'pdf') { + $field->show(); + } else { + $field->hide(); + } + }) + ->help('HTTPS URL to the roadmap PDF. Used when format is PDF.'), + + Textarea::make('Roadmap SVG', 'roadmap_svg') + ->nullable() + ->rows(10) + ->dependsOn(['roadmap_embed_kind'], function (Textarea $field, NovaRequest $request, FormData $formData) { + if (($formData->roadmap_embed_kind ?? 'pdf') === 'svg') { + $field->show(); + } else { + $field->hide(); + } + }) + ->help('Full ... markup from your design tool. Used when format is SVG.'), Text::make('Button text', 'button_text')->nullable(), diff --git a/app/TrainingResource.php b/app/TrainingResource.php index 56a438517..fabc3255c 100644 --- a/app/TrainingResource.php +++ b/app/TrainingResource.php @@ -35,6 +35,8 @@ class TrainingResource extends Model 'about_box_section', 'anchor_offset', 'roadmap_pdf_embed_url', + 'roadmap_embed_kind', + 'roadmap_svg', 'button_text', 'button_url', 'secondary_button_text', diff --git a/database/migrations/2026_05_01_140000_add_roadmap_embed_kind_and_svg_to_training_resources_table.php b/database/migrations/2026_05_01_140000_add_roadmap_embed_kind_and_svg_to_training_resources_table.php new file mode 100644 index 000000000..b698d5a50 --- /dev/null +++ b/database/migrations/2026_05_01_140000_add_roadmap_embed_kind_and_svg_to_training_resources_table.php @@ -0,0 +1,23 @@ +string('roadmap_embed_kind', 16)->default('pdf')->after('roadmap_pdf_embed_url'); + $table->mediumText('roadmap_svg')->nullable()->after('roadmap_embed_kind'); + }); + } + + public function down(): void + { + Schema::table('training_resources', function (Blueprint $table) { + $table->dropColumn(['roadmap_embed_kind', 'roadmap_svg']); + }); + } +}; diff --git a/database/seeders/TrainingResourceDiscoverDigitalProgrammeSeeder.php b/database/seeders/TrainingResourceDiscoverDigitalProgrammeSeeder.php index ba3063414..44c1695d5 100644 --- a/database/seeders/TrainingResourceDiscoverDigitalProgrammeSeeder.php +++ b/database/seeders/TrainingResourceDiscoverDigitalProgrammeSeeder.php @@ -91,6 +91,8 @@ public function run(): void HTML, 'anchor_offset' => 120, 'roadmap_pdf_embed_url' => 'https://codeweek-resources.s3.eu-west-1.amazonaws.com/+discover-digital-toolkit/DDP_toolkit_roadmap.pdf', + 'roadmap_embed_kind' => 'pdf', + 'roadmap_svg' => null, 'third_button_text' => 'Register an activity', 'third_button_url' => 'https://codeweek.eu/add?skip=1', 'meta_title' => 'Discover Digital Programme - Toolkit', diff --git a/resources/views/training/partials/roadmap-pdf-embed.blade.php b/resources/views/training/partials/roadmap-pdf-embed.blade.php index 219dd8d95..c037de629 100644 --- a/resources/views/training/partials/roadmap-pdf-embed.blade.php +++ b/resources/views/training/partials/roadmap-pdf-embed.blade.php @@ -1,17 +1,28 @@ @props([ 'url', ]) +@php + // Strip any fragment from stored URL for the "open in new tab" link. + $tabUrl = \Illuminate\Support\Str::before($url, '#'); + // Same-origin PDF.js viewer for the allowlisted DDP roadmap (see TrainingRoadmapPdfController). + // Other URLs keep the direct PDF iframe with viewer chrome hints. + $ddpRoadmapBase = 'https://codeweek-resources.s3.eu-west-1.amazonaws.com/+discover-digital-toolkit/DDP_toolkit_roadmap.pdf'; + $usePdfjsViewer = strtolower(rtrim($tabUrl, '/')) === strtolower(rtrim($ddpRoadmapBase, '/')); + $fragment = '#toolbar=0&navpanes=0&scrollbar=1&view=FitH'; + $embedSrc = $usePdfjsViewer ? route('training.roadmap_pdf_viewer') : $tabUrl.$fragment; +@endphp

- + {{ __('Open roadmap PDF in a new tab') }} — {{ __('if the preview does not load in your browser.') }} diff --git a/resources/views/training/partials/roadmap-svg-embed.blade.php b/resources/views/training/partials/roadmap-svg-embed.blade.php new file mode 100644 index 000000000..f6582e5bb --- /dev/null +++ b/resources/views/training/partials/roadmap-svg-embed.blade.php @@ -0,0 +1,6 @@ +@props([ + 'svg', +]) +

+ {!! $svg !!} +
diff --git a/resources/views/training/roadmap-pdfjs.blade.php b/resources/views/training/roadmap-pdfjs.blade.php new file mode 100644 index 000000000..54869ad6f --- /dev/null +++ b/resources/views/training/roadmap-pdfjs.blade.php @@ -0,0 +1,65 @@ + + + + + + + {{ __('Roadmap') }} + + + + +
+ + + + diff --git a/resources/views/training/show.blade.php b/resources/views/training/show.blade.php index 51c0aac98..0faeeb184 100644 --- a/resources/views/training/show.blade.php +++ b/resources/views/training/show.blade.php @@ -11,7 +11,7 @@ $pageDescription = $trainingResource->meta_description ?: $fallbackDescription; $introClass = "text-[#20262C] font-normal text-lg md:text-xl p-0 mb-6 [&_p]:p-0 [&_p]:mb-6 [&_a]:text-dark-blue [&_a]:hover:underline"; - $contentClass = "text-[#333E48] font-normal text-lg md:text-xl p-0 mb-6 [&_p]:p-0 [&_p]:mb-6 [&_h2]:text-dark-blue [&_h2]:text-2xl [&_h2]:md:text-3xl [&_h2]:leading-[44px] [&_h2]:font-medium [&_h2]:font-['Montserrat'] [&_h2]:mb-4 [&_h3]:text-dark-blue [&_h3]:text-xl [&_h3]:md:text-2xl [&_h3]:font-medium [&_h3]:font-['Montserrat'] [&_h3]:mb-4 [&_ul]:pl-8 [&_ul]:m-0 [&_ul]:mb-6 [&_ul]:list-disc [&_ol]:pl-8 [&_ol]:m-0 [&_ol]:mb-6 [&_ol]:list-decimal [&_li]:p-0 [&_li]:text-lg [&_li]:font-normal [&_li]:leading-7 [&_li]:text-default [&_a]:text-dark-blue [&_a]:hover:underline [&_img]:max-w-full [&_img]:w-auto [&_img]:h-auto [&_img]:my-8 [&_img]:mx-auto [&_img]:block"; + $contentClass = "text-[#333E48] font-normal text-lg md:text-xl p-0 mb-6 min-w-0 [&_p]:p-0 [&_p]:mb-6 [&_h2]:text-dark-blue [&_h2]:text-2xl [&_h2]:md:text-3xl [&_h2]:leading-[44px] [&_h2]:font-medium [&_h2]:font-['Montserrat'] [&_h2]:mb-4 [&_h3]:text-dark-blue [&_h3]:text-xl [&_h3]:md:text-2xl [&_h3]:font-medium [&_h3]:font-['Montserrat'] [&_h3]:mb-4 [&_ul]:pl-8 [&_ul]:m-0 [&_ul]:mb-6 [&_ul]:list-disc [&_ol]:pl-8 [&_ol]:m-0 [&_ol]:mb-6 [&_ol]:list-decimal [&_li]:p-0 [&_li]:text-lg [&_li]:font-normal [&_li]:leading-7 [&_li]:text-default [&_a]:text-dark-blue [&_a]:hover:underline [&_a]:break-words [&_img]:max-w-full [&_img]:w-auto [&_img]:h-auto [&_img]:my-8 [&_img]:mx-auto [&_img]:block [&_svg]:max-w-full [&_svg]:h-auto [&_svg]:block"; $pdfClass = "text-[#333E48] font-normal text-lg md:text-xl p-0 mb-6 [&_p]:p-0 [&_p]:mb-4 [&_h2]:text-dark-blue [&_h2]:text-2xl [&_h2]:md:text-3xl [&_h2]:leading-[44px] [&_h2]:font-medium [&_h2]:font-['Montserrat'] [&_h2]:mb-4 [&_ul]:m-0 [&_ul]:mb-6 [&_ul]:list-none [&_ol]:m-0 [&_ol]:mb-6 [&_ol]:list-none [&_li]:p-0 [&_li]:mb-2 [&_li]:font-normal [&_li]:leading-7 [&_li]:text-default [&_li]:break-words [&_a]:text-lg [&_a]:text-dark-blue [&_a]:no-underline [&_a:hover]:underline [&_a]:break-words [&_a]:max-w-full"; $contactsClass = "text-[#333E48] font-normal text-lg md:text-xl p-0 mb-8 [&_p]:p-0 [&_p]:mb-4 [&_h2]:text-dark-blue [&_h2]:text-2xl [&_h2]:md:text-3xl [&_h2]:leading-[44px] [&_h2]:font-medium [&_h2]:font-['Montserrat'] [&_h2]:mb-4 [&_a]:text-dark-blue [&_a]:hover:underline"; $registerClass = "text-[#333E48] font-normal text-base md:text-lg [&_p]:p-0 [&_p]:mb-4 [&_p:last-child]:mb-0 [&_a]:font-medium [&_a]:text-dark-blue [&_a]:hover:underline"; @@ -26,14 +26,29 @@ $renderedContent = $trainingResource->content ?? ''; $roadmapEmbedUrl = trim((string) ($trainingResource->roadmap_pdf_embed_url ?? '')); - if ($roadmapEmbedUrl !== '' && str_contains($renderedContent, '[[embed_roadmap_pdf]]')) { - $renderedContent = str_replace( - '[[embed_roadmap_pdf]]', - view('training.partials.roadmap-pdf-embed', ['url' => $roadmapEmbedUrl])->render(), - $renderedContent - ); - } elseif (str_contains($renderedContent, '[[embed_roadmap_pdf]]')) { - $renderedContent = str_replace('[[embed_roadmap_pdf]]', '', $renderedContent); + $roadmapEmbedKind = strtolower(trim((string) ($trainingResource->roadmap_embed_kind ?? 'pdf'))); + if (! in_array($roadmapEmbedKind, ['pdf', 'svg', 'none'], true)) { + $roadmapEmbedKind = 'pdf'; + } + $roadmapSvg = trim((string) ($trainingResource->roadmap_svg ?? '')); + + $roadmapPlaceholders = ['[[embed_roadmap_pdf]]', '[[embed_roadmap]]']; + $hasRoadmapPlaceholder = str_contains($renderedContent, '[[embed_roadmap_pdf]]') + || str_contains($renderedContent, '[[embed_roadmap]]'); + + if ($hasRoadmapPlaceholder) { + $roadmapEmbedHtml = ''; + if ($roadmapEmbedKind === 'pdf' && $roadmapEmbedUrl !== '') { + $roadmapEmbedHtml = view('training.partials.roadmap-pdf-embed', ['url' => $roadmapEmbedUrl])->render(); + } elseif ($roadmapEmbedKind === 'svg' && $roadmapSvg !== '') { + $roadmapEmbedHtml = view('training.partials.roadmap-svg-embed', ['svg' => $roadmapSvg])->render(); + } + + foreach ($roadmapPlaceholders as $token) { + if (str_contains($renderedContent, $token)) { + $renderedContent = str_replace($token, $roadmapEmbedHtml, $renderedContent); + } + } } @endphp @@ -189,6 +204,31 @@ class="inline-block rounded-full py-2.5 px-6 border border-[#1C4DA1] text-[#1C4D @endsection +@push('scripts') + +@endpush + @if($anchorOffset > 0) @push('scripts')