From 35e4260c87cb53234df80b2b2169d41e57afe3ae Mon Sep 17 00:00:00 2001 From: myrmidex Date: Sat, 7 Mar 2026 10:43:48 +0100 Subject: [PATCH] 76 - Lock feed language to channel language in onboarding wizard --- app/Livewire/Onboarding.php | 131 ++++++++++++++++-- config/feed.php | 9 +- config/languages.php | 61 ++++---- resources/views/livewire/onboarding.blade.php | 48 ++++--- shell.nix | 11 ++ 5 files changed, 199 insertions(+), 61 deletions(-) diff --git a/app/Livewire/Onboarding.php b/app/Livewire/Onboarding.php index 5e05ff9..dc5eda3 100644 --- a/app/Livewire/Onboarding.php +++ b/app/Livewire/Onboarding.php @@ -47,6 +47,7 @@ class Onboarding extends Component // State public array $formErrors = []; public bool $isLoading = false; + private ?int $previousChannelLanguageId = null; protected LemmyAuthService $lemmyAuthService; @@ -104,6 +105,11 @@ public function nextStep(): void { $this->step++; $this->formErrors = []; + + // When entering feed step, inherit language from channel + if ($this->step === 4 && $this->channelLanguageId) { + $this->feedLanguageId = $this->channelLanguageId; + } } public function previousStep(): void @@ -210,24 +216,37 @@ public function createFeed(): void $this->formErrors = []; $this->isLoading = true; + // Get available provider codes for validation + $availableProviders = collect($this->getProvidersForLanguage())->pluck('code')->implode(','); + $this->validate([ 'feedName' => 'required|string|max:255', - 'feedProvider' => 'required|in:belga,vrt', + 'feedProvider' => "required|in:{$availableProviders}", 'feedLanguageId' => 'required|exists:languages,id', 'feedDescription' => 'nullable|string|max:1000', ]); try { - // Map provider to URL - $url = $this->feedProvider === 'vrt' - ? 'https://www.vrt.be/vrtnws/en/' - : 'https://www.belganewsagency.eu/'; + // Get language short code + $language = Language::find($this->feedLanguageId); + $langCode = $language->short_code; + + // Look up URL from config + $url = config("feed.providers.{$this->feedProvider}.languages.{$langCode}.url"); + + if (!$url) { + $this->formErrors['general'] = 'Invalid provider and language combination.'; + $this->isLoading = false; + return; + } + + $providerConfig = config("feed.providers.{$this->feedProvider}"); Feed::firstOrCreate( ['url' => $url], [ 'name' => $this->feedName, - 'type' => 'website', + 'type' => $providerConfig['type'] ?? 'website', 'provider' => $this->feedProvider, 'language_id' => $this->feedLanguageId, 'description' => $this->feedDescription ?: null, @@ -255,6 +274,16 @@ public function createChannel(): void 'channelDescription' => 'nullable|string|max:1000', ]); + // If language changed, reset feed form + if ($this->previousChannelLanguageId !== null && $this->previousChannelLanguageId !== $this->channelLanguageId) { + $this->feedName = ''; + $this->feedProvider = ''; + $this->feedDescription = ''; + $this->routeFeedId = null; + $this->routeChannelId = null; + } + $this->previousChannelLanguageId = $this->channelLanguageId; + try { $platformInstance = PlatformInstance::findOrFail($this->platformInstanceId); @@ -340,23 +369,97 @@ public function completeOnboarding(): void $this->redirect(route('dashboard')); } + /** + * Get language codes that have at least one active provider. + */ + public function getAvailableLanguageCodes(): array + { + $providers = config('feed.providers', []); + $languageCodes = []; + + foreach ($providers as $provider) { + if (!($provider['is_active'] ?? false)) { + continue; + } + foreach (array_keys($provider['languages'] ?? []) as $code) { + $languageCodes[$code] = true; + } + } + + return array_keys($languageCodes); + } + + /** + * Get providers available for the current channel language. + */ + public function getProvidersForLanguage(): array + { + if (!$this->channelLanguageId) { + return []; + } + + $language = Language::find($this->channelLanguageId); + if (!$language) { + return []; + } + + $langCode = $language->short_code; + $providers = config('feed.providers', []); + $available = []; + + foreach ($providers as $key => $provider) { + if (!($provider['is_active'] ?? false)) { + continue; + } + if (isset($provider['languages'][$langCode])) { + $available[] = [ + 'code' => $provider['code'], + 'name' => $provider['name'], + 'description' => $provider['description'] ?? '', + ]; + } + } + + return $available; + } + + /** + * Get the current channel language model. + */ + public function getChannelLanguage(): ?Language + { + if (!$this->channelLanguageId) { + return null; + } + return Language::find($this->channelLanguageId); + } + public function render() { - $languages = Language::where('is_active', true)->orderBy('name')->get(); - $platformInstances = PlatformInstance::where('is_active', true)->orderBy('name')->get(); - $feeds = Feed::where('is_active', true)->orderBy('name')->get(); - $channels = PlatformChannel::where('is_active', true)->orderBy('name')->get(); + // For channel step: only show languages that have providers + $availableCodes = $this->getAvailableLanguageCodes(); + $wizardLanguages = Language::where('is_active', true) + ->whereIn('short_code', $availableCodes) + ->orderBy('name') + ->get(); - $feedProviders = collect(config('feed.providers', [])) - ->filter(fn($provider) => $provider['is_active'] ?? false) - ->values(); + $platformInstances = PlatformInstance::where('is_active', true)->orderBy('name')->get(); + $feeds = Feed::with('language')->where('is_active', true)->orderBy('name')->get(); + $channels = PlatformChannel::with('language')->where('is_active', true)->orderBy('name')->get(); + + // For feed step: only show providers for the channel's language + $feedProviders = collect($this->getProvidersForLanguage()); + + // Get channel language for display + $channelLanguage = $this->getChannelLanguage(); return view('livewire.onboarding', [ - 'languages' => $languages, + 'wizardLanguages' => $wizardLanguages, 'platformInstances' => $platformInstances, 'feeds' => $feeds, 'channels' => $channels, 'feedProviders' => $feedProviders, + 'channelLanguage' => $channelLanguage, ])->layout('layouts.onboarding'); } } diff --git a/config/feed.php b/config/feed.php index 2c4288d..b6bdfd7 100644 --- a/config/feed.php +++ b/config/feed.php @@ -19,6 +19,10 @@ 'description' => 'Belgian public broadcaster news', 'type' => 'website', 'is_active' => true, + 'languages' => [ + 'en' => ['url' => 'https://www.vrt.be/vrtnws/en/'], + 'nl' => ['url' => 'https://www.vrt.be/vrtnws/nl/'], + ], 'parsers' => [ 'homepage' => \App\Services\Parsers\VrtHomepageParserAdapter::class, 'article' => \App\Services\Parsers\VrtArticleParser::class, @@ -27,10 +31,13 @@ ], 'belga' => [ 'code' => 'belga', - 'name' => 'Belga News Agency', + 'name' => 'Belga News Agency', 'description' => 'Belgian national news agency', 'type' => 'rss', 'is_active' => true, + 'languages' => [ + 'en' => ['url' => 'https://www.belganewsagency.eu/'], + ], 'parsers' => [ 'homepage' => \App\Services\Parsers\BelgaHomepageParserAdapter::class, 'article' => \App\Services\Parsers\BelgaArticleParser::class, diff --git a/config/languages.php b/config/languages.php index d0f8c7f..6b63ce0 100644 --- a/config/languages.php +++ b/config/languages.php @@ -12,30 +12,43 @@ */ 'supported' => [ - 'en' => [ - 'short_code' => 'en', - 'name' => 'English', - 'native_name' => 'English', - 'is_active' => true, - ], - 'nl' => [ - 'short_code' => 'nl', - 'name' => 'Dutch', - 'native_name' => 'Nederlands', - 'is_active' => true, - ], - 'fr' => [ - 'short_code' => 'fr', - 'name' => 'French', - 'native_name' => 'Français', - 'is_active' => true, - ], - 'de' => [ - 'short_code' => 'de', - 'name' => 'German', - 'native_name' => 'Deutsch', - 'is_active' => true, - ], + 'en' => ['short_code' => 'en', 'name' => 'English', 'native_name' => 'English', 'is_active' => true], + 'nl' => ['short_code' => 'nl', 'name' => 'Dutch', 'native_name' => 'Nederlands', 'is_active' => true], + 'fr' => ['short_code' => 'fr', 'name' => 'French', 'native_name' => 'Français', 'is_active' => true], + 'de' => ['short_code' => 'de', 'name' => 'German', 'native_name' => 'Deutsch', 'is_active' => true], + 'es' => ['short_code' => 'es', 'name' => 'Spanish', 'native_name' => 'Español', 'is_active' => true], + 'it' => ['short_code' => 'it', 'name' => 'Italian', 'native_name' => 'Italiano', 'is_active' => true], + 'pt' => ['short_code' => 'pt', 'name' => 'Portuguese', 'native_name' => 'Português', 'is_active' => true], + 'pl' => ['short_code' => 'pl', 'name' => 'Polish', 'native_name' => 'Polski', 'is_active' => true], + 'ru' => ['short_code' => 'ru', 'name' => 'Russian', 'native_name' => 'Русский', 'is_active' => true], + 'uk' => ['short_code' => 'uk', 'name' => 'Ukrainian', 'native_name' => 'Українська', 'is_active' => true], + 'cs' => ['short_code' => 'cs', 'name' => 'Czech', 'native_name' => 'Čeština', 'is_active' => true], + 'sk' => ['short_code' => 'sk', 'name' => 'Slovak', 'native_name' => 'Slovenčina', 'is_active' => true], + 'hu' => ['short_code' => 'hu', 'name' => 'Hungarian', 'native_name' => 'Magyar', 'is_active' => true], + 'ro' => ['short_code' => 'ro', 'name' => 'Romanian', 'native_name' => 'Română', 'is_active' => true], + 'bg' => ['short_code' => 'bg', 'name' => 'Bulgarian', 'native_name' => 'Български', 'is_active' => true], + 'hr' => ['short_code' => 'hr', 'name' => 'Croatian', 'native_name' => 'Hrvatski', 'is_active' => true], + 'sl' => ['short_code' => 'sl', 'name' => 'Slovenian', 'native_name' => 'Slovenščina', 'is_active' => true], + 'sr' => ['short_code' => 'sr', 'name' => 'Serbian', 'native_name' => 'Српски', 'is_active' => true], + 'el' => ['short_code' => 'el', 'name' => 'Greek', 'native_name' => 'Ελληνικά', 'is_active' => true], + 'tr' => ['short_code' => 'tr', 'name' => 'Turkish', 'native_name' => 'Türkçe', 'is_active' => true], + 'da' => ['short_code' => 'da', 'name' => 'Danish', 'native_name' => 'Dansk', 'is_active' => true], + 'sv' => ['short_code' => 'sv', 'name' => 'Swedish', 'native_name' => 'Svenska', 'is_active' => true], + 'no' => ['short_code' => 'no', 'name' => 'Norwegian', 'native_name' => 'Norsk', 'is_active' => true], + 'fi' => ['short_code' => 'fi', 'name' => 'Finnish', 'native_name' => 'Suomi', 'is_active' => true], + 'et' => ['short_code' => 'et', 'name' => 'Estonian', 'native_name' => 'Eesti', 'is_active' => true], + 'lv' => ['short_code' => 'lv', 'name' => 'Latvian', 'native_name' => 'Latviešu', 'is_active' => true], + 'lt' => ['short_code' => 'lt', 'name' => 'Lithuanian', 'native_name' => 'Lietuvių', 'is_active' => true], + 'ja' => ['short_code' => 'ja', 'name' => 'Japanese', 'native_name' => '日本語', 'is_active' => true], + 'zh' => ['short_code' => 'zh', 'name' => 'Chinese', 'native_name' => '中文', 'is_active' => true], + 'ko' => ['short_code' => 'ko', 'name' => 'Korean', 'native_name' => '한국어', 'is_active' => true], + 'ar' => ['short_code' => 'ar', 'name' => 'Arabic', 'native_name' => 'العربية', 'is_active' => true], + 'he' => ['short_code' => 'he', 'name' => 'Hebrew', 'native_name' => 'עברית', 'is_active' => true], + 'hi' => ['short_code' => 'hi', 'name' => 'Hindi', 'native_name' => 'हिन्दी', 'is_active' => true], + 'th' => ['short_code' => 'th', 'name' => 'Thai', 'native_name' => 'ไทย', 'is_active' => true], + 'vi' => ['short_code' => 'vi', 'name' => 'Vietnamese', 'native_name' => 'Tiếng Việt', 'is_active' => true], + 'id' => ['short_code' => 'id', 'name' => 'Indonesian', 'native_name' => 'Bahasa Indonesia', 'is_active' => true], + 'ms' => ['short_code' => 'ms', 'name' => 'Malay', 'native_name' => 'Bahasa Melayu', 'is_active' => true], ], /* diff --git a/resources/views/livewire/onboarding.blade.php b/resources/views/livewire/onboarding.blade.php index 24b6419..f08f2d6 100644 --- a/resources/views/livewire/onboarding.blade.php +++ b/resources/views/livewire/onboarding.blade.php @@ -241,7 +241,7 @@ class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none foc required > - @foreach ($languages as $language) + @foreach ($wizardLanguages as $language) @endforeach @@ -316,6 +316,25 @@ class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none foc @error('feedName')

{{ $message }}

@enderror +
+ + +

Inherited from your channel

+
+
-
- - - @error('feedLanguageId')

{{ $message }}

@enderror -
-