From 01493ac0bfae566182379a86c8d14ad33e5b67d0 Mon Sep 17 00:00:00 2001 From: myrmidex Date: Sat, 2 Aug 2025 15:28:38 +0200 Subject: [PATCH] Convert templates to react --- package.json | 20 +- resources/js/App.tsx | 32 ++++ resources/js/app.tsx | 34 ++++ resources/js/bootstrap.ts | 21 +++ resources/js/components/Layout.tsx | 150 +++++++++++++++ resources/js/lib/api.ts | 288 +++++++++++++++++++++++++++++ resources/js/pages/Articles.tsx | 249 +++++++++++++++++++++++++ resources/js/pages/Dashboard.tsx | 191 +++++++++++++++++++ resources/js/pages/Feeds.tsx | 145 +++++++++++++++ resources/js/pages/Login.tsx | 129 +++++++++++++ resources/js/pages/Settings.tsx | 148 +++++++++++++++ resources/views/app.blade.php | 20 ++ routes/web.php | 8 + vite.config.js | 5 +- 14 files changed, 1434 insertions(+), 6 deletions(-) create mode 100644 resources/js/App.tsx create mode 100644 resources/js/app.tsx create mode 100644 resources/js/bootstrap.ts create mode 100644 resources/js/components/Layout.tsx create mode 100644 resources/js/lib/api.ts create mode 100644 resources/js/pages/Articles.tsx create mode 100644 resources/js/pages/Dashboard.tsx create mode 100644 resources/js/pages/Feeds.tsx create mode 100644 resources/js/pages/Login.tsx create mode 100644 resources/js/pages/Settings.tsx create mode 100644 resources/views/app.blade.php diff --git a/package.json b/package.json index 9843b30..883e7d9 100644 --- a/package.json +++ b/package.json @@ -6,10 +6,22 @@ "dev": "vite" }, "devDependencies": { - "laravel-vite-plugin": "^1.0", - "vite": "^6.0", "@tailwindcss/vite": "^4.0.0", + "laravel-vite-plugin": "^1.0", "tailwindcss": "^4.0.0", - "tailwindcss-animate": "^1.0.7" + "tailwindcss-animate": "^1.0.7", + "vite": "^6.0" + }, + "dependencies": { + "@tanstack/react-query": "^5.84.1", + "@types/react": "^19.1.9", + "@types/react-dom": "^19.1.7", + "@vitejs/plugin-react": "^4.7.0", + "axios": "^1.11.0", + "lucide-react": "^0.536.0", + "react": "^19.1.1", + "react-dom": "^19.1.1", + "react-router-dom": "^7.7.1", + "typescript": "^5.9.2" } -} \ No newline at end of file +} diff --git a/resources/js/App.tsx b/resources/js/App.tsx new file mode 100644 index 0000000..9949b1b --- /dev/null +++ b/resources/js/App.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { Routes, Route, Navigate } from 'react-router-dom'; +import { apiClient } from './lib/api'; +import Layout from './components/Layout'; +import Login from './pages/Login'; +import Dashboard from './pages/Dashboard'; +import Articles from './pages/Articles'; +import Feeds from './pages/Feeds'; +import Settings from './pages/Settings'; + +function App() { + const isAuthenticated = apiClient.isAuthenticated(); + + if (!isAuthenticated) { + return ; + } + + return ( + + + } /> + } /> + } /> + } /> + } /> + } /> + + + ); +} + +export default App; \ No newline at end of file diff --git a/resources/js/app.tsx b/resources/js/app.tsx new file mode 100644 index 0000000..6d10a91 --- /dev/null +++ b/resources/js/app.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { createRoot } from 'react-dom/client'; +import { BrowserRouter } from 'react-router-dom'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import App from './App'; +import './bootstrap'; + +// Create React Query client +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: 1, + refetchOnWindowFocus: false, + }, + }, +}); + +// Get the root element +const container = document.getElementById('app'); +if (!container) { + throw new Error('Root element not found'); +} + +const root = createRoot(container); + +root.render( + + + + + + + +); \ No newline at end of file diff --git a/resources/js/bootstrap.ts b/resources/js/bootstrap.ts new file mode 100644 index 0000000..d9785db --- /dev/null +++ b/resources/js/bootstrap.ts @@ -0,0 +1,21 @@ +/** + * Bootstrap file for setting up global configurations + */ + +// Set up axios defaults +import axios from 'axios'; + +axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; +axios.defaults.headers.common['Accept'] = 'application/json'; +axios.defaults.headers.common['Content-Type'] = 'application/json'; + +// Add CSRF token if available +const token = document.head.querySelector('meta[name="csrf-token"]'); +if (token) { + axios.defaults.headers.common['X-CSRF-TOKEN'] = (token as HTMLMetaElement).content; +} + +// Set base URL for API calls +axios.defaults.baseURL = '/api/v1'; + +export default axios; \ No newline at end of file diff --git a/resources/js/components/Layout.tsx b/resources/js/components/Layout.tsx new file mode 100644 index 0000000..1f64b13 --- /dev/null +++ b/resources/js/components/Layout.tsx @@ -0,0 +1,150 @@ +import React from 'react'; +import { Link, useLocation } from 'react-router-dom'; +import { + Home, + FileText, + Rss, + Settings as SettingsIcon, + LogOut, + Menu, + X +} from 'lucide-react'; +import { apiClient } from '../lib/api'; + +interface LayoutProps { + children: React.ReactNode; +} + +const Layout: React.FC = ({ children }) => { + const [sidebarOpen, setSidebarOpen] = React.useState(false); + const location = useLocation(); + const user = apiClient.getUser(); + + const navigation = [ + { name: 'Dashboard', href: '/dashboard', icon: Home }, + { name: 'Articles', href: '/articles', icon: FileText }, + { name: 'Feeds', href: '/feeds', icon: Rss }, + { name: 'Settings', href: '/settings', icon: SettingsIcon }, + ]; + + const handleLogout = async () => { + try { + await apiClient.logout(); + window.location.reload(); + } catch (error) { + console.error('Logout failed:', error); + // Force logout even if API call fails + apiClient.clearAuth(); + window.location.reload(); + } + }; + + return ( +
+ {/* Mobile sidebar overlay */} + {sidebarOpen && ( +
setSidebarOpen(false)} + /> + )} + + {/* Sidebar */} +
+
+
+ FFR + FFR +
+ +
+ + + + {/* User section */} +
+
+
+
+ + {user?.name?.charAt(0)?.toUpperCase()} + +
+
+
+

+ {user?.name} +

+

+ {user?.email} +

+
+ +
+
+
+ + {/* Main content */} +
+ {/* Top header for mobile */} +
+ +
+ FFR + FFR +
+
{/* Spacer for centering */} +
+ + {/* Page content */} +
+ {children} +
+
+
+ ); +}; + +export default Layout; \ No newline at end of file diff --git a/resources/js/lib/api.ts b/resources/js/lib/api.ts new file mode 100644 index 0000000..2831652 --- /dev/null +++ b/resources/js/lib/api.ts @@ -0,0 +1,288 @@ +import axios from 'axios'; + +// Types for API responses +export interface ApiResponse { + success: boolean; + data: T; + message: string; +} + +export interface ApiError { + success: false; + message: string; + errors?: Record; +} + +export interface PaginatedResponse { + data: T[]; + pagination: { + current_page: number; + last_page: number; + per_page: number; + total: number; + from: number | null; + to: number | null; + }; +} + +// User types +export interface User { + id: number; + name: string; + email: string; +} + +export interface LoginCredentials { + email: string; + password: string; +} + +export interface RegisterData { + name: string; + email: string; + password: string; + password_confirmation: string; +} + +export interface AuthResponse { + user: User; + token: string; + token_type: string; +} + +// Article types +export interface Article { + id: number; + feed_id: number; + url: string; + title: string; + description: string; + is_valid: boolean; + is_duplicate: boolean; + approval_status: 'pending' | 'approved' | 'rejected'; + approved_at: string | null; + approved_by: string | null; + fetched_at: string | null; + validated_at: string | null; + created_at: string; + updated_at: string; + feed?: Feed; + article_publication?: ArticlePublication; +} + +// Feed types +export interface Feed { + id: number; + name: string; + url: string; + type: 'website' | 'rss'; + is_active: boolean; + description: string | null; + created_at: string; + updated_at: string; + articles_count?: number; +} + +// Other types +export interface ArticlePublication { + id: number; + article_id: number; + status: string; + published_at: string | null; + created_at: string; + updated_at: string; +} + +export interface PlatformAccount { + id: number; + platform_instance_id: number; + account_id: string; + username: string; + display_name: string | null; + description: string | null; + is_active: boolean; + created_at: string; + updated_at: string; +} + +export interface PlatformChannel { + id: number; + platform_instance_id: number; + channel_id: string; + name: string; + display_name: string | null; + description: string | null; + is_active: boolean; + created_at: string; + updated_at: string; +} + +export interface Settings { + article_processing_enabled: boolean; + publishing_approvals_enabled: boolean; +} + +export interface DashboardStats { + article_stats: { + total_today: number; + total_week: number; + total_month: number; + approved_today: number; + approved_week: number; + approved_month: number; + approval_percentage_today: number; + approval_percentage_week: number; + approval_percentage_month: number; + }; + system_stats: { + total_feeds: number; + active_feeds: number; + total_platform_accounts: number; + active_platform_accounts: number; + total_platform_channels: number; + active_platform_channels: number; + total_routes: number; + active_routes: number; + }; + available_periods: Array<{ value: string; label: string }>; + current_period: string; +} + +// API Client class +class ApiClient { + private token: string | null = null; + + constructor() { + // Get token from localStorage if available + this.token = localStorage.getItem('auth_token'); + this.setupInterceptors(); + } + + private setupInterceptors() { + // Request interceptor to add auth token + axios.interceptors.request.use((config) => { + if (this.token) { + config.headers.Authorization = `Bearer ${this.token}`; + } + return config; + }); + + // Response interceptor to handle errors + axios.interceptors.response.use( + (response) => response, + (error) => { + if (error.response?.status === 401) { + this.clearAuth(); + window.location.href = '/login'; + } + return Promise.reject(error); + } + ); + } + + setAuth(token: string, user: User) { + this.token = token; + localStorage.setItem('auth_token', token); + localStorage.setItem('user', JSON.stringify(user)); + } + + clearAuth() { + this.token = null; + localStorage.removeItem('auth_token'); + localStorage.removeItem('user'); + } + + getUser(): User | null { + const userStr = localStorage.getItem('user'); + return userStr ? JSON.parse(userStr) : null; + } + + isAuthenticated(): boolean { + return !!this.token; + } + + // Auth endpoints + async login(credentials: LoginCredentials): Promise { + const response = await axios.post>('/auth/login', credentials); + return response.data.data; + } + + async register(data: RegisterData): Promise { + const response = await axios.post>('/auth/register', data); + return response.data.data; + } + + async logout(): Promise { + await axios.post('/auth/logout'); + this.clearAuth(); + } + + async me(): Promise { + const response = await axios.get>('/auth/me'); + return response.data.data.user; + } + + // Dashboard endpoints + async getDashboardStats(period = 'today'): Promise { + const response = await axios.get>('/dashboard/stats', { + params: { period } + }); + return response.data.data; + } + + // Articles endpoints + async getArticles(page = 1, perPage = 15): Promise<{ articles: Article[]; pagination: any; settings: any }> { + const response = await axios.get>('/articles', { + params: { page, per_page: perPage } + }); + return response.data.data; + } + + async approveArticle(articleId: number): Promise
{ + const response = await axios.post>(`/articles/${articleId}/approve`); + return response.data.data; + } + + async rejectArticle(articleId: number): Promise
{ + const response = await axios.post>(`/articles/${articleId}/reject`); + return response.data.data; + } + + // Feeds endpoints + async getFeeds(): Promise { + const response = await axios.get>('/feeds'); + return response.data.data; + } + + async createFeed(data: Partial): Promise { + const response = await axios.post>('/feeds', data); + return response.data.data; + } + + async updateFeed(id: number, data: Partial): Promise { + const response = await axios.put>(`/feeds/${id}`, data); + return response.data.data; + } + + async deleteFeed(id: number): Promise { + await axios.delete(`/feeds/${id}`); + } + + async toggleFeed(id: number): Promise { + const response = await axios.post>(`/feeds/${id}/toggle`); + return response.data.data; + } + + // Settings endpoints + async getSettings(): Promise { + const response = await axios.get>('/settings'); + return response.data.data; + } + + async updateSettings(data: Partial): Promise { + const response = await axios.put>('/settings', data); + return response.data.data; + } +} + +export const apiClient = new ApiClient(); \ No newline at end of file diff --git a/resources/js/pages/Articles.tsx b/resources/js/pages/Articles.tsx new file mode 100644 index 0000000..a31b763 --- /dev/null +++ b/resources/js/pages/Articles.tsx @@ -0,0 +1,249 @@ +import React, { useState } from 'react'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { CheckCircle, XCircle, ExternalLink, Calendar, Tag } from 'lucide-react'; +import { apiClient, Article } from '../lib/api'; + +const Articles: React.FC = () => { + const [page, setPage] = useState(1); + const queryClient = useQueryClient(); + + const { data, isLoading, error } = useQuery({ + queryKey: ['articles', page], + queryFn: () => apiClient.getArticles(page), + }); + + const approveMutation = useMutation({ + mutationFn: (articleId: number) => apiClient.approveArticle(articleId), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['articles'] }); + }, + }); + + const rejectMutation = useMutation({ + mutationFn: (articleId: number) => apiClient.rejectArticle(articleId), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['articles'] }); + }, + }); + + const handleApprove = (articleId: number) => { + approveMutation.mutate(articleId); + }; + + const handleReject = (articleId: number) => { + rejectMutation.mutate(articleId); + }; + + const getStatusBadge = (status: string) => { + switch (status) { + case 'approved': + return ( + + + Approved + + ); + case 'rejected': + return ( + + + Rejected + + ); + default: + return ( + + + Pending + + ); + } + }; + + if (isLoading) { + return ( +
+
+
+
+ {[...Array(5)].map((_, i) => ( +
+
+
+
+
+
+
+
+ ))} +
+
+
+ ); + } + + if (error) { + return ( +
+
+

Failed to load articles

+
+
+ ); + } + + const articles = data?.articles || []; + const pagination = data?.pagination; + const settings = data?.settings; + + return ( +
+
+

Articles

+

+ Manage and review articles from your feeds +

+ {settings?.publishing_approvals_enabled && ( +
+ + Approval system enabled +
+ )} +
+ +
+ {articles.map((article: Article) => ( +
+
+
+

+ {article.title || 'Untitled Article'} +

+

+ {article.description || 'No description available'} +

+
+ Feed: {article.feed?.name || 'Unknown'} + + {new Date(article.created_at).toLocaleDateString()} + {article.is_valid !== null && ( + <> + + + {article.is_valid ? 'Valid' : 'Invalid'} + + + )} + {article.is_duplicate && ( + <> + + Duplicate + + )} +
+
+
+ {getStatusBadge(article.approval_status)} + + + +
+
+ + {article.approval_status === 'pending' && settings?.publishing_approvals_enabled && ( +
+ + +
+ )} +
+ ))} + + {articles.length === 0 && ( +
+ +

No articles

+

+ No articles have been fetched yet. +

+
+ )} + + {/* Pagination */} + {pagination && pagination.last_page > 1 && ( +
+
+ + +
+
+
+

+ Showing{' '} + {pagination.from} to{' '} + {pagination.to} of{' '} + {pagination.total} results +

+
+
+ +
+
+
+ )} +
+
+ ); +}; + +export default Articles; \ No newline at end of file diff --git a/resources/js/pages/Dashboard.tsx b/resources/js/pages/Dashboard.tsx new file mode 100644 index 0000000..24a130e --- /dev/null +++ b/resources/js/pages/Dashboard.tsx @@ -0,0 +1,191 @@ +import React from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { FileText, Rss, Users, Route, TrendingUp, Clock, CheckCircle } from 'lucide-react'; +import { apiClient } from '../lib/api'; + +const Dashboard: React.FC = () => { + const { data: stats, isLoading, error } = useQuery({ + queryKey: ['dashboard-stats'], + queryFn: () => apiClient.getDashboardStats(), + }); + + if (isLoading) { + return ( +
+
+
+
+ {[...Array(4)].map((_, i) => ( +
+
+
+
+ ))} +
+
+
+ ); + } + + if (error) { + return ( +
+
+

Failed to load dashboard data

+
+
+ ); + } + + const articleStats = stats?.article_stats; + const systemStats = stats?.system_stats; + + return ( +
+
+

Dashboard

+

+ Overview of your feed management system +

+
+ + {/* Article Statistics */} +
+

Article Statistics

+
+
+
+
+ +
+
+

Articles Today

+

+ {articleStats?.total_today || 0} +

+
+
+
+ +
+
+
+ +
+
+

Articles This Week

+

+ {articleStats?.total_week || 0} +

+
+
+
+ +
+
+
+ +
+
+

Approved Today

+

+ {articleStats?.approved_today || 0} +

+
+
+
+ +
+
+
+ +
+
+

Approval Rate

+

+ {articleStats?.approval_percentage_today?.toFixed(1) || 0}% +

+
+
+
+
+
+ + {/* System Statistics */} +
+

System Overview

+
+
+
+
+ +
+
+

Active Feeds

+

+ {systemStats?.active_feeds || 0} + + /{systemStats?.total_feeds || 0} + +

+
+
+
+ +
+
+
+ +
+
+

Platform Accounts

+

+ {systemStats?.active_platform_accounts || 0} + + /{systemStats?.total_platform_accounts || 0} + +

+
+
+
+ +
+
+
+ +
+
+

Platform Channels

+

+ {systemStats?.active_platform_channels || 0} + + /{systemStats?.total_platform_channels || 0} + +

+
+
+
+ +
+
+
+ +
+
+

Active Routes

+

+ {systemStats?.active_routes || 0} + + /{systemStats?.total_routes || 0} + +

+
+
+
+
+
+
+ ); +}; + +export default Dashboard; \ No newline at end of file diff --git a/resources/js/pages/Feeds.tsx b/resources/js/pages/Feeds.tsx new file mode 100644 index 0000000..e366ae5 --- /dev/null +++ b/resources/js/pages/Feeds.tsx @@ -0,0 +1,145 @@ +import React from 'react'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { Rss, Globe, ToggleLeft, ToggleRight, ExternalLink } from 'lucide-react'; +import { apiClient, Feed } from '../lib/api'; + +const Feeds: React.FC = () => { + const queryClient = useQueryClient(); + + const { data: feeds, isLoading, error } = useQuery({ + queryKey: ['feeds'], + queryFn: () => apiClient.getFeeds(), + }); + + const toggleMutation = useMutation({ + mutationFn: (feedId: number) => apiClient.toggleFeed(feedId), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['feeds'] }); + }, + }); + + const handleToggle = (feedId: number) => { + toggleMutation.mutate(feedId); + }; + + const getTypeIcon = (type: string) => { + switch (type) { + case 'rss': + return ; + case 'website': + return ; + default: + return ; + } + }; + + if (isLoading) { + return ( +
+
+
+
+ {[...Array(6)].map((_, i) => ( +
+
+
+
+
+ ))} +
+
+
+ ); + } + + if (error) { + return ( +
+
+

Failed to load feeds

+
+
+ ); + } + + return ( +
+
+

Feeds

+

+ Manage your RSS feeds and website sources +

+
+ +
+ {feeds?.map((feed: Feed) => ( +
+
+
+ {getTypeIcon(feed.type)} +

+ {feed.name} +

+
+ +
+ +

+ {feed.description || 'No description provided'} +

+ +
+
+ + {feed.is_active ? 'Active' : 'Inactive'} + + + {feed.type.toUpperCase()} + +
+ + + +
+ +
+ Added {new Date(feed.created_at).toLocaleDateString()} +
+
+ ))} + + {feeds?.length === 0 && ( +
+ +

No feeds

+

+ Get started by adding your first feed. +

+
+ )} +
+
+ ); +}; + +export default Feeds; \ No newline at end of file diff --git a/resources/js/pages/Login.tsx b/resources/js/pages/Login.tsx new file mode 100644 index 0000000..5e24d15 --- /dev/null +++ b/resources/js/pages/Login.tsx @@ -0,0 +1,129 @@ +import React, { useState } from 'react'; +import { Eye, EyeOff, LogIn } from 'lucide-react'; +import { apiClient } from '../lib/api'; + +const Login: React.FC = () => { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [showPassword, setShowPassword] = useState(false); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + setError(''); + + try { + const authData = await apiClient.login({ email, password }); + apiClient.setAuth(authData.token, authData.user); + window.location.reload(); + } catch (err: any) { + setError(err.response?.data?.message || 'Login failed. Please try again.'); + } finally { + setLoading(false); + } + }; + + return ( +
+
+
+ FFR +
+

+ Sign in to FFR +

+

+ Feed Feed Reader - Article Management System +

+
+ +
+
+
+ {error && ( +
+

{error}

+
+ )} + +
+ +
+ setEmail(e.target.value)} + className="appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm" + placeholder="Enter your email" + /> +
+
+ +
+ +
+ setPassword(e.target.value)} + className="appearance-none block w-full px-3 py-2 pr-10 border border-gray-300 rounded-md placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm" + placeholder="Enter your password" + /> + +
+
+ +
+ +
+
+ +
+
+ Demo credentials: test@example.com / password123 +
+
+
+
+
+ ); +}; + +export default Login; \ No newline at end of file diff --git a/resources/js/pages/Settings.tsx b/resources/js/pages/Settings.tsx new file mode 100644 index 0000000..e82df99 --- /dev/null +++ b/resources/js/pages/Settings.tsx @@ -0,0 +1,148 @@ +import React from 'react'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { Settings as SettingsIcon, Save, ToggleLeft, ToggleRight } from 'lucide-react'; +import { apiClient } from '../lib/api'; + +const Settings: React.FC = () => { + const queryClient = useQueryClient(); + + const { data: settings, isLoading, error } = useQuery({ + queryKey: ['settings'], + queryFn: () => apiClient.getSettings(), + }); + + const updateMutation = useMutation({ + mutationFn: (data: any) => apiClient.updateSettings(data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['settings'] }); + }, + }); + + const handleToggle = (key: string, value: boolean) => { + updateMutation.mutate({ [key]: value }); + }; + + if (isLoading) { + return ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ); + } + + if (error) { + return ( +
+
+

Failed to load settings

+
+
+ ); + } + + return ( +
+
+

Settings

+

+ Configure your system preferences +

+
+ +
+ {/* Article Processing Settings */} +
+
+

+ + Article Processing +

+

+ Control how articles are processed and handled +

+
+
+
+
+

+ Article Processing Enabled +

+

+ Enable automatic fetching and processing of articles from feeds +

+
+ +
+ +
+
+

+ Publishing Approvals Required +

+

+ Require manual approval before articles are published to platforms +

+
+ +
+
+
+ + {/* Status indicator */} + {updateMutation.isPending && ( +
+
+
+

Updating settings...

+
+
+ )} + + {updateMutation.isError && ( +
+

Failed to update settings. Please try again.

+
+ )} + + {updateMutation.isSuccess && ( +
+

Settings updated successfully!

+
+ )} +
+
+ ); +}; + +export default Settings; \ No newline at end of file diff --git a/resources/views/app.blade.php b/resources/views/app.blade.php new file mode 100644 index 0000000..eeb71de --- /dev/null +++ b/resources/views/app.blade.php @@ -0,0 +1,20 @@ + + + + + + + + {{ config('app.name', 'FFR') }} + + + + + + + @vite(['resources/css/app.css', 'resources/js/app.tsx']) + + +
+ + \ No newline at end of file diff --git a/routes/web.php b/routes/web.php index db4a53d..b18ec28 100644 --- a/routes/web.php +++ b/routes/web.php @@ -6,6 +6,13 @@ use App\Http\Controllers\SettingsController; use Illuminate\Support\Facades\Route; +// React SPA - catch all routes and serve the React app +Route::get('/{path?}', function () { + return view('app'); +})->where('path', '.*')->name('spa'); + +// Legacy routes (can be removed once fully migrated to React) +/* // Onboarding routes Route::get('/', [OnboardingController::class, 'index'])->name('onboarding.index'); Route::get('/onboarding/platform', [OnboardingController::class, 'platform'])->name('onboarding.platform'); @@ -35,3 +42,4 @@ Route::put('/routing/{feed}/{channel}', [App\Http\Controllers\RoutingController::class, 'update'])->name('routing.update'); Route::delete('/routing/{feed}/{channel}', [App\Http\Controllers\RoutingController::class, 'destroy'])->name('routing.destroy'); Route::post('/routing/{feed}/{channel}/toggle', [App\Http\Controllers\RoutingController::class, 'toggle'])->name('routing.toggle'); +*/ diff --git a/vite.config.js b/vite.config.js index fb1c8f3..f1fd407 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,15 +1,16 @@ import { defineConfig } from 'vite'; import laravel from 'laravel-vite-plugin'; +import react from '@vitejs/plugin-react'; import tailwindcss from '@tailwindcss/vite'; export default defineConfig({ plugins: [ tailwindcss(), + react(), laravel({ input: [ 'resources/css/app.css', - 'resources/js/app.js', - 'resources/js/routing.js' + 'resources/js/app.tsx', ], refresh: true, }),