diff --git a/backend/app/Http/Controllers/Api/V1/OnboardingController.php b/backend/app/Http/Controllers/Api/V1/OnboardingController.php index d77057b..2553b33 100644 --- a/backend/app/Http/Controllers/Api/V1/OnboardingController.php +++ b/backend/app/Http/Controllers/Api/V1/OnboardingController.php @@ -11,6 +11,7 @@ use App\Models\PlatformAccount; use App\Models\PlatformChannel; use App\Models\PlatformInstance; +use App\Models\Setting; use App\Services\Auth\LemmyAuthService; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -31,8 +32,12 @@ public function status(): JsonResponse $hasPlatformAccount = PlatformAccount::where('is_active', true)->exists(); $hasFeed = Feed::where('is_active', true)->exists(); $hasChannel = PlatformChannel::where('is_active', true)->exists(); + + // Check if onboarding was explicitly skipped + $onboardingSkipped = Setting::where('key', 'onboarding_skipped')->value('value') === 'true'; - $needsOnboarding = !$hasPlatformAccount || !$hasFeed || !$hasChannel; + // User needs onboarding if they don't have the required components AND haven't skipped it + $needsOnboarding = (!$hasPlatformAccount || !$hasFeed || !$hasChannel) && !$onboardingSkipped; // Determine current step $currentStep = null; @@ -52,6 +57,7 @@ public function status(): JsonResponse 'has_platform_account' => $hasPlatformAccount, 'has_feed' => $hasFeed, 'has_channel' => $hasChannel, + 'onboarding_skipped' => $onboardingSkipped, ], 'Onboarding status retrieved successfully.'); } @@ -80,10 +86,12 @@ public function options(): JsonResponse public function createPlatform(Request $request): JsonResponse { $validator = Validator::make($request->all(), [ - 'instance_url' => 'required|url|max:255', + 'instance_url' => 'required|string|max:255|regex:/^[a-zA-Z0-9]([a-zA-Z0-9\-\.]*[a-zA-Z0-9])?$/', 'username' => 'required|string|max:255', 'password' => 'required|string|min:6', 'platform' => 'required|in:lemmy', + ], [ + 'instance_url.regex' => 'Please enter a valid domain name (e.g., lemmy.world, belgae.social)' ]); if ($validator->fails()) { @@ -92,19 +100,23 @@ public function createPlatform(Request $request): JsonResponse $validated = $validator->validated(); + // Normalize the instance URL - prepend https:// if needed + $instanceDomain = $validated['instance_url']; + $fullInstanceUrl = 'https://' . $instanceDomain; + try { // Create or get platform instance $platformInstance = PlatformInstance::firstOrCreate([ - 'url' => $validated['instance_url'], + 'url' => $fullInstanceUrl, 'platform' => $validated['platform'], ], [ - 'name' => parse_url($validated['instance_url'], PHP_URL_HOST) ?? 'Lemmy Instance', + 'name' => ucfirst($instanceDomain), 'is_active' => true, ]); - // Authenticate with Lemmy API + // Authenticate with Lemmy API using the full URL $authResponse = $this->lemmyAuthService->authenticate( - $validated['instance_url'], + $fullInstanceUrl, $validated['username'], $validated['password'] ); @@ -112,7 +124,7 @@ public function createPlatform(Request $request): JsonResponse // Create platform account with the current schema $platformAccount = PlatformAccount::create([ 'platform' => $validated['platform'], - 'instance_url' => $validated['instance_url'], + 'instance_url' => $fullInstanceUrl, 'username' => $validated['username'], 'password' => $validated['password'], 'api_token' => $authResponse['jwt'] ?? null, @@ -232,4 +244,33 @@ public function complete(): JsonResponse 'Onboarding completed successfully.' ); } + + /** + * Skip onboarding - user can access the app without completing setup + */ + public function skip(): JsonResponse + { + Setting::updateOrCreate( + ['key' => 'onboarding_skipped'], + ['value' => 'true'] + ); + + return $this->sendResponse( + ['skipped' => true], + 'Onboarding skipped successfully.' + ); + } + + /** + * Reset onboarding skip status - force user back to onboarding + */ + public function resetSkip(): JsonResponse + { + Setting::where('key', 'onboarding_skipped')->delete(); + + return $this->sendResponse( + ['reset' => true], + 'Onboarding skip status reset successfully.' + ); + } } \ No newline at end of file diff --git a/backend/app/Modules/Lemmy/LemmyRequest.php b/backend/app/Modules/Lemmy/LemmyRequest.php index 5bdb5d8..1c6cdc2 100644 --- a/backend/app/Modules/Lemmy/LemmyRequest.php +++ b/backend/app/Modules/Lemmy/LemmyRequest.php @@ -12,10 +12,25 @@ class LemmyRequest public function __construct(string $instance, ?string $token = null) { - $this->instance = $instance; + // Handle both full URLs and just domain names + $this->instance = $this->normalizeInstance($instance); $this->token = $token; } + /** + * Normalize instance URL to just the domain name + */ + private function normalizeInstance(string $instance): string + { + // Remove protocol if present + $instance = preg_replace('/^https?:\/\//', '', $instance); + + // Remove trailing slash if present + $instance = rtrim($instance, '/'); + + return $instance; + } + /** * @param array $params */ diff --git a/backend/routes/api.php b/backend/routes/api.php index f89d445..3966f2d 100644 --- a/backend/routes/api.php +++ b/backend/routes/api.php @@ -41,6 +41,8 @@ Route::post('/onboarding/feed', [OnboardingController::class, 'createFeed'])->name('api.onboarding.feed'); Route::post('/onboarding/channel', [OnboardingController::class, 'createChannel'])->name('api.onboarding.channel'); Route::post('/onboarding/complete', [OnboardingController::class, 'complete'])->name('api.onboarding.complete'); + Route::post('/onboarding/skip', [OnboardingController::class, 'skip'])->name('api.onboarding.skip'); + Route::post('/onboarding/reset-skip', [OnboardingController::class, 'resetSkip'])->name('api.onboarding.reset-skip'); // Dashboard stats Route::get('/dashboard/stats', [DashboardController::class, 'stats'])->name('api.dashboard.stats'); diff --git a/backend/tests/Feature/Http/Controllers/Api/V1/OnboardingControllerTest.php b/backend/tests/Feature/Http/Controllers/Api/V1/OnboardingControllerTest.php new file mode 100644 index 0000000..dc98eb2 --- /dev/null +++ b/backend/tests/Feature/Http/Controllers/Api/V1/OnboardingControllerTest.php @@ -0,0 +1,364 @@ +create([ + 'id' => 1, + 'short_code' => 'en', + 'name' => 'English', + 'native_name' => 'English', + 'is_active' => true, + ]); + } + + public function test_status_shows_needs_onboarding_when_no_components_exist() + { + $response = $this->getJson('/api/v1/onboarding/status'); + + $response->assertStatus(200) + ->assertJson([ + 'success' => true, + 'data' => [ + 'needs_onboarding' => true, + 'current_step' => 'platform', + 'has_platform_account' => false, + 'has_feed' => false, + 'has_channel' => false, + 'onboarding_skipped' => false, + ], + ]); + } + + public function test_status_shows_feed_step_when_platform_account_exists() + { + PlatformAccount::factory()->create(['is_active' => true]); + + $response = $this->getJson('/api/v1/onboarding/status'); + + $response->assertStatus(200) + ->assertJson([ + 'success' => true, + 'data' => [ + 'needs_onboarding' => true, + 'current_step' => 'feed', + 'has_platform_account' => true, + 'has_feed' => false, + 'has_channel' => false, + ], + ]); + } + + public function test_status_shows_channel_step_when_platform_account_and_feed_exist() + { + PlatformAccount::factory()->create(['is_active' => true]); + Feed::factory()->create(['is_active' => true]); + + $response = $this->getJson('/api/v1/onboarding/status'); + + $response->assertStatus(200) + ->assertJson([ + 'success' => true, + 'data' => [ + 'needs_onboarding' => true, + 'current_step' => 'channel', + 'has_platform_account' => true, + 'has_feed' => true, + 'has_channel' => false, + ], + ]); + } + + public function test_status_shows_no_onboarding_needed_when_all_components_exist() + { + PlatformAccount::factory()->create(['is_active' => true]); + Feed::factory()->create(['is_active' => true]); + PlatformChannel::factory()->create(['is_active' => true]); + + $response = $this->getJson('/api/v1/onboarding/status'); + + $response->assertStatus(200) + ->assertJson([ + 'success' => true, + 'data' => [ + 'needs_onboarding' => false, + 'current_step' => null, + 'has_platform_account' => true, + 'has_feed' => true, + 'has_channel' => true, + ], + ]); + } + + public function test_status_shows_no_onboarding_needed_when_skipped() + { + // No components exist but onboarding is skipped + Setting::create([ + 'key' => 'onboarding_skipped', + 'value' => 'true', + ]); + + $response = $this->getJson('/api/v1/onboarding/status'); + + $response->assertStatus(200) + ->assertJson([ + 'success' => true, + 'data' => [ + 'needs_onboarding' => false, + 'current_step' => null, + 'has_platform_account' => false, + 'has_feed' => false, + 'has_channel' => false, + 'onboarding_skipped' => true, + ], + ]); + } + + public function test_options_returns_languages_and_platform_instances() + { + PlatformInstance::factory()->create([ + 'platform' => 'lemmy', + 'url' => 'https://lemmy.world', + 'name' => 'Lemmy World', + 'is_active' => true, + ]); + + $response = $this->getJson('/api/v1/onboarding/options'); + + $response->assertStatus(200) + ->assertJsonStructure([ + 'success', + 'data' => [ + 'languages' => [ + '*' => ['id', 'short_code', 'name', 'native_name', 'is_active'] + ], + 'platform_instances' => [ + '*' => ['id', 'platform', 'url', 'name', 'description', 'is_active'] + ] + ] + ]); + } + + public function test_create_feed_validates_required_fields() + { + $response = $this->postJson('/api/v1/onboarding/feed', []); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['name', 'url', 'type', 'language_id']); + } + + public function test_create_feed_creates_feed_successfully() + { + $feedData = [ + 'name' => 'Test Feed', + 'url' => 'https://example.com/rss', + 'type' => 'rss', + 'language_id' => 1, + 'description' => 'Test description', + ]; + + $response = $this->postJson('/api/v1/onboarding/feed', $feedData); + + $response->assertStatus(200) + ->assertJson([ + 'success' => true, + 'data' => [ + 'name' => 'Test Feed', + 'url' => 'https://example.com/rss', + 'type' => 'rss', + 'is_active' => true, + ] + ]); + + $this->assertDatabaseHas('feeds', [ + 'name' => 'Test Feed', + 'url' => 'https://example.com/rss', + 'type' => 'rss', + 'language_id' => 1, + 'is_active' => true, + ]); + } + + public function test_create_channel_validates_required_fields() + { + $response = $this->postJson('/api/v1/onboarding/channel', []); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['name', 'platform_instance_id', 'language_id']); + } + + public function test_create_channel_creates_channel_successfully() + { + $platformInstance = PlatformInstance::factory()->create(); + + $channelData = [ + 'name' => 'test_community', + 'platform_instance_id' => $platformInstance->id, + 'language_id' => 1, + 'description' => 'Test community description', + ]; + + $response = $this->postJson('/api/v1/onboarding/channel', $channelData); + + $response->assertStatus(200) + ->assertJson([ + 'success' => true, + 'data' => [ + 'name' => 'test_community', + 'display_name' => 'Test_community', + 'channel_id' => 'test_community', + 'is_active' => true, + ] + ]); + + $this->assertDatabaseHas('platform_channels', [ + 'name' => 'test_community', + 'channel_id' => 'test_community', + 'platform_instance_id' => $platformInstance->id, + 'language_id' => 1, + 'is_active' => true, + ]); + } + + public function test_complete_onboarding_returns_success() + { + $response = $this->postJson('/api/v1/onboarding/complete'); + + $response->assertStatus(200) + ->assertJson([ + 'success' => true, + 'data' => ['completed' => true] + ]); + } + + public function test_skip_onboarding_creates_setting() + { + $response = $this->postJson('/api/v1/onboarding/skip'); + + $response->assertStatus(200) + ->assertJson([ + 'success' => true, + 'data' => ['skipped' => true] + ]); + + $this->assertDatabaseHas('settings', [ + 'key' => 'onboarding_skipped', + 'value' => 'true', + ]); + } + + public function test_skip_onboarding_updates_existing_setting() + { + // Create existing setting with false value + Setting::create([ + 'key' => 'onboarding_skipped', + 'value' => 'false', + ]); + + $response = $this->postJson('/api/v1/onboarding/skip'); + + $response->assertStatus(200); + + $this->assertDatabaseHas('settings', [ + 'key' => 'onboarding_skipped', + 'value' => 'true', + ]); + + // Ensure only one setting exists + $this->assertEquals(1, Setting::where('key', 'onboarding_skipped')->count()); + } + + public function test_reset_skip_removes_setting() + { + // Create skipped setting + Setting::create([ + 'key' => 'onboarding_skipped', + 'value' => 'true', + ]); + + $response = $this->postJson('/api/v1/onboarding/reset-skip'); + + $response->assertStatus(200) + ->assertJson([ + 'success' => true, + 'data' => ['reset' => true] + ]); + + $this->assertDatabaseMissing('settings', [ + 'key' => 'onboarding_skipped', + ]); + } + + public function test_reset_skip_works_when_no_setting_exists() + { + $response = $this->postJson('/api/v1/onboarding/reset-skip'); + + $response->assertStatus(200) + ->assertJson([ + 'success' => true, + 'data' => ['reset' => true] + ]); + } + + public function test_create_platform_validates_instance_url_format() + { + $response = $this->postJson('/api/v1/onboarding/platform', [ + 'instance_url' => 'invalid.domain.with.spaces and symbols!', + 'username' => 'testuser', + 'password' => 'password123', + 'platform' => 'lemmy', + ]); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['instance_url']); + } + + public function test_create_platform_validates_required_fields() + { + $response = $this->postJson('/api/v1/onboarding/platform', []); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['instance_url', 'username', 'password', 'platform']); + } + + public function test_onboarding_flow_integration() + { + // 1. Initial status - needs onboarding + $response = $this->getJson('/api/v1/onboarding/status'); + $response->assertJson(['data' => ['needs_onboarding' => true, 'current_step' => 'platform']]); + + // 2. Skip onboarding + $response = $this->postJson('/api/v1/onboarding/skip'); + $response->assertJson(['data' => ['skipped' => true]]); + + // 3. Status after skip - no longer needs onboarding + $response = $this->getJson('/api/v1/onboarding/status'); + $response->assertJson(['data' => ['needs_onboarding' => false, 'onboarding_skipped' => true]]); + + // 4. Reset skip + $response = $this->postJson('/api/v1/onboarding/reset-skip'); + $response->assertJson(['data' => ['reset' => true]]); + + // 5. Status after reset - needs onboarding again + $response = $this->getJson('/api/v1/onboarding/status'); + $response->assertJson(['data' => ['needs_onboarding' => true, 'onboarding_skipped' => false]]); + } +} \ No newline at end of file diff --git a/docker/dev/podman/.env.dev b/docker/dev/podman/.env.dev index f0df808..f81be47 100644 --- a/docker/dev/podman/.env.dev +++ b/docker/dev/podman/.env.dev @@ -1,6 +1,6 @@ APP_NAME="FFR Development" APP_ENV=local -APP_KEY=base64:YOUR_APP_KEY_HERE +APP_KEY= APP_DEBUG=true APP_TIMEZONE=UTC APP_URL=http://localhost:8000 diff --git a/docker/dev/podman/container-start.sh b/docker/dev/podman/container-start.sh index 580feb7..175e412 100755 --- a/docker/dev/podman/container-start.sh +++ b/docker/dev/podman/container-start.sh @@ -3,19 +3,17 @@ # Copy development environment configuration to backend cp /var/www/html/docker/dev/podman/.env.dev /var/www/html/backend/.env -# Setup nginx configuration -cp /var/www/html/docker/nginx.conf /etc/nginx/sites-available/default +# Setup nginx configuration for development +cp /var/www/html/docker/dev/podman/nginx.conf /etc/nginx/sites-available/default # Install/update dependencies echo "Installing PHP dependencies..." cd /var/www/html/backend composer install --no-interaction -# Generate app key if not set or empty -if grep -q "APP_KEY=base64:YOUR_APP_KEY_HERE" /var/www/html/backend/.env || ! grep -q "APP_KEY=base64:" /var/www/html/backend/.env; then - echo "Generating application key..." - php artisan key:generate --force -fi +# Generate application key +echo "Generating application key..." +php artisan key:generate --force # Wait for database to be ready echo "Waiting for database..." @@ -39,6 +37,10 @@ fi # Start services echo "Starting services..." +# Start React dev server +cd /var/www/html/frontend +npm run dev -- --host 0.0.0.0 --port 5173 & + # Start Laravel backend cd /var/www/html/backend php artisan serve --host=127.0.0.1 --port=8000 & diff --git a/docker/dev/podman/nginx.conf b/docker/dev/podman/nginx.conf new file mode 100644 index 0000000..ef692f5 --- /dev/null +++ b/docker/dev/podman/nginx.conf @@ -0,0 +1,87 @@ +server { + listen 80; + server_name localhost; + + # Proxy API requests to Laravel backend + location /api/ { + proxy_pass http://127.0.0.1:8000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_redirect off; + } + + # Serve Laravel public assets (images, etc.) + location /images/ { + alias /var/www/html/backend/public/images/; + expires 1y; + add_header Cache-Control "public, immutable"; + } + + # Proxy Vite dev server assets + location /@vite/ { + proxy_pass http://127.0.0.1:5173; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_redirect off; + } + + # Proxy Vite HMR WebSocket + location /@vite/client { + proxy_pass http://127.0.0.1:5173; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_redirect off; + } + + # Proxy node_modules for Vite deps + location /node_modules/ { + proxy_pass http://127.0.0.1:5173; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_redirect off; + } + + # Proxy /src/ for Vite source files + location /src/ { + proxy_pass http://127.0.0.1:5173; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_redirect off; + } + + # Proxy React dev server for development (catch-all) + location / { + proxy_pass http://127.0.0.1:5173; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # WebSocket support for HMR + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_redirect off; + } + + # Security headers + add_header X-Content-Type-Options nosniff; + add_header X-Frame-Options DENY; + add_header X-XSS-Protection "1; mode=block"; + + # Gzip compression + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json; +} \ No newline at end of file diff --git a/docker/dev/podman/start-dev.sh b/docker/dev/podman/start-dev.sh index 29681b8..00c9060 100755 --- a/docker/dev/podman/start-dev.sh +++ b/docker/dev/podman/start-dev.sh @@ -10,14 +10,12 @@ echo "🚀 Starting FFR development environment with Podman..." if [ ! -f .env ]; then echo "📋 Creating .env file from .env.example..." cp .env.example .env - echo "⚠️ Please update your .env file with appropriate values, especially APP_KEY" fi # Check if podman-compose is available if ! command -v podman-compose &> /dev/null; then - echo "❌ podman-compose not found. Installing..." - pip3 install --user podman-compose - echo "✅ podman-compose installed" + echo "❌ podman-compose not found." + exit fi # Start services @@ -28,22 +26,32 @@ podman-compose -f docker/dev/podman/docker-compose.yml up -d echo "⏳ Waiting for database to be ready..." sleep 10 -# Check if APP_KEY is set -if grep -q "APP_KEY=base64:YOUR_APP_KEY_HERE" .env || grep -q "APP_KEY=$" .env; then - echo "🔑 Generating application key..." - podman exec ffr-dev-app php artisan key:generate -fi +# Install/update dependencies if needed +echo "📦 Installing dependencies..." +podman exec ffr-dev-app bash -c "cd /var/www/html/backend && composer install" +podman exec ffr-dev-app bash -c "cd /var/www/html/frontend && npm install" # Run migrations and seeders echo "🗃️ Running database migrations..." -podman exec ffr-dev-app php artisan migrate --force +podman exec ffr-dev-app bash -c "cd /var/www/html/backend && php artisan migrate --force" echo "🌱 Running database seeders..." -podman exec ffr-dev-app php artisan db:seed --force +podman exec ffr-dev-app bash -c "cd /var/www/html/backend && php artisan db:seed --force" -# Install/update dependencies if needed -echo "📦 Installing dependencies..." -podman exec ffr-dev-app composer install -podman exec ffr-dev-app npm install +# Wait for container services to be fully ready +echo "⏳ Waiting for container services to initialize..." +sleep 5 + +# Start React dev server if not already running +echo "🚀 Starting React dev server..." +podman exec -d ffr-dev-app bash -c "cd /var/www/html/frontend && npm run dev -- --host 0.0.0.0 --port 5173 > /dev/null 2>&1 &" +sleep 5 + +# Verify Vite is running +if podman exec ffr-dev-app bash -c "curl -s http://localhost:5173 > /dev/null 2>&1"; then + echo "✅ Vite dev server is running" +else + echo "⚠️ Vite dev server may not have started properly" +fi echo "✅ Development environment is ready!" echo "🌐 Application: http://localhost:8000" @@ -55,4 +63,4 @@ echo "📋 Useful commands:" echo " Stop: podman-compose -f docker/dev/podman/docker-compose.yml down" echo " Logs: podman-compose -f docker/dev/podman/docker-compose.yml logs -f" echo " Exec: podman exec -it ffr-dev-app bash" -echo " Tests: podman exec ffr-dev-app php artisan test" \ No newline at end of file +echo " Tests: podman exec ffr-dev-app php artisan test" diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 04a0e4b..179022a 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -6,22 +6,8 @@ import Articles from './pages/Articles'; import Feeds from './pages/Feeds'; import Settings from './pages/Settings'; import OnboardingWizard from './pages/onboarding/OnboardingWizard'; -import { OnboardingProvider, useOnboarding } from './contexts/OnboardingContext'; - -const AppContent: React.FC = () => { - const { isLoading } = useOnboarding(); - - if (isLoading) { - return ( -
-
-
-

Loading...

-
-
- ); - } +const App: React.FC = () => { return ( {/* Onboarding routes - outside of main layout */} @@ -44,12 +30,4 @@ const AppContent: React.FC = () => { ); }; -const App: React.FC = () => { - return ( - - - - ); -}; - export default App; diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index 161cc32..5f07b0d 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -129,6 +129,8 @@ const Layout: React.FC = ({ children }) => {

FFR

+ +
{children}
diff --git a/frontend/src/contexts/OnboardingContext.tsx b/frontend/src/contexts/OnboardingContext.tsx index 47b9287..b5a1525 100644 --- a/frontend/src/contexts/OnboardingContext.tsx +++ b/frontend/src/contexts/OnboardingContext.tsx @@ -28,22 +28,17 @@ export const OnboardingProvider: React.FC = ({ children const needsOnboarding = onboardingStatus?.needs_onboarding ?? false; const isOnOnboardingPage = location.pathname.startsWith('/onboarding'); - // Redirect logic + // Redirect logic - only redirect if user explicitly navigates to a protected route React.useEffect(() => { if (isLoading) return; - if (needsOnboarding && !isOnOnboardingPage) { - // User needs onboarding but is not on onboarding pages - const targetStep = onboardingStatus?.current_step; - if (targetStep) { - navigate(`/onboarding/${targetStep}`, { replace: true }); - } else { - navigate('/onboarding', { replace: true }); - } - } else if (!needsOnboarding && isOnOnboardingPage) { - // User doesn't need onboarding but is on onboarding pages + // Only redirect if user doesn't need onboarding but is on onboarding pages + if (!needsOnboarding && isOnOnboardingPage) { navigate('/dashboard', { replace: true }); } + + // Don't auto-redirect to onboarding - let user navigate manually or via links + // This prevents the app from being "stuck" in onboarding mode }, [onboardingStatus, isLoading, needsOnboarding, isOnOnboardingPage, navigate]); const value: OnboardingContextValue = { diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index b2fd143..01a004a 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -151,6 +151,7 @@ export interface OnboardingStatus { has_platform_account: boolean; has_feed: boolean; has_channel: boolean; + onboarding_skipped: boolean; } export interface OnboardingOptions { @@ -288,6 +289,14 @@ class ApiClient { async completeOnboarding(): Promise { await axios.post('/onboarding/complete'); } + + async skipOnboarding(): Promise { + await axios.post('/onboarding/skip'); + } + + async resetOnboardingSkip(): Promise { + await axios.post('/onboarding/reset-skip'); + } } export const apiClient = new ApiClient(); \ No newline at end of file diff --git a/frontend/src/pages/onboarding/steps/PlatformStep.tsx b/frontend/src/pages/onboarding/steps/PlatformStep.tsx index 9d13b52..95b4a3f 100644 --- a/frontend/src/pages/onboarding/steps/PlatformStep.tsx +++ b/frontend/src/pages/onboarding/steps/PlatformStep.tsx @@ -65,17 +65,18 @@ const PlatformStep: React.FC = () => {
handleChange('instance_url', e.target.value)} - placeholder="https://lemmy.world" + 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]}

)} diff --git a/frontend/src/pages/onboarding/steps/WelcomeStep.tsx b/frontend/src/pages/onboarding/steps/WelcomeStep.tsx index c4a39d1..793c1af 100644 --- a/frontend/src/pages/onboarding/steps/WelcomeStep.tsx +++ b/frontend/src/pages/onboarding/steps/WelcomeStep.tsx @@ -1,7 +1,24 @@ import React from 'react'; -import { Link } from 'react-router-dom'; +import { Link, useNavigate } from 'react-router-dom'; +import { useMutation } from '@tanstack/react-query'; +import { apiClient } from '../../../lib/api'; 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 (

Welcome to FFR

@@ -28,13 +45,21 @@ const WelcomeStep: React.FC = () => {
-
+
Get Started + +
);