diff --git a/app/Console/Commands/DeleteUserAccount.php b/app/Console/Commands/DeleteUserAccount.php
new file mode 100644
index 000000000..04c51a3ed
--- /dev/null
+++ b/app/Console/Commands/DeleteUserAccount.php
@@ -0,0 +1,70 @@
+argument('email')));
+
+ /** @var User|null $user */
+ $user = User::withTrashed()
+ ->whereRaw('LOWER(email) = ?', [$email])
+ ->first();
+
+ if (!$user) {
+ $this->error("No user found for email: {$email}");
+ return self::FAILURE;
+ }
+
+ $this->info("Found user #{$user->id}: {$user->firstname} {$user->lastname} <{$user->email}>");
+
+ if ($this->option('dry-run')) {
+ $this->line('Dry run enabled. No changes were made.');
+ return self::SUCCESS;
+ }
+
+ if (!$this->option('force')) {
+ $confirmed = $this->confirm(
+ "This will permanently delete user #{$user->id} ({$user->email}). Continue?",
+ false
+ );
+
+ if (!$confirmed) {
+ $this->line('Cancelled.');
+ return self::SUCCESS;
+ }
+ }
+
+ try {
+ if ($this->option('queue')) {
+ ProcessUserDeletion::dispatch($user->id);
+ $this->info("Deletion job dispatched for user #{$user->id}.");
+ return self::SUCCESS;
+ }
+
+ // Execute immediately for urgent/account-support requests.
+ (new ProcessUserDeletion($user->id))->handle();
+ $this->info("User #{$user->id} has been deleted.");
+
+ return self::SUCCESS;
+ } catch (Throwable $e) {
+ $this->error("Failed to delete user #{$user->id}: {$e->getMessage()}");
+ return self::FAILURE;
+ }
+ }
+}
diff --git a/app/HackathonsPage.php b/app/HackathonsPage.php
index 60f511146..34565fc05 100644
--- a/app/HackathonsPage.php
+++ b/app/HackathonsPage.php
@@ -12,6 +12,7 @@ class HackathonsPage extends Model
'dynamic_content',
'hero_title',
'hero_subtitle',
+ 'hero_background_image_url',
'intro_title',
'intro_paragraph_1',
'intro_paragraph_2',
@@ -21,6 +22,7 @@ class HackathonsPage extends Model
'details_paragraph_3',
'details_paragraph_4',
'video_url',
+ 'video_poster_image_url',
'extra_button_text',
'extra_button_link',
'recap_button_text',
diff --git a/app/Nova/HackathonsPage.php b/app/Nova/HackathonsPage.php
index 1351c1bf0..d8e3578e2 100644
--- a/app/Nova/HackathonsPage.php
+++ b/app/Nova/HackathonsPage.php
@@ -49,6 +49,9 @@ public function fields(Request $request): array
Panel::make('Hero', [
Text::make('Hero title', 'hero_title')->nullable(),
Trix::make('Hero subtitle', 'hero_subtitle')->nullable(),
+ Text::make('Hero background image URL', 'hero_background_image_url')
+ ->nullable()
+ ->help('Optional. Use a relative path like /images/hackathons/hackathons_bg.png or a full URL.'),
])->collapsable()->collapsedByDefault(),
Panel::make('Intro section', [
@@ -64,6 +67,9 @@ public function fields(Request $request): array
Trix::make('Details paragraph 3', 'details_paragraph_3')->nullable(),
Trix::make('Details paragraph 4', 'details_paragraph_4')->nullable(),
Text::make('Video URL (embed)', 'video_url')->nullable(),
+ Text::make('Video poster image URL', 'video_poster_image_url')
+ ->nullable()
+ ->help('Optional. Image shown behind the play button. Use a relative path like /images/Visual%20for%20EU%20Finals%20webpage.png or a full URL.'),
Text::make('Extra button text (optional)', 'extra_button_text')->nullable(),
Text::make('Extra button link (optional)', 'extra_button_link')->nullable(),
Text::make('Recap button text', 'recap_button_text')->nullable(),
diff --git a/database/migrations/2026_04_07_120000_add_images_to_hackathons_page_table.php b/database/migrations/2026_04_07_120000_add_images_to_hackathons_page_table.php
new file mode 100644
index 000000000..0c600fa1b
--- /dev/null
+++ b/database/migrations/2026_04_07_120000_add_images_to_hackathons_page_table.php
@@ -0,0 +1,38 @@
+string('hero_background_image_url')->nullable()->after('hero_subtitle');
+ });
+ }
+
+ if (Schema::hasTable('hackathons_page') && !Schema::hasColumn('hackathons_page', 'video_poster_image_url')) {
+ Schema::table('hackathons_page', function (Blueprint $table) {
+ $table->string('video_poster_image_url')->nullable()->after('video_url');
+ });
+ }
+ }
+
+ public function down(): void
+ {
+ if (Schema::hasTable('hackathons_page') && Schema::hasColumn('hackathons_page', 'video_poster_image_url')) {
+ Schema::table('hackathons_page', function (Blueprint $table) {
+ $table->dropColumn('video_poster_image_url');
+ });
+ }
+
+ if (Schema::hasTable('hackathons_page') && Schema::hasColumn('hackathons_page', 'hero_background_image_url')) {
+ Schema::table('hackathons_page', function (Blueprint $table) {
+ $table->dropColumn('hero_background_image_url');
+ });
+ }
+ }
+};
+
diff --git a/public/images/Visual for EU Finals webpage.png b/public/images/Visual for EU Finals webpage.png
new file mode 100644
index 000000000..431d2b661
Binary files /dev/null and b/public/images/Visual for EU Finals webpage.png differ
diff --git a/resources/views/hackathons/index.blade.php b/resources/views/hackathons/index.blade.php
index d1e55fbb7..658389653 100644
--- a/resources/views/hackathons/index.blade.php
+++ b/resources/views/hackathons/index.blade.php
@@ -9,6 +9,13 @@
$hasHackathonsPageTable = \Illuminate\Support\Facades\Schema::hasTable('hackathons_page');
$page = $hasHackathonsPageTable ? \App\HackathonsPage::config() : null;
$dynamic = $page && $page->dynamic_content;
+
+ $defaultHeroBackground = '/images/hackathons/hackathons_bg.png';
+ $heroBackground = ($dynamic && $page && $page->hero_background_image_url) ? $page->hero_background_image_url : $defaultHeroBackground;
+
+ // Note: file on disk has spaces; URL must be encoded.
+ $defaultVideoPoster = '/images/Visual%20for%20EU%20Finals%20webpage.png';
+ $videoPoster = ($dynamic && $page && $page->video_poster_image_url) ? $page->video_poster_image_url : $defaultVideoPoster;
@endphp
@section('layout.breadcrumb')
@@ -46,13 +53,13 @@
@@ -105,7 +112,7 @@ class="animation-element move-background duration-[1.5s] absolute z-0 lg:-bottom
+