Files
house-plan-maker/apps/client/src/api/client.ts
T
alexei.dolgolyov af8b9fe00f feat: complete house plan maker application
Full-featured house/apartment floor plan editor with:

- Turborepo monorepo (React/Vite client, Fastify/Prisma server, shared Zod schemas)
- 2D room editor with walls, doors, windows, furniture, electrical elements
- 3D room preview with Three.js (auto-hide nearest walls, bird's eye default)
- Wall projection views with interactive drag (elevation, position)
- Apartment floor plan view with room positioning
- Copy/paste, alignment tools, measurement tool, annotations
- Item-attached annotations with leader lines (visible on projections)
- Door open direction (LEFT/RIGHT/INWARD/OUTWARD) with swing arc
- Floor type textures (wood, tile, concrete, laminate, herringbone)
- Wall color picker for 3D view
- Furniture: bed, desk, wardrobe, sofa, table, chair, shelf, nightstand, dresser, bookcase, TV (with stand toggle), AC unit
- Furniture elevation support (wall-mounted items)
- Auto-save with dirty state tracking, batch save API
- Rotation-aware collision detection (SAT/OBB) with 3D elevation check
- Rotation-aware hit testing
- i18n (English/Russian) with locale-aware number formatting
- Dark mode with system preference detection
- Undo/redo, keyboard shortcuts, scale bar
- PDF/PNG/JSON export and JSON import
- Focus trap modal, toast notifications, tooltips
- Responsive layout with overlay palettes
2026-04-05 22:34:03 +03:00

341 lines
7.6 KiB
TypeScript

import type {
Apartment,
ApartmentWithRooms,
Room,
RoomFull,
Wall,
WallOpening,
ElectricalItem,
FurnitureItem,
CreateApartmentDto,
UpdateApartmentDto,
CreateRoomDto,
UpdateRoomDto,
CreateWallDto,
CreateWallOpeningDto,
UpdateWallOpeningDto,
CreateElectricalItemDto,
UpdateElectricalItemDto,
CreateFurnitureItemDto,
UpdateFurnitureItemDto,
BatchSyncOpeningsDto,
BatchSyncElectricalDto,
BatchSyncFurnitureDto,
ApiResponse,
ApiListResponse,
ApiErrorResponse,
} from '@house-plan-maker/shared';
const BASE_URL = '/api';
class ApiError extends Error {
readonly statusCode: number;
constructor(message: string, statusCode: number) {
super(message);
this.name = 'ApiError';
this.statusCode = statusCode;
}
}
async function request<T>(
path: string,
options: RequestInit = {},
): Promise<T> {
const url = `${BASE_URL}${path}`;
const headers: Record<string, string> = {
...((options.headers as Record<string, string>) ?? {}),
};
// Only set Content-Type for requests with a body
if (options.body) {
headers['Content-Type'] = 'application/json';
}
const response = await fetch(url, { ...options, headers });
if (!response.ok) {
let errorMessage = `Request failed with status ${response.status}`;
try {
const errorBody = (await response.json()) as ApiErrorResponse;
errorMessage = errorBody.error ?? errorMessage;
} catch {
// If parsing fails, use the default message
}
throw new ApiError(errorMessage, response.status);
}
// Handle 204 No Content (e.g. DELETE responses)
if (response.status === 204) {
return undefined as T;
}
return response.json() as Promise<T>;
}
// ── Apartments ──
export async function getApartments(): Promise<readonly Apartment[]> {
const result = await request<ApiListResponse<Apartment>>('/apartments');
return result.data;
}
export async function getApartment(id: string): Promise<ApartmentWithRooms> {
const result = await request<ApiResponse<ApartmentWithRooms>>(
`/apartments/${id}`,
);
return result.data;
}
export async function createApartment(
data: CreateApartmentDto,
): Promise<Apartment> {
const result = await request<ApiResponse<Apartment>>('/apartments', {
method: 'POST',
body: JSON.stringify(data),
});
return result.data;
}
export async function updateApartment(
id: string,
data: UpdateApartmentDto,
): Promise<Apartment> {
const result = await request<ApiResponse<Apartment>>(`/apartments/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
});
return result.data;
}
export async function deleteApartment(id: string): Promise<void> {
await request<void>(`/apartments/${id}`, { method: 'DELETE' });
}
// ── Rooms ──
export async function getRooms(apartmentId: string): Promise<readonly Room[]> {
const result = await request<ApiListResponse<Room>>(
`/apartments/${apartmentId}/rooms`,
);
return result.data;
}
export async function getRoomFull(roomId: string): Promise<RoomFull> {
const result = await request<ApiResponse<RoomFull>>(
`/rooms/${roomId}/full`,
);
return result.data;
}
export async function createRoom(
apartmentId: string,
data: CreateRoomDto,
): Promise<Room> {
const result = await request<ApiResponse<Room>>(
`/apartments/${apartmentId}/rooms`,
{
method: 'POST',
body: JSON.stringify(data),
},
);
return result.data;
}
export async function updateRoom(
id: string,
data: UpdateRoomDto,
): Promise<Room> {
const result = await request<ApiResponse<Room>>(`/rooms/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
});
return result.data;
}
export async function deleteRoom(id: string): Promise<void> {
await request<void>(`/rooms/${id}`, { method: 'DELETE' });
}
// ── Walls ──
export async function bulkUpdateWalls(
roomId: string,
walls: readonly CreateWallDto[],
): Promise<readonly Wall[]> {
const result = await request<ApiResponse<readonly Wall[]>>(
`/rooms/${roomId}/walls`,
{
method: 'PUT',
body: JSON.stringify({ walls }),
},
);
return result.data;
}
// ── Wall Openings ──
export async function createWallOpening(
roomId: string,
data: CreateWallOpeningDto,
): Promise<WallOpening> {
const result = await request<ApiResponse<WallOpening>>(
`/rooms/${roomId}/openings`,
{
method: 'POST',
body: JSON.stringify(data),
},
);
return result.data;
}
export async function updateWallOpening(
roomId: string,
openingId: string,
data: UpdateWallOpeningDto,
): Promise<WallOpening> {
const result = await request<ApiResponse<WallOpening>>(
`/rooms/${roomId}/openings/${openingId}`,
{
method: 'PUT',
body: JSON.stringify(data),
},
);
return result.data;
}
export async function deleteWallOpening(
roomId: string,
openingId: string,
): Promise<void> {
await request<void>(`/rooms/${roomId}/openings/${openingId}`, {
method: 'DELETE',
});
}
// ── Electrical Items ──
export async function createElectricalItem(
roomId: string,
data: CreateElectricalItemDto,
): Promise<ElectricalItem> {
const result = await request<ApiResponse<ElectricalItem>>(
`/rooms/${roomId}/electrical`,
{
method: 'POST',
body: JSON.stringify(data),
},
);
return result.data;
}
export async function updateElectricalItem(
roomId: string,
itemId: string,
data: UpdateElectricalItemDto,
): Promise<ElectricalItem> {
const result = await request<ApiResponse<ElectricalItem>>(
`/electrical/${itemId}`,
{
method: 'PUT',
body: JSON.stringify(data),
},
);
return result.data;
}
export async function deleteElectricalItem(
roomId: string,
itemId: string,
): Promise<void> {
await request<void>(`/electrical/${itemId}`, {
method: 'DELETE',
});
}
// ── Furniture Items ──
export async function createFurnitureItem(
roomId: string,
data: CreateFurnitureItemDto,
): Promise<FurnitureItem> {
const result = await request<ApiResponse<FurnitureItem>>(
`/rooms/${roomId}/furniture`,
{
method: 'POST',
body: JSON.stringify(data),
},
);
return result.data;
}
export async function updateFurnitureItem(
roomId: string,
itemId: string,
data: UpdateFurnitureItemDto,
): Promise<FurnitureItem> {
const result = await request<ApiResponse<FurnitureItem>>(
`/furniture/${itemId}`,
{
method: 'PUT',
body: JSON.stringify(data),
},
);
return result.data;
}
export async function deleteFurnitureItem(
roomId: string,
itemId: string,
): Promise<void> {
await request<void>(`/furniture/${itemId}`, {
method: 'DELETE',
});
}
// ── Batch Sync ──
export async function batchSyncOpenings(
roomId: string,
data: BatchSyncOpeningsDto,
): Promise<readonly WallOpening[]> {
const result = await request<ApiListResponse<WallOpening>>(
`/rooms/${roomId}/openings/batch`,
{
method: 'PUT',
body: JSON.stringify(data),
},
);
return result.data;
}
export async function batchSyncElectrical(
roomId: string,
data: BatchSyncElectricalDto,
): Promise<readonly ElectricalItem[]> {
const result = await request<ApiListResponse<ElectricalItem>>(
`/rooms/${roomId}/electrical/batch`,
{
method: 'PUT',
body: JSON.stringify(data),
},
);
return result.data;
}
export async function batchSyncFurniture(
roomId: string,
data: BatchSyncFurnitureDto,
): Promise<readonly FurnitureItem[]> {
const result = await request<ApiListResponse<FurnitureItem>>(
`/rooms/${roomId}/furniture/batch`,
{
method: 'PUT',
body: JSON.stringify(data),
},
);
return result.data;
}
export { ApiError };