feat: initial commit web-cad-neu with docker-compose, frontend and backend
This commit is contained in:
@@ -0,0 +1,420 @@
|
||||
/**
|
||||
* API Service – Backend communication for projects, drawings, elements, layers, blocks
|
||||
*/
|
||||
import type { CADElement, CADLayer, BlockDefinition } from '../types/cad.types';
|
||||
|
||||
const API_BASE = import.meta.env.VITE_API_BASE || 'http://localhost:3001';
|
||||
|
||||
function authHeaders(token: string): Record<string, string> {
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Types ──────────────────────────────────────────────
|
||||
export interface Project {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
owner_id: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface Drawing {
|
||||
id: string;
|
||||
project_id: string;
|
||||
name: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
// ─── Auth ───────────────────────────────────────────────
|
||||
export async function login(email: string, password: string): Promise<{ user: any; session: { token: string } }> {
|
||||
const res = await fetch(`${API_BASE}/api/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, password }),
|
||||
});
|
||||
if (!res.ok) throw new Error('Login failed');
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function register(email: string, password: string, name: string): Promise<{ user: any; session: { token: string } }> {
|
||||
const res = await fetch(`${API_BASE}/api/auth/register`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, password, name }),
|
||||
});
|
||||
if (!res.ok) throw new Error('Registration failed');
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function getMe(token: string): Promise<any> {
|
||||
const res = await fetch(`${API_BASE}/api/auth/me`, {
|
||||
headers: authHeaders(token),
|
||||
});
|
||||
if (!res.ok) throw new Error('Not authenticated');
|
||||
return res.json();
|
||||
}
|
||||
|
||||
// ─── Projects ───────────────────────────────────────────
|
||||
export async function getProjects(token: string): Promise<Project[]> {
|
||||
const res = await fetch(`${API_BASE}/api/projects`, { headers: authHeaders(token) });
|
||||
if (!res.ok) throw new Error('Failed to load projects');
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function createProject(token: string, name: string, description?: string): Promise<Project> {
|
||||
const res = await fetch(`${API_BASE}/api/projects`, {
|
||||
method: 'POST',
|
||||
headers: authHeaders(token),
|
||||
body: JSON.stringify({ name, description: description || null }),
|
||||
});
|
||||
if (!res.ok) throw new Error('Failed to create project');
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function deleteProject(token: string, id: string): Promise<void> {
|
||||
await fetch(`${API_BASE}/api/projects/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: authHeaders(token),
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Drawings ───────────────────────────────────────────
|
||||
export async function getDrawings(token: string, projectId: string): Promise<Drawing[]> {
|
||||
const res = await fetch(`${API_BASE}/api/projects/${projectId}/drawings`, { headers: authHeaders(token) });
|
||||
if (!res.ok) throw new Error('Failed to load drawings');
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function createDrawing(token: string, projectId: string, name: string): Promise<Drawing> {
|
||||
const res = await fetch(`${API_BASE}/api/projects/${projectId}/drawings`, {
|
||||
method: 'POST',
|
||||
headers: authHeaders(token),
|
||||
body: JSON.stringify({ name }),
|
||||
});
|
||||
if (!res.ok) throw new Error('Failed to create drawing');
|
||||
return res.json();
|
||||
}
|
||||
|
||||
// ─── Elements ───────────────────────────────────────────
|
||||
export async function getElements(token: string, drawingId: string): Promise<CADElement[]> {
|
||||
const res = await fetch(`${API_BASE}/api/drawings/${drawingId}/elements`, { headers: authHeaders(token) });
|
||||
if (!res.ok) throw new Error('Failed to load elements');
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function createElement(token: string, drawingId: string, el: CADElement): Promise<CADElement> {
|
||||
const res = await fetch(`${API_BASE}/api/drawings/${drawingId}/elements`, {
|
||||
method: 'POST',
|
||||
headers: authHeaders(token),
|
||||
body: JSON.stringify(el),
|
||||
});
|
||||
if (!res.ok) throw new Error('Failed to create element');
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function updateElement(token: string, elementId: string, patch: Partial<CADElement>): Promise<CADElement> {
|
||||
const res = await fetch(`${API_BASE}/api/elements/${elementId}`, {
|
||||
method: 'PATCH',
|
||||
headers: authHeaders(token),
|
||||
body: JSON.stringify(patch),
|
||||
});
|
||||
if (!res.ok) throw new Error('Failed to update element');
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function deleteElement(token: string, elementId: string): Promise<void> {
|
||||
await fetch(`${API_BASE}/api/elements/${elementId}`, {
|
||||
method: 'DELETE',
|
||||
headers: authHeaders(token),
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Layers ─────────────────────────────────────────────
|
||||
export async function getLayers(token: string, drawingId: string): Promise<CADLayer[]> {
|
||||
const res = await fetch(`${API_BASE}/api/drawings/${drawingId}/layers`, { headers: authHeaders(token) });
|
||||
if (!res.ok) throw new Error('Failed to load layers');
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function createLayer(token: string, drawingId: string, layer: CADLayer): Promise<CADLayer> {
|
||||
const res = await fetch(`${API_BASE}/api/drawings/${drawingId}/layers`, {
|
||||
method: 'POST',
|
||||
headers: authHeaders(token),
|
||||
body: JSON.stringify(layer),
|
||||
});
|
||||
if (!res.ok) throw new Error('Failed to create layer');
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function updateLayer(token: string, layerId: string, patch: Partial<CADLayer>): Promise<CADLayer> {
|
||||
const res = await fetch(`${API_BASE}/api/layers/${layerId}`, {
|
||||
method: 'PATCH',
|
||||
headers: authHeaders(token),
|
||||
body: JSON.stringify(patch),
|
||||
});
|
||||
if (!res.ok) throw new Error('Failed to update layer');
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function deleteLayer(token: string, layerId: string): Promise<void> {
|
||||
await fetch(`${API_BASE}/api/layers/${layerId}`, {
|
||||
method: 'DELETE',
|
||||
headers: authHeaders(token),
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Blocks ─────────────────────────────────────────────
|
||||
export async function getBlocks(token: string, drawingId: string): Promise<BlockDefinition[]> {
|
||||
const res = await fetch(`${API_BASE}/api/drawings/${drawingId}/blocks`, { headers: authHeaders(token) });
|
||||
if (!res.ok) throw new Error('Failed to load blocks');
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function createBlock(token: string, drawingId: string, block: BlockDefinition): Promise<BlockDefinition> {
|
||||
const res = await fetch(`${API_BASE}/api/drawings/${drawingId}/blocks`, {
|
||||
method: 'POST',
|
||||
headers: authHeaders(token),
|
||||
body: JSON.stringify(block),
|
||||
});
|
||||
if (!res.ok) throw new Error('Failed to create block');
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function updateBlock(token: string, blockId: string, patch: Partial<BlockDefinition>): Promise<BlockDefinition> {
|
||||
const res = await fetch(`${API_BASE}/api/blocks/${blockId}`, {
|
||||
method: 'PATCH',
|
||||
headers: authHeaders(token),
|
||||
body: JSON.stringify(patch),
|
||||
});
|
||||
if (!res.ok) throw new Error('Failed to update block');
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function deleteBlock(token: string, blockId: string): Promise<void> {
|
||||
await fetch(`${API_BASE}/api/blocks/${blockId}`, {
|
||||
method: 'DELETE',
|
||||
headers: authHeaders(token),
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Composite: Load full project data ───────────────────
|
||||
export interface ProjectData {
|
||||
project: Project;
|
||||
drawing: Drawing | null;
|
||||
elements: CADElement[];
|
||||
layers: CADLayer[];
|
||||
blocks: BlockDefinition[];
|
||||
}
|
||||
|
||||
export async function loadProjectData(token: string, projectId: string): Promise<ProjectData> {
|
||||
const drawings = await getDrawings(token, projectId);
|
||||
const project = (await getProjects(token)).find(p => p.id === projectId);
|
||||
|
||||
if (!project) throw new Error('Project not found');
|
||||
|
||||
// Use first drawing or create one
|
||||
let drawing = drawings[0] || null;
|
||||
if (!drawing) {
|
||||
drawing = await createDrawing(token, projectId, 'Hauptzeichnung');
|
||||
}
|
||||
|
||||
const [elements, layers, blocks] = await Promise.all([
|
||||
getElements(token, drawing.id),
|
||||
getLayers(token, drawing.id),
|
||||
getBlocks(token, drawing.id),
|
||||
]);
|
||||
|
||||
return { project, drawing, elements, layers, blocks };
|
||||
}
|
||||
|
||||
// ─── Format Conversion: DB (snake_case) ↔ Frontend (camelCase) ─────
|
||||
|
||||
function dbElementToFrontend(dbEl: any): CADElement {
|
||||
return {
|
||||
id: dbEl.id,
|
||||
type: dbEl.type,
|
||||
layerId: dbEl.layer_id,
|
||||
x: dbEl.x,
|
||||
y: dbEl.y,
|
||||
width: dbEl.width,
|
||||
height: dbEl.height,
|
||||
properties: typeof dbEl.properties_json === 'string'
|
||||
? JSON.parse(dbEl.properties_json)
|
||||
: (dbEl.properties_json || {}),
|
||||
};
|
||||
}
|
||||
|
||||
function frontendElementToDb(el: CADElement, drawingId: string): any {
|
||||
return {
|
||||
id: el.id,
|
||||
drawing_id: drawingId,
|
||||
layer_id: el.layerId,
|
||||
type: el.type,
|
||||
x: el.x,
|
||||
y: el.y,
|
||||
width: el.width,
|
||||
height: el.height,
|
||||
properties_json: JSON.stringify(el.properties),
|
||||
};
|
||||
}
|
||||
|
||||
function dbLayerToFrontend(dbLayer: any): CADLayer {
|
||||
return {
|
||||
id: dbLayer.id,
|
||||
name: dbLayer.name,
|
||||
visible: !!dbLayer.visible,
|
||||
locked: !!dbLayer.locked,
|
||||
color: dbLayer.color,
|
||||
lineType: dbLayer.line_type as 'solid' | 'dashed' | 'dotted',
|
||||
transparency: dbLayer.transparency,
|
||||
sortOrder: dbLayer.sort_order,
|
||||
parentId: dbLayer.parent_id,
|
||||
};
|
||||
}
|
||||
|
||||
function frontendLayerToDb(layer: CADLayer, drawingId: string): any {
|
||||
return {
|
||||
id: layer.id,
|
||||
drawing_id: drawingId,
|
||||
name: layer.name,
|
||||
visible: layer.visible ? 1 : 0,
|
||||
locked: layer.locked ? 1 : 0,
|
||||
color: layer.color,
|
||||
line_type: layer.lineType,
|
||||
transparency: layer.transparency,
|
||||
sort_order: layer.sortOrder,
|
||||
parent_id: layer.parentId,
|
||||
};
|
||||
}
|
||||
|
||||
function dbBlockToFrontend(dbBlock: any): BlockDefinition {
|
||||
return {
|
||||
id: dbBlock.id,
|
||||
name: dbBlock.name,
|
||||
description: dbBlock.description || '',
|
||||
category: dbBlock.category,
|
||||
elements: typeof dbBlock.elements_json === 'string'
|
||||
? JSON.parse(dbBlock.elements_json)
|
||||
: (dbBlock.elements_json || []),
|
||||
thumbnail: dbBlock.thumbnail || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function frontendBlockToDb(block: BlockDefinition, drawingId: string): any {
|
||||
return {
|
||||
id: block.id,
|
||||
drawing_id: drawingId,
|
||||
name: block.name,
|
||||
description: block.description,
|
||||
category: block.category,
|
||||
elements_json: JSON.stringify(block.elements),
|
||||
thumbnail: block.thumbnail || null,
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Typed API calls with conversion ─────────────────────
|
||||
|
||||
export async function getElementsTyped(token: string, drawingId: string): Promise<CADElement[]> {
|
||||
const raw = await getElements(token, drawingId);
|
||||
return raw.map(dbElementToFrontend);
|
||||
}
|
||||
|
||||
export async function createElementTyped(token: string, drawingId: string, el: CADElement): Promise<CADElement> {
|
||||
const raw = await createElement(token, drawingId, frontendElementToDb(el, drawingId));
|
||||
return dbElementToFrontend(raw);
|
||||
}
|
||||
|
||||
export async function getLayersTyped(token: string, drawingId: string): Promise<CADLayer[]> {
|
||||
const raw = await getLayers(token, drawingId);
|
||||
return raw.map(dbLayerToFrontend);
|
||||
}
|
||||
|
||||
export async function createLayerTyped(token: string, drawingId: string, layer: CADLayer): Promise<CADLayer> {
|
||||
const raw = await createLayer(token, drawingId, frontendLayerToDb(layer, drawingId));
|
||||
return dbLayerToFrontend(raw);
|
||||
}
|
||||
|
||||
export async function getBlocksTyped(token: string, drawingId: string): Promise<BlockDefinition[]> {
|
||||
const raw = await getBlocks(token, drawingId);
|
||||
return raw.map(dbBlockToFrontend);
|
||||
}
|
||||
|
||||
export async function createBlockTyped(token: string, drawingId: string, block: BlockDefinition): Promise<BlockDefinition> {
|
||||
const raw = await createBlock(token, drawingId, frontendBlockToDb(block, drawingId));
|
||||
return dbBlockToFrontend(raw);
|
||||
}
|
||||
|
||||
const projectLoadCache = new Map<string, Promise<ProjectData>>();
|
||||
|
||||
export async function loadProjectDataTyped(token: string, projectId: string): Promise<ProjectData> {
|
||||
// Dedup concurrent calls (React StrictMode double-render)
|
||||
const existing = projectLoadCache.get(projectId);
|
||||
if (existing) return existing;
|
||||
|
||||
const promise = (async () => {
|
||||
const drawings = await getDrawings(token, projectId);
|
||||
const projects = await getProjects(token);
|
||||
const project = projects.find(p => p.id === projectId);
|
||||
if (!project) throw new Error('Project not found');
|
||||
|
||||
let drawing = drawings[0] || null;
|
||||
if (!drawing) {
|
||||
drawing = await createDrawing(token, projectId, 'Hauptzeichnung');
|
||||
}
|
||||
|
||||
const [elementsRaw, layersRaw, blocksRaw] = await Promise.all([
|
||||
getElements(token, drawing.id),
|
||||
getLayers(token, drawing.id),
|
||||
getBlocks(token, drawing.id),
|
||||
]);
|
||||
|
||||
return {
|
||||
project,
|
||||
drawing,
|
||||
elements: elementsRaw.map(dbElementToFrontend),
|
||||
layers: layersRaw.map(dbLayerToFrontend),
|
||||
blocks: blocksRaw.map(dbBlockToFrontend),
|
||||
};
|
||||
})();
|
||||
|
||||
projectLoadCache.set(projectId, promise);
|
||||
promise.finally(() => projectLoadCache.delete(projectId));
|
||||
return promise;
|
||||
}
|
||||
|
||||
export { API_BASE };
|
||||
|
||||
// ─── AI Copilot ─────────────────────────────────────────
|
||||
export interface AIChatMessage {
|
||||
role: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface AIChatContext {
|
||||
projectName?: string;
|
||||
elementCount?: number;
|
||||
layerCount?: number;
|
||||
elementTypeSummary?: Record<string, number>;
|
||||
}
|
||||
|
||||
export async function aiChat(
|
||||
token: string,
|
||||
messages: AIChatMessage[],
|
||||
context?: AIChatContext
|
||||
): Promise<{ content: string; suggestions?: string[] }> {
|
||||
const res = await fetch(`${API_BASE}/api/ai/chat`, {
|
||||
method: 'POST',
|
||||
headers: authHeaders(token),
|
||||
body: JSON.stringify({ messages, context }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({ error: 'AI request failed' }));
|
||||
throw new Error(err.error || 'AI request failed');
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
Reference in New Issue
Block a user