Fix nginx error, startup script, add skip

This commit is contained in:
myrmidex 2025-08-09 02:51:18 +02:00
parent 73ba089e46
commit 387920e82b
14 changed files with 600 additions and 71 deletions

View file

@ -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;
@ -32,7 +33,11 @@ public function status(): JsonResponse
$hasFeed = Feed::where('is_active', true)->exists();
$hasChannel = PlatformChannel::where('is_active', true)->exists();
$needsOnboarding = !$hasPlatformAccount || !$hasFeed || !$hasChannel;
// Check if onboarding was explicitly skipped
$onboardingSkipped = Setting::where('key', 'onboarding_skipped')->value('value') === 'true';
// 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.'
);
}
}

View file

@ -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<string, mixed> $params
*/

View file

@ -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');

View file

@ -0,0 +1,364 @@
<?php
namespace Tests\Feature\Http\Controllers\Api\V1;
use App\Models\Feed;
use App\Models\Language;
use App\Models\PlatformAccount;
use App\Models\PlatformChannel;
use App\Models\PlatformInstance;
use App\Models\Setting;
use App\Services\Auth\LemmyAuthService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class OnboardingControllerTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
// Create a language for testing
Language::factory()->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]]);
}
}

View file

@ -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

View file

@ -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
# Generate application key
echo "Generating application key..."
php artisan key:generate --force
fi
# 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 &

View file

@ -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;
}

View file

@ -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"

View file

@ -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 (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-4"></div>
<p className="text-gray-600">Loading...</p>
</div>
</div>
);
}
const App: React.FC = () => {
return (
<Routes>
{/* Onboarding routes - outside of main layout */}
@ -44,12 +30,4 @@ const AppContent: React.FC = () => {
);
};
const App: React.FC = () => {
return (
<OnboardingProvider>
<AppContent />
</OnboardingProvider>
);
};
export default App;

View file

@ -129,6 +129,8 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
<h1 className="text-lg font-medium text-gray-900">FFR</h1>
</div>
</div>
<main className="flex-1">
{children}
</main>

View file

@ -28,22 +28,17 @@ export const OnboardingProvider: React.FC<OnboardingProviderProps> = ({ 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 = {

View file

@ -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<void> {
await axios.post('/onboarding/complete');
}
async skipOnboarding(): Promise<void> {
await axios.post('/onboarding/skip');
}
async resetOnboardingSkip(): Promise<void> {
await axios.post('/onboarding/reset-skip');
}
}
export const apiClient = new ApiClient();

View file

@ -65,17 +65,18 @@ const PlatformStep: React.FC = () => {
<div>
<label htmlFor="instance_url" className="block text-sm font-medium text-gray-700 mb-2">
Lemmy Instance URL
Lemmy Instance Domain
</label>
<input
type="url"
type="text"
id="instance_url"
value={formData.instance_url}
onChange={(e) => 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
/>
<p className="text-sm text-gray-500 mt-1">Enter just the domain name (e.g., lemmy.world, belgae.social)</p>
{errors.instance_url && (
<p className="text-red-600 text-sm mt-1">{errors.instance_url[0]}</p>
)}

View file

@ -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 (
<div className="text-center">
<h1 className="text-3xl font-bold text-gray-900 mb-2">Welcome to FFR</h1>
@ -28,13 +45,21 @@ const WelcomeStep: React.FC = () => {
</div>
</div>
<div className="mt-8">
<div className="mt-8 space-y-3">
<Link
to="/onboarding/platform"
className="w-full bg-blue-600 text-white py-3 px-4 rounded-md hover:bg-blue-700 transition duration-200 inline-block"
>
Get Started
</Link>
<button
onClick={handleSkip}
disabled={skipMutation.isPending}
className="w-full text-gray-500 hover:text-gray-700 py-2 px-4 text-sm transition duration-200 disabled:opacity-50"
>
{skipMutation.isPending ? 'Skipping...' : 'Skip for now'}
</button>
</div>
</div>
);