From 03353aecf9a84fbaf4e3549da92dc4cfbb786d1f Mon Sep 17 00:00:00 2001 From: bernardhanna Date: Mon, 9 Feb 2026 17:56:07 +0000 Subject: [PATCH] make adding slides dynamic --- app/HomeSlide.php | 17 ++ .../HomeSlideLocaleOverridesController.php | 48 ++++++ .../ExportHomeSlideLocaleOverrides.php | 29 ++++ .../ImportHomeSlideLocaleOverrides.php | 91 ++++++++++ app/Nova/HomeSlide.php | 161 +++++++++++++++--- routes/web.php | 4 + 6 files changed, 330 insertions(+), 20 deletions(-) create mode 100644 app/Http/Controllers/HomeSlideLocaleOverridesController.php create mode 100644 app/Nova/Actions/ExportHomeSlideLocaleOverrides.php create mode 100644 app/Nova/Actions/ImportHomeSlideLocaleOverrides.php diff --git a/app/HomeSlide.php b/app/HomeSlide.php index f54aac2d1..768d14339 100644 --- a/app/HomeSlide.php +++ b/app/HomeSlide.php @@ -33,6 +33,23 @@ class HomeSlide extends Model 'open_second_new_tab' => 'boolean', ]; + /** + * Normalize locale_overrides so empty/invalid JSON from Nova doesn't cause 500. + */ + public function setLocaleOverridesAttribute($value): void + { + if ($value === null || $value === '') { + $this->attributes['locale_overrides'] = null; + return; + } + if (is_string($value)) { + $decoded = json_decode($value, true); + $this->attributes['locale_overrides'] = json_encode(is_array($decoded) ? $decoded : []); + return; + } + $this->attributes['locale_overrides'] = is_array($value) ? json_encode($value) : null; + } + /** * Display value for current locale: use optional override or translate via lang key (default English). */ diff --git a/app/Http/Controllers/HomeSlideLocaleOverridesController.php b/app/Http/Controllers/HomeSlideLocaleOverridesController.php new file mode 100644 index 000000000..e872c8f5a --- /dev/null +++ b/app/Http/Controllers/HomeSlideLocaleOverridesController.php @@ -0,0 +1,48 @@ +query('id'); + $slide = $id ? HomeSlide::find($id) : null; + if (! $slide) { + abort(404, 'Slide not found.'); + } + + $filename = 'home-slide-' . $slide->id . '-locale-overrides.csv'; + $headers = [ + 'Content-Type' => 'text/csv; charset=UTF-8', + 'Content-Disposition' => 'attachment; filename="' . $filename . '"', + ]; + + return response()->streamDownload(function () use ($slide) { + $stream = fopen('php://output', 'w'); + fprintf($stream, chr(0xEF) . chr(0xBB) . chr(0xBF)); + fputcsv($stream, ['locale', 'title', 'description', 'button_text', 'button2_text']); + $overrides = $slide->locale_overrides ?? []; + $locales = HomeSlideResource::localesSorted(); + foreach ($locales as $locale) { + $row = $overrides[$locale] ?? []; + fputcsv($stream, [ + $locale, + $row['title'] ?? '', + $row['description'] ?? '', + $row['button_text'] ?? '', + $row['button2_text'] ?? '', + ]); + } + fclose($stream); + }, $filename, $headers); + } +} diff --git a/app/Nova/Actions/ExportHomeSlideLocaleOverrides.php b/app/Nova/Actions/ExportHomeSlideLocaleOverrides.php new file mode 100644 index 000000000..d7ce450f9 --- /dev/null +++ b/app/Nova/Actions/ExportHomeSlideLocaleOverrides.php @@ -0,0 +1,29 @@ +first(); + if (! $slide instanceof HomeSlide) { + return Action::danger('Please select a homepage slide to export.'); + } + + return Action::redirect(route('admin.home-slides.export-locale-overrides', ['id' => $slide->id])); + } + + public function fields(NovaRequest $request): array + { + return []; + } +} diff --git a/app/Nova/Actions/ImportHomeSlideLocaleOverrides.php b/app/Nova/Actions/ImportHomeSlideLocaleOverrides.php new file mode 100644 index 000000000..d61b2b5b2 --- /dev/null +++ b/app/Nova/Actions/ImportHomeSlideLocaleOverrides.php @@ -0,0 +1,91 @@ +first(); + if (! $slide instanceof HomeSlide) { + return Action::danger('Please select a homepage slide to import into.'); + } + + $file = $fields->csv_file ?? null; + if (! $file) { + return Action::danger('Please select a CSV file to import.'); + } + + $path = is_object($file) && method_exists($file, 'getRealPath') + ? $file->getRealPath() + : (is_string($file) ? $file : null); + if (! $path || ! is_readable($path)) { + return Action::danger('Could not read the uploaded file.'); + } + $rows = array_map('str_getcsv', file($path)); + if (empty($rows)) { + return Action::danger('The CSV file is empty.'); + } + + $header = array_map('trim', array_map('strtolower', $rows[0])); + $localeIndex = array_search('locale', $header, true); + if ($localeIndex === false) { + return Action::danger('CSV must have a "locale" column.'); + } + + $titleIndex = array_search('title', $header, true); + $descIndex = array_search('description', $header, true); + $btnIndex = array_search('button_text', $header, true); + $btn2Index = array_search('button2_text', $header, true); + + $overrides = $slide->locale_overrides ?? []; + for ($i = 1; $i < count($rows); $i++) { + $row = $rows[$i]; + if (count($row) <= $localeIndex) { + continue; + } + $locale = trim($row[$localeIndex] ?? ''); + if ($locale === '') { + continue; + } + if (! isset($overrides[$locale])) { + $overrides[$locale] = []; + } + if ($titleIndex !== false && isset($row[$titleIndex])) { + $overrides[$locale]['title'] = trim($row[$titleIndex]); + } + if ($descIndex !== false && isset($row[$descIndex])) { + $overrides[$locale]['description'] = trim($row[$descIndex]); + } + if ($btnIndex !== false && isset($row[$btnIndex])) { + $overrides[$locale]['button_text'] = trim($row[$btnIndex]); + } + if ($btn2Index !== false && isset($row[$btn2Index])) { + $overrides[$locale]['button2_text'] = trim($row[$btn2Index]); + } + } + + $slide->locale_overrides = $overrides; + $slide->save(); + + return Action::message('Locale overrides imported. Rows: ' . (count($rows) - 1)); + } + + public function fields(NovaRequest $request): array + { + return [ + File::make('CSV file', 'csv_file') + ->rules('required', 'file', 'mimes:csv,txt') + ->help('CSV with columns: locale, title, description, button_text, button2_text. Use the Export action to get a template.'), + ]; + } +} diff --git a/app/Nova/HomeSlide.php b/app/Nova/HomeSlide.php index f9cd29307..d4f865d61 100644 --- a/app/Nova/HomeSlide.php +++ b/app/Nova/HomeSlide.php @@ -5,7 +5,6 @@ use Illuminate\Http\Request; use Illuminate\Support\Facades\File; use Laravel\Nova\Fields\Boolean; -use Laravel\Nova\Fields\Code; use Laravel\Nova\Fields\DateTime; use Laravel\Nova\Fields\ID; use Laravel\Nova\Fields\Number; @@ -13,6 +12,8 @@ use Laravel\Nova\Fields\Textarea; use Laravel\Nova\Http\Requests\NovaRequest; use Laravel\Nova\Panel; +use App\Nova\Actions\ExportHomeSlideLocaleOverrides; +use App\Nova\Actions\ImportHomeSlideLocaleOverrides; class HomeSlide extends Resource { @@ -60,29 +61,148 @@ public static function homeLangKeys(): array } /** - * Locale codes from config (LOCALES env). + * Locale codes from config (LOCALES env), sorted A–Z. Used for override rows and CSV. * - * @return array + * @return list */ - public static function localeOptions(): array + public static function localesSorted(): array { $locales = config('app.locales', ['en']); if (is_string($locales)) { - $locales = explode(',', $locales); + $locales = array_map('trim', explode(',', $locales)); + } + $locales = array_values(array_filter($locales)); + if (empty($locales)) { + $locales = ['en']; } + sort($locales); + return $locales; + } + + /** + * Locale codes as options (key => label). + * + * @return array + */ + public static function localeOptions(): array + { $out = []; - foreach ($locales as $locale) { - $locale = trim($locale); - if ($locale !== '') { - $out[$locale] = $locale; - } + foreach (self::localesSorted() as $locale) { + $out[$locale] = $locale; } return $out; } + private const OVERRIDE_KEYS = ['title', 'description', 'button_text', 'button2_text']; + + private static function parseOverrideAttribute(string $attribute): ?array + { + if (! str_starts_with($attribute, 'override_') || strlen($attribute) < 12) { + return null; + } + $rest = substr($attribute, 9); + $parts = explode('_', $rest); + if (count($parts) < 2) { + return null; + } + $locale = $parts[0]; + $key = implode('_', array_slice($parts, 1)); + if (! in_array($key, self::OVERRIDE_KEYS, true)) { + return null; + } + return ['locale' => $locale, 'key' => $key]; + } + public function fields(Request $request): array { - $locales = self::localeOptions(); + $locales = self::localesSorted(); + $overrideFields = []; + + foreach ($locales as $locale) { + $overrideFields[] = Text::make('[' . $locale . '] Title', 'override_' . $locale . '_title') + ->nullable() + ->onlyOnForms() + ->hideFromIndex() + ->resolveUsing(function () use ($locale) { + $overrides = $this->resource->locale_overrides ?? []; + return $overrides[$locale]['title'] ?? ''; + }) + ->fillUsing(function ($request, $model, $attribute, $requestAttribute) { + $parsed = self::parseOverrideAttribute($attribute); + if ($parsed === null) { + return; + } + $overrides = $model->locale_overrides ?? []; + if (! isset($overrides[$parsed['locale']])) { + $overrides[$parsed['locale']] = []; + } + $overrides[$parsed['locale']][$parsed['key']] = $request->get($requestAttribute) ?: null; + $model->locale_overrides = $overrides; + }); + + $overrideFields[] = Textarea::make('[' . $locale . '] Description', 'override_' . $locale . '_description') + ->nullable() + ->onlyOnForms() + ->hideFromIndex() + ->resolveUsing(function () use ($locale) { + $overrides = $this->resource->locale_overrides ?? []; + return $overrides[$locale]['description'] ?? ''; + }) + ->fillUsing(function ($request, $model, $attribute, $requestAttribute) { + $parsed = self::parseOverrideAttribute($attribute); + if ($parsed === null) { + return; + } + $overrides = $model->locale_overrides ?? []; + if (! isset($overrides[$parsed['locale']])) { + $overrides[$parsed['locale']] = []; + } + $overrides[$parsed['locale']][$parsed['key']] = $request->get($requestAttribute) ?: null; + $model->locale_overrides = $overrides; + }); + + $overrideFields[] = Text::make('[' . $locale . '] Button', 'override_' . $locale . '_button_text') + ->nullable() + ->onlyOnForms() + ->hideFromIndex() + ->resolveUsing(function () use ($locale) { + $overrides = $this->resource->locale_overrides ?? []; + return $overrides[$locale]['button_text'] ?? ''; + }) + ->fillUsing(function ($request, $model, $attribute, $requestAttribute) { + $parsed = self::parseOverrideAttribute($attribute); + if ($parsed === null) { + return; + } + $overrides = $model->locale_overrides ?? []; + if (! isset($overrides[$parsed['locale']])) { + $overrides[$parsed['locale']] = []; + } + $overrides[$parsed['locale']][$parsed['key']] = $request->get($requestAttribute) ?: null; + $model->locale_overrides = $overrides; + }); + + $overrideFields[] = Text::make('[' . $locale . '] Button 2', 'override_' . $locale . '_button2_text') + ->nullable() + ->onlyOnForms() + ->hideFromIndex() + ->resolveUsing(function () use ($locale) { + $overrides = $this->resource->locale_overrides ?? []; + return $overrides[$locale]['button2_text'] ?? ''; + }) + ->fillUsing(function ($request, $model, $attribute, $requestAttribute) { + $parsed = self::parseOverrideAttribute($attribute); + if ($parsed === null) { + return; + } + $overrides = $model->locale_overrides ?? []; + if (! isset($overrides[$parsed['locale']])) { + $overrides[$parsed['locale']] = []; + } + $overrides[$parsed['locale']][$parsed['key']] = $request->get($requestAttribute) ?: null; + $model->locale_overrides = $overrides; + }); + } return [ ID::make()->sortable(), @@ -121,15 +241,8 @@ public function fields(Request $request): array ->nullable() ->help('Target date/time for countdown (e.g. Code Week start). Only used if Show countdown is on.'), - new Panel('Optional: per-locale overrides', [ - Code::make('Locale overrides', 'locale_overrides') - ->json() - ->nullable() - ->help( - 'Optional. Override text for specific locales. Leave empty to use lang keys above (from resources/lang). ' . - 'Locales: ' . implode(', ', array_keys($locales)) . '. ' . - 'Example: {"es": {"title": "Título", "description": "Descripción", "button_text": "Ver", "button2_text": ""}, "fr": {"title": "Titre"}}' - ), + new Panel('Per-locale overrides (optional)', [ + ...$overrideFields, ]), ]; } @@ -138,4 +251,12 @@ public static function indexQuery(NovaRequest $request, $query) { return $query->orderBy('position')->orderBy('id'); } + + public function actions(NovaRequest $request): array + { + return [ + new ExportHomeSlideLocaleOverrides, + new ImportHomeSlideLocaleOverrides, + ]; + } } diff --git a/routes/web.php b/routes/web.php index ece6f4ee1..d3b109af7 100644 --- a/routes/web.php +++ b/routes/web.php @@ -66,6 +66,7 @@ use App\Http\Controllers\UserController; use App\Http\Controllers\VolunteerController; use App\Http\Controllers\ConsentController; +use App\Http\Controllers\HomeSlideLocaleOverridesController; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Route; @@ -86,6 +87,9 @@ Route::post('/admin/resources-import/import', [ResourcesImportController::class, 'import'])->name('admin.resources-import.import'); Route::get('/admin/resources-import/import', fn () => redirect()->route('admin.resources-import.index'))->name('admin.resources-import.import.get'); Route::get('/admin/resources-import/report', [ResourcesImportController::class, 'report'])->name('admin.resources-import.report'); + + Route::get('/admin/home-slides/export-locale-overrides', [HomeSlideLocaleOverridesController::class, 'export']) + ->name('admin.home-slides.export-locale-overrides'); }); //redirects start