Frontend Development Guide
This guide explains how to develop frontend features in the D'n'A Cruises system.
Architecture
The frontend consists of multiple Next.js applications:
- Web App (
apps/web): Main management interface - Load App (
apps/load): Mobile-optimized loading interface - Docs (
apps/docs): Documentation site
Project Structure
apps/web/
├── src/
│ ├── app/ # Next.js App Router pages
│ ├── components/ # React components
│ ├── hooks/ # Custom React hooks
│ ├── lib/ # Utilities and API client
│ └── styles/ # Global styles
├── public/ # Static assets
└── package.json
Design System
The system uses a dual design approach:
Friendly Mode
- Large elements
- Spacious layout
- Clear typography
- Used for: Load sessions, Inventory, Wallboard, Settings
Dense Mode
- Compact tables
- Excel-like interface
- Maximum information density
- Used for: Items, Assets, Projects, Kits management
Homepage Design
The homepage uses a dashboard-style layout with:
- Welcome message with Czech declension of user's first name
- Dashboard cards for navigation (Projects, Items, Assets, Kits, Load Sessions, Files, Users, etc.)
- Statistics cards showing key metrics
- Recent activity from audit log
- Current projects and open load sessions overview
All navigation items are in dashboard cards, not in the top menu. The header only contains:
- Logo and app name
- Settings link
- User email and role badge
- Logout button
See Design Language for details.
Creating a New Page
1. Create Page File
// apps/web/src/app/my-page/page.tsx
'use client';
/**
* My Page
*
* Description of what this page does.
*
* Design: Friendly mode / Dense mode
* Accessible to: Loader, Producer, Admin
*/
import { useState, useEffect } from 'react';
import { useAuth } from '../../hooks/useAuth';
import { ProtectedRoute } from '../../components/ProtectedRoute';
import { api } from '../../lib/api';
export default function MyPage() {
return (
<ProtectedRoute>
<MyPageContent />
</ProtectedRoute>
);
}
function MyPageContent() {
const { user } = useAuth();
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
useEffect(() => {
loadData();
}, []);
const loadData = async () => {
try {
const response = await api.get('/my-endpoint');
if (response.success) {
setData(response.data);
} else {
setError(response.message || 'Failed to load data');
}
} catch (err) {
setError('An error occurred');
} finally {
setLoading(false);
}
};
if (loading) {
return <div>Loading...</div>;
}
return (
<div className="min-h-screen bg-gradient-primary py-8">
<div className="max-w-7xl mx-auto px-6">
<h1 className="text-h1 font-bold text-primary mb-8">My Page</h1>
{/* Your content */}
</div>
</div>
);
}
2. Design Mode Selection
Friendly Mode:
<div className="card-friendly">
<h2 className="text-h2 font-bold text-primary">Title</h2>
<button className="btn-primary">Action</button>
</div>
Dense Mode:
<div className="table-dense">
<table className="w-full">
<thead>
<tr>
<th className="text-table-header">Column</th>
</tr>
</thead>
<tbody>
<tr>
<td className="text-table-cell">Data</td>
</tr>
</tbody>
</table>
</div>
API Integration
Using the API Client
import { api } from '../../lib/api';
// GET request
const response = await api.get<{ items: Item[] }>('/items?page=1&pageSize=10');
if (response.success) {
const items = response.data.items;
}
// POST request
const response = await api.post('/items', {
name: 'New Item',
category_id: 1,
});
if (response.success) {
// Handle success
}
// PUT request
const response = await api.put(`/items/${id}`, {
name: 'Updated Item',
});
// DELETE request
const response = await api.delete(`/items/${id}`);
Error Handling
try {
const response = await api.get('/items');
if (response.success) {
// Handle success
} else {
setError(response.message || 'Failed to load');
}
} catch (err) {
setError('An error occurred');
}
Components
ProtectedRoute
Wraps pages that require authentication:
<ProtectedRoute>
<YourContent />
</ProtectedRoute>
QRScanner
For scanning QR codes:
<QRScanner
onScan={(scanId) => handleScan(scanId)}
disabled={false}
placeholder="Scan QR code..."
/>
Styling
Tailwind Classes
Use design system classes:
- Typography:
text-h1,text-h2,text-body-lg,text-table-cell - Colors:
text-primary,text-secondary,text-cyan-400 - Cards:
card-friendly,card-dense - Buttons:
btn-primary,btn-secondary,btn-dense - Inputs:
input-friendly,input-dense
Sharp Edges
Always use style={{ borderRadius: 0 }} for sharp edges:
<div className="card-friendly" style={{ borderRadius: 0 }}>
{/* Content */}
</div>
State Management
Local State
Use React hooks for local state:
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
Polling
For real-time updates:
useEffect(() => {
const interval = setInterval(() => {
loadData();
}, 2000); // Poll every 2 seconds
return () => clearInterval(interval);
}, []);
Forms
Friendly Mode Form
<div className="card-friendly">
<h2 className="text-h2 font-bold text-primary mb-6">Form Title</h2>
<form onSubmit={handleSubmit}>
<label className="text-body-lg text-primary mb-2">Label</label>
<input
type="text"
className="input-friendly"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
<button type="submit" className="btn-primary mt-6">
Submit
</button>
</form>
</div>
Dense Mode Form
<div className="bg-slate-800 border border-slate-400/20 p-4" style={{ borderRadius: 0 }}>
<label className="text-table-cell mb-2">Label</label>
<input
type="text"
className="input-dense"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
<button type="submit" className="btn-dense mt-4">
Submit
</button>
</div>
File Upload
FileUploadSection Component
For uploading files to projects or other entities:
import { FileUploadSection } from './FileUploadSection';
<FileUploadSection
projectId={projectId}
fileType="FILE" // or "PHOTO"
entityType="project"
entityId={projectId}
/>
Manual File Upload
const handleFileSelect = async (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files;
if (!files || files.length === 0) return;
for (const file of Array.from(files)) {
const formData = new FormData();
formData.append('file', file);
formData.append('file_type', 'FILE'); // or 'PHOTO', 'OTHER'
formData.append('entity_type', 'project');
formData.append('entity_id', projectId.toString());
const response = await api.post('/files/upload', formData);
if (response.success) {
// File uploaded successfully
}
}
};
<input
type="file"
multiple
onChange={handleFileSelect}
accept="image/*" // for photos, or omit for all types
/>
File Management
// List files with filtering
const response = await api.get(
`/files?entity_type=project&entity_id=${projectId}&page=1&pageSize=100`
);
// Download file
const downloadUrl = `${API_URL}/files/${fileId}/download`;
// Share file (generate public token)
const shareResponse = await api.put(`/files/${fileId}/public`);
const publicUrl = `${API_URL}/files/public/${shareResponse.data.public_token}`;
// Delete file
await api.delete(`/files/${fileId}`);
File Organization:
- Project files:
projects/{project_id}/files/ - Project photos:
projects/{project_id}/photos/ - Other files:
{file_type}/
Best Practices
- Use TypeScript - Type safety is important
- Follow Design System - Use predefined classes
- Handle Loading States - Show loading indicators
- Handle Errors - Display error messages
- Document Code - Add JSDoc comments
- Test Responsively - Check mobile and desktop
- Optimize Performance - Use React.memo when needed
- Accessibility - Use semantic HTML
- File Upload - Always validate file size and type before upload
- Categories - Use GET /categories endpoint to display category names, not IDs
Related Topics
- Design Language - Design system details
- API Development - Backend integration
- Setup - Development setup