feat[agent]: set up TanStack Router and Loads page component for React frontend
This commit is contained in:
parent
f1c75faba1
commit
4f53ee4210
9 changed files with 303 additions and 0 deletions
22
frontend/package.json
Normal file
22
frontend/package.json
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"name": "freightdesk-frontend",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite --port 3000",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.3.0",
|
||||
"react-dom": "^18.3.0",
|
||||
"@tanstack/react-query": "^5.0.0",
|
||||
"@tanstack/react-router": "^1.0.0",
|
||||
"@supabase/supabase-js": "^2.45.0",
|
||||
"styled-components": "^6.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^5.0.0",
|
||||
"@vitejs/plugin-react": "^4.0.0"
|
||||
}
|
||||
}
|
||||
24
frontend/src/App.jsx
Normal file
24
frontend/src/App.jsx
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import React from 'react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { AppRouter } from './router';
|
||||
|
||||
// Create QueryClient instance
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
refetchOnWindowFocus: false,
|
||||
retry: 1,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<QueryClientProvider value={queryClient}>
|
||||
<AppRouter />
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
132
frontend/src/components/LoadsList.jsx
Normal file
132
frontend/src/components/LoadsList.jsx
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
import React, { useState } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { supabase } from '../supabaseClient';
|
||||
|
||||
// Utility functions (mirroring india.js logic for now)
|
||||
const formatINR = (n) => {
|
||||
if (n === null || n === undefined || isNaN(n)) return '—';
|
||||
return '₹' + parseFloat(n).toLocaleString('en-IN');
|
||||
};
|
||||
|
||||
const getStatusColor = (status) => {
|
||||
const colors = {
|
||||
'settled': 'success',
|
||||
'completed': 'success',
|
||||
'commission received': 'success',
|
||||
'reconciled': 'success',
|
||||
'loaded / in transit': 'primary',
|
||||
'assigned': 'primary',
|
||||
'assigned vehicle': 'primary',
|
||||
'pending collection': 'warning',
|
||||
'partially pending': 'warning',
|
||||
'fully pending from shipper': 'warning',
|
||||
'commission due': 'warning',
|
||||
'cancelled': 'danger',
|
||||
'partial': 'secondary',
|
||||
'available vehicle': 'secondary',
|
||||
};
|
||||
return colors[status] || 'secondary';
|
||||
};
|
||||
|
||||
function LoadsList() {
|
||||
const [filterStatus, setFilterStatus] = useState('');
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
|
||||
const { data: loads = [], isLoading, isError, refetch } = useQuery({
|
||||
queryKey: ['loads', filterStatus, searchTerm],
|
||||
queryFn: async () => {
|
||||
let query = supabase
|
||||
.from('loads')
|
||||
.select(`
|
||||
id,
|
||||
date,
|
||||
from_city,
|
||||
to_city,
|
||||
freight_charged,
|
||||
commission,
|
||||
status,
|
||||
shipper:shippers(name),
|
||||
vehicle:vehicles(number)
|
||||
`)
|
||||
.order('date', { ascending: false })
|
||||
.limit(100);
|
||||
|
||||
if (filterStatus) query = query.eq('status', filterStatus);
|
||||
if (searchTerm) {
|
||||
query = query.or(`from_city.ilike.%${searchTerm}%,to_city.ilike.%${searchTerm}%`);
|
||||
}
|
||||
|
||||
const { data, error } = await query;
|
||||
if (error) throw error;
|
||||
return data;
|
||||
},
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
});
|
||||
|
||||
if (isLoading) return <div className="text-center py-5">Loading loads...</div>;
|
||||
if (isError) return <div className="text-center py-5 text-danger">Error loading loads</div>;
|
||||
|
||||
return (
|
||||
<div className="container mt-4">
|
||||
<h2>Loads Management</h2>
|
||||
|
||||
{/* Filters */}
|
||||
<div className="row mb-3 g-2">
|
||||
<div className="col-md-4">
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder="Search cities..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-4">
|
||||
<select
|
||||
className="form-select"
|
||||
value={filterStatus}
|
||||
onChange={(e) => setFilterStatus(e.target.value)}
|
||||
>
|
||||
<option value="">All Statuses</option>
|
||||
<option value="settled">Settled</option>
|
||||
<option value="loaded / in transit">In Transit</option>
|
||||
<option value="pending collection">Pending Collection</option>
|
||||
<option value="cancelled">Cancelled</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Loads Table */}
|
||||
<table className="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Route</th>
|
||||
<th>Shipper</th>
|
||||
<th>Freight</th>
|
||||
<th>Commission</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{loads.map((load) => (
|
||||
<tr key={load.id}>
|
||||
<td>{new Date(load.date).toLocaleDateString('en-IN')}</td>
|
||||
<td>{load.from_city} → {load.to_city}</td>
|
||||
<td>{load.shipper?.name || '—'}</td>
|
||||
<td>{formatINR(load.freight_charged)}</td>
|
||||
<td>{formatINR(load.commission)}</td>
|
||||
<td>
|
||||
<span className={`badge bg-${getStatusColor(load.status)}`}>
|
||||
{load.status}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default LoadsList;
|
||||
28
frontend/src/index.css
Normal file
28
frontend/src/index.css
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
/* Basic styling for FreightDesk frontend */
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
#root {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Status badge colors override */
|
||||
.badge.bg-success {
|
||||
background-color: #28a745 !important;
|
||||
}
|
||||
.badge.bg-warning {
|
||||
background-color: #ffc107 !important;
|
||||
color: #212529 !important;
|
||||
}
|
||||
.badge.bg-danger {
|
||||
background-color: #dc3545 !important;
|
||||
}
|
||||
.badge.bg-primary {
|
||||
background-color: #0d6efd !important;
|
||||
}
|
||||
.badge.bg-secondary {
|
||||
background-color: #6c757d !important;
|
||||
}
|
||||
21
frontend/src/index.jsx
Normal file
21
frontend/src/index.jsx
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import React from 'react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
|
||||
// Create QueryClient instance
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
// We'll define query state overrides in individual components
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<QueryClientProvider value={queryClient}>
|
||||
<supabaseClient />
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
12
frontend/src/main.jsx
Normal file
12
frontend/src/main.jsx
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
import './index.css';
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
48
frontend/src/router.jsx
Normal file
48
frontend/src/router.jsx
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import React from 'react';
|
||||
import { createRootRoute, RouterProvider, createBrowserRouter } from '@tanstack/react-router';
|
||||
import LoadsList from './components/LoadsList';
|
||||
|
||||
// Root layout – can later include a navbar or sidebar
|
||||
function RootLayout({ children }) {
|
||||
return (
|
||||
<div>
|
||||
{/* Simple header */}
|
||||
<header className="bg-primary text-white p-3">
|
||||
<h1 className="mb-0" style={{ fontSize: '1.5rem' }}>FreightDesk Dashboard</h1>
|
||||
</header>
|
||||
<main>{children}</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Define the root route (layout)
|
||||
const rootRoute = createRootRoute({
|
||||
component: RootLayout,
|
||||
});
|
||||
|
||||
// Loads page route
|
||||
const loadsRoute = rootRoute.createRoute({
|
||||
path: '/loads',
|
||||
component: LoadsList,
|
||||
});
|
||||
|
||||
// Default route – redirect to /loads
|
||||
const indexRoute = rootRoute.createRoute({
|
||||
path: '/',
|
||||
component: () => {
|
||||
// Simple redirect component
|
||||
React.useEffect(() => {
|
||||
window.location.replace('/loads');
|
||||
}, []);
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
// Build the router
|
||||
const routeTree = rootRoute.addChildren([loadsRoute, indexRoute]);
|
||||
|
||||
export const router = createBrowserRouter({ routeTree });
|
||||
|
||||
export function AppRouter() {
|
||||
return <RouterProvider router={router} />;
|
||||
}
|
||||
6
frontend/src/supabaseClient.js
Normal file
6
frontend/src/supabaseClient.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { createClient } from '@supabase/supabase-js';
|
||||
|
||||
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
|
||||
const supabaseKey = import.meta.env.VITE_SUPABASE_ANON_KEY;
|
||||
|
||||
export const supabase = createClient(supabaseUrl, supabaseKey);
|
||||
10
frontend/vite.config.js
Normal file
10
frontend/vite.config.js
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
port: 3000,
|
||||
},
|
||||
});
|
||||
Loading…
Reference in a new issue