π Frontend Overview
React 18 + Vite + TypeScript + Tailwind CSS 4. Architecture is a modified FSD (Feature-Sliced Design) β practical, not dogmatic.
Tech Stackβ
- React 18 + Vite (no SSR)
- TypeScript (strict)
- Redux Toolkit with custom slice pattern (see Redux)
- Tailwind CSS 4 via
@import 'tailwindcss'+ semantic CSS variables - React Router (with route-based code splitting)
- Axios for HTTP, native
WebSocketfor WS, JWT in HttpOnly cookies + interceptor refresh - lucide-react for icons
- i18next with
ua(fallback) +enlocales
File Structureβ
packages/frontend/src/
βββ pages/ β page components, one folder per page
βββ components/ β feature-shared components (DeviceCard, TimePicker, AddDeviceModal, ...)
βββ shared/
β βββ ui/ β reusable UI primitives (see UI page)
β βββ Layout/ β Layout, Sidebar, Header, Breadcrumbs, BottomTabBar
β βββ tokenStore.ts β token persistence (localStorage)
β βββ toast.ts β re-export of sonner toast
β βββ useWsSubscription.ts β WebSocket subscription hook
β βββ useNavigationBlocker.ts
β βββ usePolling.ts
β βββ useViewMode.ts
β βββ formatRelativeTime.ts
βββ store/ β Redux slices, by domain
β βββ {domain}/
β β βββ {domain}.slice.ts β sync reducers only
β β βββ {domain}.actions.ts β async thunks + WS event actions
β β βββ {domain}.types.ts β state + payload/param types
β βββ helpers.ts β createAppAsyncThunk, withToast, withLoading, makeActionCreator, createSliceHook
β βββ hooks.ts β useAppDispatch, useAppSelector
β βββ store.ts
βββ transport/
β βββ http/ β API functions (axios)
β βββ ws/ β WebSocket client + middleware
βββ router/ β React Router setup
βββ providers/ β context providers
βββ i18n.ts β i18next config
βββ colors.css β semantic color tokens (light + dark)
βββ index.css β Tailwind import + base styles
Layer Boundariesβ
Hard rulesβ
- No transport in UI β pages and components never
importfrom@/transport/. All API calls go through thunks in*.actions.ts. - No barrel exports β always import from the exact file (
@/shared/ui/Button/Button). useTranslation()for all user-facing text β never hardcode strings.- No inline styles β Tailwind classes only.
- No hardcoded hex/rgb β use semantic tokens (
text-text-primary,bg-bg-surface) or named colors (text-honolulu-blue).
Path Aliasesβ
| Alias | Maps to |
|---|---|
@/store/... | src/store/... |
@/transport/... | src/transport/... |
@/shared/... | src/shared/... |
@/components/... | src/components/... |
Component Patternβ
type Props = {
device: Device;
onClick: () => void;
showTelemetry?: boolean;
};
export const DeviceCard = ({ device, onClick, showTelemetry = false }: Props) => {
const { t } = useTranslation();
return (
<Card
className="cursor-pointer hover:border-honolulu-blue/50 transition-colors"
onClick={onClick}
>
<div className="flex items-start justify-between gap-3">
<span className="font-medium text-text-primary">{device.name}</span>
<StatusBadge status={device.status} />
</div>
</Card>
);
};
type Props = ...(notinterface) above the component- Named export, arrow function
- Default values in destructuring (
showTelemetry = false)
Modal Patternβ
<Modal isOpen={isOpen} onClose={onClose} title={t('my.modal.title')}>
{/* content */}
<div className="mt-4 flex justify-end gap-2">
<Button variant="secondary" onClick={onClose}>{t('common.cancel')}</Button>
<Button onClick={handleSubmit}>{t('common.save')}</Button>
</div>
</Modal>
Auth Flowβ
- JWT stored in HttpOnly cookie (login response sets it)
apiClient(axios) auto-adds the cookie on every request- On
401, the interceptor calls/api/auth/refresh(also cookie-based) and retries the original request - On refresh failure β
dispatch(authActions.reset())+ redirect to login