Arbat Beauty CRM
My main commercial project since May 2025. An internal CRM used daily by salon administrators for bookings, staff scheduling, inventory and payroll. I am the top contributor — I lead the migration of the legacy frontend to React 19 and TypeScript, design the architecture along Feature-Sliced Design, and own modules end to end.
- React 19
- TypeScript
- Vite
- TanStack Query
- Zustand
- React Hook Form
- React Router
- Sass
- Storybook
- Vitest
- Playwright
- Top contributor: 1486 commits over 15 months.
- Custom drag-and-drop built on mouse events instead of a ready-made library — extracted into a shared hook and reused by the schedule grid and the shared data table.
- Own entire modules: staff scheduling, visits and clients, inventory and tech maps, payroll and payouts.
- Feature-Sliced Design architecture across 10 domain entities.
- Testing pyramid: Vitest for unit and component tests, Playwright for e2e, 34 Storybook stories.
case study
How it was built
An internal product for the daily work of salon administrators: client bookings, staff schedules, consumable inventory and employee payroll.
Problem
- Legacy JavaScript frontend with duplicated business logic: a single change had to be made in several places.
- The schedule is the heaviest screen in the product: dozens of employees, a monthly grid, reordering rows and shifts without a reload.
- Off-the-shelf drag-and-drop libraries did not fit: the native HTML5 drag decides whether a drag starts before React can mark the row, and it drags an unstylable ghost with a "copy" badge.
- Modules had to be rewritten without pausing feature delivery for the business.
Solution
- Moved to React 19 + TypeScript and decomposed the code along Feature-Sliced Design: 10 domain entities instead of one shared pile of components.
- Split server state from client state: TanStack Query for server data, Zustand for UI state.
- Built a custom row-reordering hook on mouse events: a copy of the row follows the cursor while neighbours slide apart via CSS transforms written straight to the DOM, bypassing React re-renders.
- Replaced copy-paste with shared components: a data table, React Hook Form based forms and one set of controls used across every module.
- Test coverage on three levels so the migration would not break already working screens.
Contribution
1486 commits, top contributor
Duration
15 months of continuous work
Architecture
10 domain entities, Feature-Sliced Design
Reuse
One drag-and-drop hook powers 2 modules
// Transforms are written straight to the DOM: going through React would
// re-render every row on every crossed row, and a schedule row is an
// employee cell plus one cell per day of the month.
const applyShifts = (target: number) => {
const pitch = pitchRef.current;
const from = draggedRef.current;
if (from === null || !pitch) return;
rowElementsRef.current.forEach((elements, rowIndex) => {
let shift = 0;
if (rowIndex === from) shift = (target - from) * pitch;
else if (rowIndex > from && rowIndex <= target) shift = -pitch;
else if (rowIndex < from && rowIndex >= target) shift = pitch;
elements.forEach((element) => {
element.style.transform = shift ? `translateY(${shift}px)` : '';
});
});
};
10:30Анна Петроваnew
12:00Мария Ивановаconfirmed