diff --git a/backend/app/Http/Controllers/Api/V1/OnboardingController.php b/backend/app/Http/Controllers/Api/V1/OnboardingController.php index 454cd2f..562c15c 100644 --- a/backend/app/Http/Controllers/Api/V1/OnboardingController.php +++ b/backend/app/Http/Controllers/Api/V1/OnboardingController.php @@ -217,15 +217,17 @@ public function createFeed(Request $request): JsonResponse $url = 'https://www.belganewsagency.eu/'; } - $feed = Feed::create([ - 'name' => $validated['name'], - 'url' => $url, - 'type' => $type, - 'provider' => $provider, - 'language_id' => $validated['language_id'], - 'description' => $validated['description'] ?? null, - 'is_active' => true, - ]); + $feed = Feed::firstOrCreate( + ['url' => $url], + [ + 'name' => $validated['name'], + 'type' => $type, + 'provider' => $provider, + 'language_id' => $validated['language_id'], + 'description' => $validated['description'] ?? null, + 'is_active' => true, + ] + ); return $this->sendResponse( new FeedResource($feed->load('language')), diff --git a/backend/app/Http/Resources/FeedResource.php b/backend/app/Http/Resources/FeedResource.php index ac5641b..c220d40 100644 --- a/backend/app/Http/Resources/FeedResource.php +++ b/backend/app/Http/Resources/FeedResource.php @@ -19,6 +19,8 @@ public function toArray(Request $request): array 'name' => $this->name, 'url' => $this->url, 'type' => $this->type, + 'provider' => $this->provider, + 'language_id' => $this->language_id, 'is_active' => $this->is_active, 'description' => $this->description, 'created_at' => $this->created_at->toISOString(), diff --git a/backend/app/Http/Resources/PlatformChannelResource.php b/backend/app/Http/Resources/PlatformChannelResource.php index 1c68e2d..cdebaa4 100644 --- a/backend/app/Http/Resources/PlatformChannelResource.php +++ b/backend/app/Http/Resources/PlatformChannelResource.php @@ -21,6 +21,7 @@ public function toArray(Request $request): array 'name' => $this->name, 'display_name' => $this->display_name, 'description' => $this->description, + 'language_id' => $this->language_id, 'is_active' => $this->is_active, 'created_at' => $this->created_at->toISOString(), 'updated_at' => $this->updated_at->toISOString(), diff --git a/backend/app/Listeners/LogExceptionToDatabase.php b/backend/app/Listeners/LogExceptionToDatabase.php index 23da3a2..3ccfa07 100644 --- a/backend/app/Listeners/LogExceptionToDatabase.php +++ b/backend/app/Listeners/LogExceptionToDatabase.php @@ -10,18 +10,29 @@ class LogExceptionToDatabase public function handle(ExceptionOccurred $event): void { - $log = Log::create([ - 'level' => $event->level, - 'message' => $event->message, - 'context' => [ - 'exception_class' => get_class($event->exception), - 'file' => $event->exception->getFile(), - 'line' => $event->exception->getLine(), - 'trace' => $event->exception->getTraceAsString(), - ...$event->context - ] - ]); + // Truncate the message to prevent database errors + $message = strlen($event->message) > 255 + ? substr($event->message, 0, 252) . '...' + : $event->message; - ExceptionLogged::dispatch($log); + try { + $log = Log::create([ + 'level' => $event->level, + 'message' => $message, + 'context' => [ + 'exception_class' => get_class($event->exception), + 'file' => $event->exception->getFile(), + 'line' => $event->exception->getLine(), + 'trace' => $event->exception->getTraceAsString(), + ...$event->context + ] + ]); + + ExceptionLogged::dispatch($log); + } catch (\Exception $e) { + // Prevent infinite recursion by not logging this exception + // Optionally log to file or other non-database destination + error_log("Failed to log exception to database: " . $e->getMessage()); + } } } diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index f8d6e00..41329e8 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -55,6 +55,8 @@ export interface Feed { type: 'website' | 'rss'; is_active: boolean; description: string | null; + language_id?: number; + provider?: string; created_at: string; updated_at: string; articles_count?: number; @@ -78,6 +80,8 @@ export interface PlatformAccount { display_name: string | null; description: string | null; is_active: boolean; + instance_url?: string; + password?: string; created_at: string; updated_at: string; } @@ -90,6 +94,7 @@ export interface PlatformChannel { display_name: string | null; description: string | null; is_active: boolean; + language_id?: number; created_at: string; updated_at: string; platform_instance?: PlatformInstance; @@ -442,6 +447,10 @@ class ApiClient { const response = await axios.get>('/platform-accounts'); return response.data.data; } + + async deletePlatformAccount(id: number): Promise { + await axios.delete(`/platform-accounts/${id}`); + } } export const apiClient = new ApiClient(); \ No newline at end of file diff --git a/frontend/src/pages/onboarding/steps/ChannelStep.tsx b/frontend/src/pages/onboarding/steps/ChannelStep.tsx index 24dcf9e..cd69426 100644 --- a/frontend/src/pages/onboarding/steps/ChannelStep.tsx +++ b/frontend/src/pages/onboarding/steps/ChannelStep.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { Link, useNavigate } from 'react-router-dom'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { apiClient, type ChannelRequest, type Language, type PlatformInstance } from '../../../lib/api'; @@ -20,6 +20,26 @@ const ChannelStep: React.FC = () => { queryFn: () => apiClient.getOnboardingOptions() }); + // Fetch existing channels to pre-fill form when going back + const { data: channels } = useQuery({ + queryKey: ['platform-channels'], + queryFn: () => apiClient.getPlatformChannels(), + retry: false, + }); + + // Pre-fill form with existing data + useEffect(() => { + if (channels && channels.length > 0) { + const firstChannel = channels[0]; + setFormData({ + name: firstChannel.name || '', + platform_instance_id: firstChannel.platform_instance_id || 0, + language_id: firstChannel.language_id || 0, + description: firstChannel.description || '' + }); + } + }, [channels]); + const createChannelMutation = useMutation({ mutationFn: (data: ChannelRequest) => apiClient.createChannelForOnboarding(data), onSuccess: () => { diff --git a/frontend/src/pages/onboarding/steps/FeedStep.tsx b/frontend/src/pages/onboarding/steps/FeedStep.tsx index da6e54a..1a2682e 100644 --- a/frontend/src/pages/onboarding/steps/FeedStep.tsx +++ b/frontend/src/pages/onboarding/steps/FeedStep.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { Link, useNavigate } from 'react-router-dom'; import { useMutation, useQuery } from '@tanstack/react-query'; import { apiClient, type FeedRequest, type Language, type FeedProvider } from '../../../lib/api'; @@ -19,6 +19,26 @@ const FeedStep: React.FC = () => { queryFn: () => apiClient.getOnboardingOptions() }); + // Fetch existing feeds to pre-fill form when going back + const { data: feeds } = useQuery({ + queryKey: ['feeds'], + queryFn: () => apiClient.getFeeds(), + retry: false, + }); + + // Pre-fill form with existing data + useEffect(() => { + if (feeds && feeds.length > 0) { + const firstFeed = feeds[0]; + setFormData({ + name: firstFeed.name || '', + provider: firstFeed.provider || 'vrt', + language_id: firstFeed.language_id ?? 0, + description: firstFeed.description || '' + }); + } + }, [feeds]); + const createFeedMutation = useMutation({ mutationFn: (data: FeedRequest) => apiClient.createFeedForOnboarding(data), onSuccess: () => { diff --git a/frontend/src/pages/onboarding/steps/PlatformStep.tsx b/frontend/src/pages/onboarding/steps/PlatformStep.tsx index 95b4a3f..0632cd7 100644 --- a/frontend/src/pages/onboarding/steps/PlatformStep.tsx +++ b/frontend/src/pages/onboarding/steps/PlatformStep.tsx @@ -1,10 +1,11 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { Link, useNavigate } from 'react-router-dom'; -import { useMutation } from '@tanstack/react-query'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { apiClient, type PlatformAccountRequest } from '../../../lib/api'; const PlatformStep: React.FC = () => { const navigate = useNavigate(); + const queryClient = useQueryClient(); const [formData, setFormData] = useState({ instance_url: '', username: '', @@ -13,9 +14,17 @@ const PlatformStep: React.FC = () => { }); const [errors, setErrors] = useState>({}); + // Fetch existing platform accounts + const { data: platformAccounts, isLoading } = useQuery({ + queryKey: ['platform-accounts'], + queryFn: () => apiClient.getPlatformAccounts(), + retry: false, + }); + const createPlatformMutation = useMutation({ mutationFn: (data: PlatformAccountRequest) => apiClient.createPlatformAccount(data), onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['platform-accounts'] }); navigate('/onboarding/feed'); }, onError: (error: any) => { @@ -27,6 +36,17 @@ const PlatformStep: React.FC = () => { } }); + const deletePlatformMutation = useMutation({ + mutationFn: (accountId: number) => apiClient.deletePlatformAccount(accountId), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['platform-accounts'] }); + setErrors({}); + }, + onError: (error: any) => { + setErrors({ general: [error.response?.data?.message || 'Failed to delete account'] }); + } + }); + const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); setErrors({}); @@ -41,11 +61,28 @@ const PlatformStep: React.FC = () => { } }; + const handleDeleteAccount = (accountId: number) => { + if (confirm('Are you sure you want to remove this account? You will need to re-enter your credentials.')) { + deletePlatformMutation.mutate(accountId); + } + }; + + const handleContinueWithExisting = () => { + navigate('/onboarding/feed'); + }; + + // Show loading state + if (isLoading) { + return
Loading...
; + } + + const existingAccount = platformAccounts && platformAccounts.length > 0 ? platformAccounts[0] : null; + return (

Connect Your Lemmy Account

- Enter your Lemmy instance details and login credentials + {existingAccount ? 'Your connected Lemmy account' : 'Enter your Lemmy instance details and login credentials'}

{/* Progress indicator */} @@ -56,82 +93,133 @@ const PlatformStep: React.FC = () => {
4
-
- {errors.general && ( -
-

{errors.general[0]}

+ {/* Show errors */} + {errors.general && ( +
+

{errors.general[0]}

+
+ )} + + {existingAccount ? ( + /* Account Card */ +
+
+
+
+
+ + + +
+
+

Account Connected

+
+

Username: {existingAccount.username}

+

Instance: {existingAccount.instance_url?.replace('https://', '')}

+
+
+
+ +
+
+ +
+ + ← Back + +
- )} - -
- - handleChange('instance_url', e.target.value)} - placeholder="lemmy.world" - className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" - required - /> -

Enter just the domain name (e.g., lemmy.world, belgae.social)

- {errors.instance_url && ( -

{errors.instance_url[0]}

- )}
- -
- - handleChange('username', e.target.value)} - className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" - required - /> - {errors.username && ( -

{errors.username[0]}

- )} -
- -
- - handleChange('password', e.target.value)} - className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" - required - /> - {errors.password && ( -

{errors.password[0]}

- )} -
- -
- - ← Back - - -
- + ) : ( + /* Login Form */ +
+
+ + handleChange('instance_url', e.target.value)} + placeholder="lemmy.world" + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" + required + /> +

Enter just the domain name (e.g., lemmy.world, belgae.social)

+ {errors.instance_url && ( +

{errors.instance_url[0]}

+ )} +
+ +
+ + handleChange('username', e.target.value)} + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" + required + /> + {errors.username && ( +

{errors.username[0]}

+ )} +
+ +
+ + handleChange('password', e.target.value)} + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" + required + /> + {errors.password && ( +

{errors.password[0]}

+ )} +
+ +
+ + ← Back + + +
+
+ )}
); }; diff --git a/frontend/src/pages/onboarding/steps/RouteStep.tsx b/frontend/src/pages/onboarding/steps/RouteStep.tsx index fed6249..51b6aa9 100644 --- a/frontend/src/pages/onboarding/steps/RouteStep.tsx +++ b/frontend/src/pages/onboarding/steps/RouteStep.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { Link, useNavigate } from 'react-router-dom'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { apiClient, type RouteRequest, type Feed, type PlatformChannel } from '../../../lib/api'; @@ -19,6 +19,25 @@ const RouteStep: React.FC = () => { queryFn: () => apiClient.getOnboardingOptions() }); + // Fetch existing routes to pre-fill form when going back + const { data: routes } = useQuery({ + queryKey: ['routes'], + queryFn: () => apiClient.getRoutes(), + retry: false, + }); + + // Pre-fill form with existing data + useEffect(() => { + if (routes && routes.length > 0) { + const firstRoute = routes[0]; + setFormData({ + feed_id: firstRoute.feed_id || 0, + platform_channel_id: firstRoute.platform_channel_id || 0, + priority: firstRoute.priority || 50 + }); + } + }, [routes]); + const createRouteMutation = useMutation({ mutationFn: (data: RouteRequest) => apiClient.createRouteForOnboarding(data), onSuccess: () => { diff --git a/frontend/src/pages/onboarding/steps/WelcomeStep.tsx b/frontend/src/pages/onboarding/steps/WelcomeStep.tsx index 9678f55..ba6b2f6 100644 --- a/frontend/src/pages/onboarding/steps/WelcomeStep.tsx +++ b/frontend/src/pages/onboarding/steps/WelcomeStep.tsx @@ -1,23 +1,7 @@ import React from 'react'; -import { Link, useNavigate } from 'react-router-dom'; -import { useMutation } from '@tanstack/react-query'; -import { apiClient } from '../../../lib/api'; +import { Link } from 'react-router-dom'; const WelcomeStep: React.FC = () => { - const navigate = useNavigate(); - - const skipMutation = useMutation({ - mutationFn: () => apiClient.skipOnboarding(), - onSuccess: () => { - navigate('/dashboard'); - }, - }); - - const handleSkip = () => { - if (confirm('Are you sure you want to skip the setup? You can configure FFR later from the settings page.')) { - skipMutation.mutate(); - } - }; return (
@@ -49,21 +33,13 @@ const WelcomeStep: React.FC = () => {
-
+
Get Started - -
);