Release v1.1.0 #79
5 changed files with 199 additions and 61 deletions
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -31,6 +35,9 @@
|
|||
'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,
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
],
|
||||
|
||||
/*
|
||||
|
|
|
|||
|
|
@ -241,7 +241,7 @@ class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none foc
|
|||
required
|
||||
>
|
||||
<option value="">Select language</option>
|
||||
@foreach ($languages as $language)
|
||||
@foreach ($wizardLanguages as $language)
|
||||
<option value="{{ $language->id }}">{{ $language->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
|
|
@ -316,6 +316,25 @@ class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none foc
|
|||
@error('feedName') <p class="text-red-600 text-sm mt-1">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="feedLanguageId" class="block text-sm font-medium text-gray-700 mb-2 flex items-center">
|
||||
Language
|
||||
<span class="ml-2 text-gray-400 cursor-help" title="Language matches your channel. Additional languages can be configured from the dashboard after setup.">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
</span>
|
||||
</label>
|
||||
<select
|
||||
id="feedLanguageId"
|
||||
disabled
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md bg-gray-100 text-gray-600 cursor-not-allowed"
|
||||
>
|
||||
<option>{{ $channelLanguage?->name ?? 'Unknown' }}</option>
|
||||
</select>
|
||||
<p class="text-sm text-gray-500 mt-1">Inherited from your channel</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="feedProvider" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
News Provider
|
||||
|
|
@ -331,27 +350,12 @@ class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none foc
|
|||
<option value="{{ $provider['code'] }}">{{ $provider['name'] }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@if ($feedProviders->isEmpty())
|
||||
<p class="text-sm text-amber-600 mt-1">No providers available for this language.</p>
|
||||
@endif
|
||||
@error('feedProvider') <p class="text-red-600 text-sm mt-1">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="feedLanguageId" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Language
|
||||
</label>
|
||||
<select
|
||||
id="feedLanguageId"
|
||||
wire:model="feedLanguageId"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
required
|
||||
>
|
||||
<option value="">Select language</option>
|
||||
@foreach ($languages as $language)
|
||||
<option value="{{ $language->id }}">{{ $language->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('feedLanguageId') <p class="text-red-600 text-sm mt-1">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="feedDescription" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Description (Optional)
|
||||
|
|
@ -372,7 +376,7 @@ class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none foc
|
|||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
@disabled($isLoading)
|
||||
@disabled($isLoading || $feedProviders->isEmpty())
|
||||
class="bg-blue-600 text-white py-2 px-6 rounded-md hover:bg-blue-700 transition duration-200 disabled:opacity-50"
|
||||
>
|
||||
{{ $isLoading ? 'Creating...' : 'Continue' }}
|
||||
|
|
@ -418,7 +422,7 @@ class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none foc
|
|||
>
|
||||
<option value="">Select a feed</option>
|
||||
@foreach ($feeds as $feed)
|
||||
<option value="{{ $feed->id }}">{{ $feed->name }}</option>
|
||||
<option value="{{ $feed->id }}">{{ $feed->name }} ({{ $feed->language?->short_code ?? '?' }})</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('routeFeedId') <p class="text-red-600 text-sm mt-1">{{ $message }}</p> @enderror
|
||||
|
|
@ -436,7 +440,7 @@ class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none foc
|
|||
>
|
||||
<option value="">Select a channel</option>
|
||||
@foreach ($channels as $channel)
|
||||
<option value="{{ $channel->id }}">{{ $channel->display_name ?? $channel->name }}</option>
|
||||
<option value="{{ $channel->id }}">{{ $channel->display_name ?? $channel->name }} ({{ $channel->language?->short_code ?? '?' }})</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@if ($channels->isEmpty())
|
||||
|
|
|
|||
11
shell.nix
11
shell.nix
|
|
@ -65,6 +65,16 @@ pkgs.mkShell {
|
|||
podman-compose -f $COMPOSE_FILE restart "$@"
|
||||
}
|
||||
|
||||
dev-rebuild() {
|
||||
echo "Rebuilding services (down -v + up)..."
|
||||
podman-compose -f $COMPOSE_FILE down -v
|
||||
PODMAN_USERNS=keep-id podman-compose -f $COMPOSE_FILE up -d "$@"
|
||||
echo ""
|
||||
podman-compose -f $COMPOSE_FILE ps
|
||||
echo ""
|
||||
echo "App available at: http://localhost:8000"
|
||||
}
|
||||
|
||||
dev-logs() {
|
||||
podman-compose -f $COMPOSE_FILE logs -f app "$@"
|
||||
}
|
||||
|
|
@ -123,6 +133,7 @@ pkgs.mkShell {
|
|||
echo "Commands:"
|
||||
echo " dev-up [services] Start all or specific services"
|
||||
echo " dev-down [-v] Stop services (-v removes volumes)"
|
||||
echo " dev-rebuild Fresh start (down -v + up)"
|
||||
echo " dev-restart Restart services"
|
||||
echo " dev-logs Tail app logs"
|
||||
echo " dev-logs-db Tail database logs"
|
||||
|
|
|
|||
Loading…
Reference in a new issue