Bỏ qua

Frontend Portal

React 18
Vite 5
MapLibre GL
TailwindCSS

Cấu Trúc Dự Án

src/
├── apiService.js       # Axios configuration & API calls
├── App.jsx             # Root component with routing
├── main.jsx            # Entry point
├── index.css           # Global styles (Tailwind)
├── components/         # Reusable UI components
│   ├── common/         # Buttons, Inputs, Modals
│   ├── dashboard/      # Dashboard widgets
│   ├── map/            # Map components
│   └── layout/         # Sidebar, Header
├── context/            # React Context providers
│   ├── AuthContext.jsx # Authentication state
│   └── MapContext.jsx  # Map state & filters
├── pages/              # Page components
│   ├── Dashboard.jsx
│   ├── MapView.jsx
│   ├── Reports.jsx
│   └── Login.jsx
└── utils/              # Helper functions
    ├── formatDate.js
    └── aqiColors.js

Component Architecture

graph TD
    App --> AuthProvider
    AuthProvider --> Router
    Router --> Layout
    Layout --> Sidebar
    Layout --> Header
    Layout --> MainContent
    MainContent --> DashboardPage
    MainContent --> MapPage
    MainContent --> ReportsPage

State Management

Sử dụng React Context API cho global state:

AuthContext

const AuthContext = createContext();

export function AuthProvider({ children }) {
  const [user, setUser] = useState(null);
  const [token, setToken] = useState(localStorage.getItem('token'));

  const login = async (email, password) => {
    const response = await api.login(email, password);
    setToken(response.access_token);
    localStorage.setItem('token', response.access_token);
  };

  const logout = () => {
    setToken(null);
    localStorage.removeItem('token');
  };

  return (
    <AuthContext.Provider value={{ user, token, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
}

MapContext

const MapContext = createContext();

export function MapProvider({ children }) {
  const [activeLayer, setActiveLayer] = useState('AQI');
  const [filters, setFilters] = useState({
    radius: 5, // km
    center: [105.8542, 21.0285] // Hanoi
  });

  return (
    <MapContext.Provider value={{ activeLayer, setActiveLayer, filters, setFilters }}>
      {children}
    </MapContext.Provider>
  );
}

MapLibre Integration

Basic Map Setup

import maplibregl from 'maplibre-gl';

function MapView() {
  const mapContainer = useRef(null);
  const map = useRef(null);

  useEffect(() => {
    if (map.current) return;

    map.current = new maplibregl.Map({
      container: mapContainer.current,
      style: `https://api.maptiler.com/maps/streets/style.json?key=${MAPTILER_KEY}`,
      center: [105.8542, 21.0285],
      zoom: 12,
      pitch: 45 // 3D view
    });

    map.current.addControl(new maplibregl.NavigationControl());
  }, []);

  return <div ref={mapContainer} className="w-full h-full" />;
}

Adding Markers

function addMarker(map, coordinates, color) {
  new maplibregl.Marker({ color })
    .setLngLat(coordinates)
    .setPopup(new maplibregl.Popup().setHTML('<h3>Station Info</h3>'))
    .addTo(map);
}

API Service

// apiService.js
import axios from 'axios';

const api = axios.create({
  baseURL: import.meta.env.VITE_API_URL,
  headers: {
    'Content-Type': 'application/json'
  }
});

// Request interceptor - Add token
api.interceptors.request.use((config) => {
  const token = localStorage.getItem('token');
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

export const reportService = {
  getAll: (params) => api.get('/reports/', { params }),
  create: (data) => api.post('/reports/', data),
  update: (id, data) => api.put(`/reports/${id}`, data)
};

Styling với TailwindCSS

Ví dụ Component

function Button({ children, variant = 'primary', ...props }) {
  const baseClasses = 'px-4 py-2 rounded-lg font-medium transition-colors';
  const variants = {
    primary: 'bg-teal-500 text-white hover:bg-teal-600',
    secondary: 'bg-gray-200 text-gray-800 hover:bg-gray-300',
    danger: 'bg-red-500 text-white hover:bg-red-600'
  };

  return (
    <button className={`${baseClasses} ${variants[variant]}`} {...props}>
      {children}
    </button>
  );
}

Build & Deploy

# Development
npm run dev

# Production build
npm run build

# Preview production build
npm run preview

Build output sẽ nằm trong thư mục dist/.