From 009b93fa7628c2f94958d906c13932ce3465d5da Mon Sep 17 00:00:00 2001 From: bernardhanna Date: Wed, 29 Apr 2026 11:13:14 +0100 Subject: [PATCH] Add DDP training resource layout controls for preview setup. Introduce optional hero CTAs, an about info card, and sticky-header anchor offsets so the Discover Digital Programme page can match the requested structure without changing existing resources unless configured. Made-with: Cursor --- .gitignore | 1 + app/Nova/TrainingResource.php | 26 ++++ app/TrainingResource.php | 7 + ...ton_fields_to_training_resources_table.php | 36 +++++ ...hor_offset_to_training_resources_table.php | 32 ++++ ...ResourceDiscoverDigitalProgrammeSeeder.php | 106 +++++++++++++ resources/views/codingathome/banner.blade.php | 33 ++++ resources/views/training/show.blade.php | 147 +++++++++++++----- 8 files changed, 352 insertions(+), 36 deletions(-) create mode 100644 database/migrations/2026_04_29_110000_add_hero_button_fields_to_training_resources_table.php create mode 100644 database/migrations/2026_04_29_113000_add_about_box_and_anchor_offset_to_training_resources_table.php create mode 100644 database/seeders/TrainingResourceDiscoverDigitalProgrammeSeeder.php diff --git a/.gitignore b/.gitignore index 8c2a178d7..c74360ae0 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,4 @@ vue-i18n-locales.generated.js vite.config.js .DS_Store resources/excel/example.xlsx +resources/ai-agent-plans/* diff --git a/app/Nova/TrainingResource.php b/app/Nova/TrainingResource.php index 1c5e35bce..5cc0138b5 100644 --- a/app/Nova/TrainingResource.php +++ b/app/Nova/TrainingResource.php @@ -82,6 +82,22 @@ public function fields(Request $request): array ->nullable() ->help('Optional pill text in the header banner'), + Text::make('Hero button text', 'hero_button_text') + ->nullable() + ->help('Optional primary CTA shown in the hero section.'), + + Text::make('Hero button URL', 'hero_button_url') + ->nullable() + ->help('Supports full URLs, root-relative paths, or #anchors.'), + + Text::make('Hero secondary button text', 'hero_secondary_button_text') + ->nullable() + ->help('Optional outline CTA shown beside the hero primary button.'), + + Text::make('Hero secondary button URL', 'hero_secondary_button_url') + ->nullable() + ->help('Supports full URLs, root-relative paths, or #anchors.'), + Trix::make('Intro', 'intro') ->nullable() ->help('Optional intro block shown above the main content'), @@ -126,6 +142,16 @@ public function fields(Request $request): array ->nullable() ->help('Optional text shown in a highlighted callout box (register on map, hashtags, etc).'), + Trix::make('About box section', 'about_box_section') + ->nullable() + ->help('Optional blue info card shown below register box (supports heading, text, lists).'), + + Number::make('Anchor offset', 'anchor_offset') + ->min(0) + ->step(1) + ->nullable() + ->help('Optional scroll offset in pixels for in-page anchor links (useful with sticky headers).'), + Text::make('Button text', 'button_text')->nullable(), Text::make('Button URL', 'button_url') diff --git a/app/TrainingResource.php b/app/TrainingResource.php index 1c98026a9..58774377c 100644 --- a/app/TrainingResource.php +++ b/app/TrainingResource.php @@ -17,6 +17,10 @@ class TrainingResource extends Model 'card_image', 'page_title', 'hero_author', + 'hero_button_text', + 'hero_button_url', + 'hero_secondary_button_text', + 'hero_secondary_button_url', 'intro', 'highlight_box', 'video_url', @@ -28,6 +32,8 @@ class TrainingResource extends Model 'pdf_links_section', 'contacts_section', 'register_box_section', + 'about_box_section', + 'anchor_offset', 'button_text', 'button_url', 'secondary_button_text', @@ -43,6 +49,7 @@ class TrainingResource extends Model protected $casts = [ 'active' => 'boolean', 'position' => 'integer', + 'anchor_offset' => 'integer', ]; public function scopeActive($query) diff --git a/database/migrations/2026_04_29_110000_add_hero_button_fields_to_training_resources_table.php b/database/migrations/2026_04_29_110000_add_hero_button_fields_to_training_resources_table.php new file mode 100644 index 000000000..3af1f7067 --- /dev/null +++ b/database/migrations/2026_04_29_110000_add_hero_button_fields_to_training_resources_table.php @@ -0,0 +1,36 @@ +string('hero_button_text')->nullable()->after('hero_author'); + $table->string('hero_button_url')->nullable()->after('hero_button_text'); + $table->string('hero_secondary_button_text')->nullable()->after('hero_button_url'); + $table->string('hero_secondary_button_url')->nullable()->after('hero_secondary_button_text'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('training_resources', function (Blueprint $table) { + $table->dropColumn([ + 'hero_button_text', + 'hero_button_url', + 'hero_secondary_button_text', + 'hero_secondary_button_url', + ]); + }); + } +}; diff --git a/database/migrations/2026_04_29_113000_add_about_box_and_anchor_offset_to_training_resources_table.php b/database/migrations/2026_04_29_113000_add_about_box_and_anchor_offset_to_training_resources_table.php new file mode 100644 index 000000000..d2787ed6e --- /dev/null +++ b/database/migrations/2026_04_29_113000_add_about_box_and_anchor_offset_to_training_resources_table.php @@ -0,0 +1,32 @@ +longText('about_box_section')->nullable()->after('register_box_section'); + $table->unsignedInteger('anchor_offset')->nullable()->after('about_box_section'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('training_resources', function (Blueprint $table) { + $table->dropColumn([ + 'about_box_section', + 'anchor_offset', + ]); + }); + } +}; diff --git a/database/seeders/TrainingResourceDiscoverDigitalProgrammeSeeder.php b/database/seeders/TrainingResourceDiscoverDigitalProgrammeSeeder.php new file mode 100644 index 000000000..7507befe8 --- /dev/null +++ b/database/seeders/TrainingResourceDiscoverDigitalProgrammeSeeder.php @@ -0,0 +1,106 @@ + 'discover-digital-programme'], + [ + 'card_title' => 'Discover Digital Programme', + 'card_author' => 'Code4Europe | Deliverable D4.2 | Public toolkit', + // Temporary thumbnail until final artwork is provided. + 'card_image' => '/images/banner_training.svg', + 'page_title' => 'Discover Digital Programme', + 'hero_author' => 'Code4Europe | Deliverable D4.2 | Public toolkit', + 'hero_button_text' => 'Open the complete toolkit', + 'hero_button_url' => 'https://codeweek-resources.s3.eu-west-1.amazonaws.com/+discover-digital-toolkit/DDP_toolkit.pdf', + 'hero_secondary_button_text' => 'Get the one-pagers pack', + 'hero_secondary_button_url' => '#key-one-pagers', + 'intro' => <<A practical toolkit for higher education institutions (HEIs) with an ICT and STEM focus to engage secondary school students through school visits, campus experiences, and digital outreach, supporting learners in the senior cycle of secondary education to imagine themselves in technology careers and understand the academic pathways that will lead them there.

+HTML, + 'content' => <<How the toolkit works +

The toolkit is organised as a flexible, non-prescriptive set of steps and formats. It structures engagement into two mirroring frameworks: STEM On Tour (bringing higher education into schools and community settings) and STEM In (bringing students into a higher education institution through campus visits, open days, and themed events). HEIs may implement either framework independently or combine both for greater impact.

+

Start here

+
    +
  1. Establish essential preparation (contacts + GDPR).
  2. +
  3. Choose your delivery format: STEM On Tour and/or STEM In.
  4. +
  5. Plan evaluation and feedback to measure outcomes.
  6. +
  7. Use digital outreach and communication to build awareness and follow-up.
  8. +
+
+

Roadmap

+

Use this roadmap if you do not have time to read the full deliverable. The one-pagers mirror the toolkit flow and provide a practical checklist for implementation.

+ + Discover Digital Programme roadmap + +
+HTML, + 'body_image' => null, + 'body_image_alt' => 'Discover Digital Programme roadmap', + 'pdf_links_section' => <<Key one-pagers +

These documents summarise the main operational parts of the toolkit.

+ +

Useful detail info

+

These documents provide the supporting operational detail referenced in the key one-pagers.

+ +HTML, + 'button_text' => 'Open the complete toolkit', + 'button_url' => 'https://codeweek-resources.s3.eu-west-1.amazonaws.com/+discover-digital-toolkit/DDP_toolkit.pdf', + 'secondary_button_text' => 'Download printable materials', + 'secondary_button_url' => '#key-one-pagers', + 'contacts_section' => null, + 'register_box_section' => <<Share your activity and build the pipeline +

Register school visits, campus visits, open days, workshops, and outreach actions on the EU Code Week platform to support visibility, coordination, and cumulative impact across the ecosystem.

+HTML, + 'about_box_section' => <<About this toolkit +

The programme serves as a bridge between secondary and higher education at a critical stage in the student journey. It addresses two common challenges: students who feel overwhelmed by the range of choices available and students who disengage because higher education feels distant or irrelevant. The toolkit provides structured guidance, adaptable frameworks and models, and opportunities to experience what a future educational path in STEM can look and feel like, while also engaging families and key stakeholders within secondary education.

+

At a glance

+ +HTML, + 'anchor_offset' => 120, + 'third_button_text' => 'Register an activity', + 'third_button_url' => 'https://codeweek.eu/add?skip=1', + 'meta_title' => 'Discover Digital Programme - Toolkit', + 'meta_description' => 'A practical toolkit for HEIs to engage secondary school students through STEM outreach, campus experiences, and digital communication.', + 'position' => 50, + // Keep unpublished so it is visible only via signed preview URL. + 'active' => false, + ] + ); + } +} diff --git a/resources/views/codingathome/banner.blade.php b/resources/views/codingathome/banner.blade.php index fd4576613..d9eda5a70 100644 --- a/resources/views/codingathome/banner.blade.php +++ b/resources/views/codingathome/banner.blade.php @@ -1,5 +1,14 @@ @php $author = $author ?? 'By EUCodeWeek'; + $primaryButtonText = $primaryButtonText ?? null; + $primaryButtonUrl = $primaryButtonUrl ?? null; + $secondaryButtonText = $secondaryButtonText ?? null; + $secondaryButtonUrl = $secondaryButtonUrl ?? null; + $isExternalLink = static function (?string $url): bool { + $url = trim((string) $url); + + return \Illuminate\Support\Str::startsWith($url, ['http://', 'https://', '//']); + }; @endphp
@@ -10,6 +19,30 @@

{{$title}}

+ @if(!empty($primaryButtonText) && !empty($primaryButtonUrl)) +
+ + {{ $primaryButtonText }} + + @if(!empty($secondaryButtonText) && !empty($secondaryButtonUrl)) + + {{ $secondaryButtonText }} + + @endif +
+ @endif @if ($author)
{{$author}} diff --git a/resources/views/training/show.blade.php b/resources/views/training/show.blade.php index ca23b33f1..93e392699 100644 --- a/resources/views/training/show.blade.php +++ b/resources/views/training/show.blade.php @@ -11,10 +11,18 @@ $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 [&_ul]:pl-8 [&_ul]:m-0 [&_ul]:mb-6 [&_ul]:list-disc [&_li]:p-0 [&_li]:text-lg [&_li]:font-normal [&_li]:leading-7 [&_li]:text-default [&_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]:w-full [&_img]:h-auto [&_img]:my-8"; $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 [&_a]:text-lg [&_a]:text-dark-blue [&_a]:no-underline [&_a:hover]:underline"; $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"; + $aboutBoxClass = "text-slate-500 text-[16px] leading-[22px] tablet:text-xl tablet:leading-7 [&_h2]:text-dark-blue [&_h2]:text-2xl [&_h2]:md:text-3xl [&_h2]:leading-[44px] [&_h2]:font-medium [&_h2]:font-['Montserrat'] [&_h2]:mb-3 [&_h3]:text-dark-blue [&_h3]:text-xl [&_h3]:md:text-2xl [&_h3]:font-medium [&_h3]:font-['Montserrat'] [&_h3]:mb-3 [&_p]:p-0 [&_p]:mb-3 [&_p:last-child]:mb-0 [&_ul]:pl-8 [&_ul]:m-0 [&_ul]:mb-2 [&_ul]:list-disc [&_li]:mb-2 [&_li:last-child]:mb-0 [&_a]:font-semibold [&_a]:underline [&_a]:text-dark-blue"; + $anchorOffset = max(0, (int) ($trainingResource->anchor_offset ?? 0)); + + $isExternalLink = static function (?string $url): bool { + $url = trim((string) $url); + + return \Illuminate\Support\Str::startsWith($url, ['http://', 'https://', '//']); + }; @endphp @section('title', $pageTitle) @@ -33,6 +41,10 @@ @include('codingathome.banner', [ 'author' => $trainingResource->hero_author, 'title' => $displayTitle, + 'primaryButtonText' => $trainingResource->hero_button_text, + 'primaryButtonUrl' => $trainingResource->hero_button_url, + 'secondaryButtonText' => $trainingResource->hero_secondary_button_text, + 'secondaryButtonUrl' => $trainingResource->hero_secondary_button_url, ])
@@ -87,24 +99,26 @@ class="mb-12 w-full h-full max-h-[630px] object-contain"
@endif - @if(!empty($trainingResource->button_text) && !empty($trainingResource->button_url)) -
- - {{ $trainingResource->button_text }} - + @if(!empty($trainingResource->contacts_section)) +
+ {!! $trainingResource->contacts_section !!} +
+ @endif + @if(!empty($trainingResource->register_box_section)) +
+
+ {!! $trainingResource->register_box_section !!} +
@if(!empty($trainingResource->third_button_text) && !empty($trainingResource->third_button_url)) -
+
third_button_url)) + target="_blank" + rel="noopener noreferrer" + @endif > {{ $trainingResource->third_button_text }} @@ -113,30 +127,46 @@ class="max-xl:!hidden inline-block bg-[#F95C22] rounded-full py-2.5 px-6 font-['
@endif - @if(!empty($trainingResource->contacts_section)) -
- {!! $trainingResource->contacts_section !!} + @if(!empty($trainingResource->about_box_section)) +
+ Info +
+ {!! $trainingResource->about_box_section !!} +
@endif - @if(!empty($trainingResource->secondary_button_text) && !empty($trainingResource->secondary_button_url)) - - - {{ $trainingResource->secondary_button_text }} - - -
- @endif - - @if(!empty($trainingResource->register_box_section)) -
-
- {!! $trainingResource->register_box_section !!} + @if( + (!empty($trainingResource->button_text) && !empty($trainingResource->button_url)) + || (!empty($trainingResource->secondary_button_text) && !empty($trainingResource->secondary_button_url)) + ) +
+

Toolkit access

+
+ @if(!empty($trainingResource->button_text) && !empty($trainingResource->button_url)) + button_url)) + target="_blank" + rel="noopener noreferrer" + @endif + > + {{ $trainingResource->button_text }} + + @endif + @if(!empty($trainingResource->secondary_button_text) && !empty($trainingResource->secondary_button_url)) + secondary_button_url)) + target="_blank" + rel="noopener noreferrer" + @endif + > + {{ $trainingResource->secondary_button_text }} + + @endif
@endif @@ -146,3 +176,48 @@ class="max-xl:!hidden bg-[#F95C22] rounded-full py-2.5 px-6 font-['Blinker'] hov @include('include.licence')
@endsection + +@if($anchorOffset > 0) + @push('scripts') + + @endpush +@endif