Přeskočit na hlavní obsah

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

  1. Use TypeScript - Type safety is important
  2. Follow Design System - Use predefined classes
  3. Handle Loading States - Show loading indicators
  4. Handle Errors - Display error messages
  5. Document Code - Add JSDoc comments
  6. Test Responsively - Check mobile and desktop
  7. Optimize Performance - Use React.memo when needed
  8. Accessibility - Use semantic HTML
  9. File Upload - Always validate file size and type before upload
  10. Categories - Use GET /categories endpoint to display category names, not IDs