Skip to main content

🌐 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 WebSocket for WS, JWT in HttpOnly cookies + interceptor refresh
  • lucide-react for icons
  • i18next with ua (fallback) + en locales

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 import from @/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​

AliasMaps 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 = ... (not interface) above the component
  • Named export, arrow function
  • Default values in destructuring (showTelemetry = false)
<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​

  1. JWT stored in HttpOnly cookie (login response sets it)
  2. apiClient (axios) auto-adds the cookie on every request
  3. On 401, the interceptor calls /api/auth/refresh (also cookie-based) and retries the original request
  4. On refresh failure β†’ dispatch(authActions.reset()) + redirect to login

Reference​