merge: combine all features from both codebases - mobile dashboard + layer panel + notifications + shares + all fixes

This commit is contained in:
Leopoldadmin
2026-07-04 16:43:55 +02:00
parent 0606dbb501
commit be62470b53
79 changed files with 9562 additions and 3132 deletions
+243 -8
View File
@@ -18,6 +18,7 @@ export interface Project {
name: string;
description: string | null;
owner_id: string;
folder_id: string | null;
created_at: string;
updated_at: string;
}
@@ -30,8 +31,32 @@ export interface Drawing {
updated_at: string;
}
// ─── Auth Types ────────────────────────────────────────
export interface AuthUser {
id: string;
email: string;
name: string;
role: string;
created_at: string;
updated_at: string;
}
export interface AuthSession {
token: string;
}
export interface LoginResponse {
user: AuthUser;
session: AuthSession;
}
export interface RegisterResponse {
user: AuthUser;
session: AuthSession;
}
// ─── Auth ───────────────────────────────────────────────
export async function login(email: string, password: string): Promise<{ user: any; session: { token: string } }> {
export async function login(email: string, password: string): Promise<LoginResponse> {
const res = await fetch(`${API_BASE}/api/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -41,7 +66,7 @@ export async function login(email: string, password: string): Promise<{ user: an
return res.json();
}
export async function register(email: string, password: string, name: string): Promise<{ user: any; session: { token: string } }> {
export async function register(email: string, password: string, name: string): Promise<RegisterResponse> {
const res = await fetch(`${API_BASE}/api/auth/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -51,7 +76,7 @@ export async function register(email: string, password: string, name: string): P
return res.json();
}
export async function getMe(token: string): Promise<any> {
export async function getMe(token: string): Promise<AuthUser> {
const res = await fetch(`${API_BASE}/api/auth/me`, {
headers: authHeaders(token),
});
@@ -83,6 +108,78 @@ export async function deleteProject(token: string, id: string): Promise<void> {
});
}
export async function updateProject(token: string, id: string, updates: { name?: string; description?: string | null }): Promise<Project> {
const res = await fetch(`${API_BASE}/api/projects/${id}`, {
method: 'PATCH',
headers: authHeaders(token),
body: JSON.stringify(updates),
});
if (!res.ok) throw new Error('Failed to update project');
return res.json();
}
// ─── Project Folders ─────────────────────────────────────
export interface ProjectFolder {
id: string;
name: string;
parent_id: string | null;
owner_id: string;
created_at: string;
}
export async function getProjectFolders(token: string): Promise<ProjectFolder[]> {
const res = await fetch(`${API_BASE}/api/project-folders`, { headers: authHeaders(token) });
if (!res.ok) throw new Error('Failed to load project folders');
return res.json();
}
export async function createProjectFolder(token: string, name: string, parentId?: string | null): Promise<ProjectFolder> {
const res = await fetch(`${API_BASE}/api/project-folders`, {
method: 'POST',
headers: authHeaders(token),
body: JSON.stringify({ name, parent_id: parentId ?? null }),
});
if (!res.ok) throw new Error('Failed to create project folder');
return res.json();
}
export async function renameProjectFolder(token: string, id: string, name: string): Promise<ProjectFolder> {
const res = await fetch(`${API_BASE}/api/project-folders/${id}`, {
method: 'PUT',
headers: authHeaders(token),
body: JSON.stringify({ name }),
});
if (!res.ok) throw new Error('Failed to rename project folder');
return res.json();
}
export async function deleteProjectFolder(token: string, id: string): Promise<void> {
const res = await fetch(`${API_BASE}/api/project-folders/${id}`, {
method: 'DELETE',
headers: authHeaders(token),
});
if (!res.ok) throw new Error('Failed to delete project folder');
}
export async function moveProjectToFolder(token: string, projectId: string, folderId: string | null): Promise<Project> {
const res = await fetch(`${API_BASE}/api/projects/${projectId}/folder`, {
method: 'PUT',
headers: authHeaders(token),
body: JSON.stringify({ folder_id: folderId }),
});
if (!res.ok) throw new Error('Failed to move project to folder');
return res.json();
}
export async function getProjectsByFolder(token: string, folderId: string | null): Promise<Project[]> {
const url = folderId === null
? `${API_BASE}/api/projects?folderId=null`
: `${API_BASE}/api/projects?folderId=${folderId}`;
const res = await fetch(url, { headers: authHeaders(token) });
if (!res.ok) throw new Error('Failed to load projects by folder');
return res.json();
}
// ─── Drawings ───────────────────────────────────────────
export async function getDrawings(token: string, projectId: string): Promise<Drawing[]> {
const res = await fetch(`${API_BASE}/api/projects/${projectId}/drawings`, { headers: authHeaders(token) });
@@ -349,12 +446,20 @@ export async function createBlockTyped(token: string, drawingId: string, block:
return dbBlockToFrontend(raw);
}
const projectLoadCache = new Map<string, Promise<ProjectData>>();
const projectLoadCache = new Map<string, { promise: Promise<ProjectData>; requestId: number }>();
const projectLoadRequestCounter = new Map<string, number>();
export async function loadProjectDataTyped(token: string, projectId: string): Promise<ProjectData> {
// Dedup concurrent calls (React StrictMode double-render)
// Generate a new request ID to track the latest request for this project
const currentRequestId = (projectLoadRequestCounter.get(projectId) ?? 0) + 1;
projectLoadRequestCounter.set(projectId, currentRequestId);
// Dedup concurrent calls (React StrictMode double-render): if there is an in-flight
// request with the same ID, reuse it. Otherwise start a fresh one.
const existing = projectLoadCache.get(projectId);
if (existing) return existing;
if (existing && existing.requestId === currentRequestId - 1) {
return existing.promise;
}
const promise = (async () => {
const drawings = await getDrawings(token, projectId);
@@ -373,6 +478,7 @@ export async function loadProjectDataTyped(token: string, projectId: string): Pr
getBlocks(token, drawing.id),
]);
// Return data even if a newer request exists (React StrictMode double-render safe)
return {
project,
drawing,
@@ -382,8 +488,14 @@ export async function loadProjectDataTyped(token: string, projectId: string): Pr
};
})();
projectLoadCache.set(projectId, promise);
promise.finally(() => projectLoadCache.delete(projectId));
projectLoadCache.set(projectId, { promise, requestId: currentRequestId });
promise.finally(() => {
// Only clear cache if this is still the latest request
const cached = projectLoadCache.get(projectId);
if (cached && cached.requestId === currentRequestId) {
projectLoadCache.delete(projectId);
}
});
return promise;
}
@@ -465,8 +577,131 @@ export async function deleteProjectShare(token: string, shareId: string): Promis
if (!res.ok) throw new Error('Failed to delete share');
}
// ─── Global Block Folders ───────────────────────────────────
export interface GlobalBlockFolder {
id: string;
name: string;
parent_id: string | null;
created_at: string;
}
export async function getGlobalFolders(token: string, parentId?: string | null): Promise<GlobalBlockFolder[]> {
const url = parentId !== undefined
? `${API_BASE}/api/global-folders?parentId=${parentId === null ? 'null' : parentId}`
: `${API_BASE}/api/global-folders`;
const res = await fetch(url, { headers: authHeaders(token) });
if (!res.ok) throw new Error('Failed to load global folders');
return res.json();
}
export async function createGlobalFolder(token: string, name: string, parentId?: string | null): Promise<GlobalBlockFolder> {
const res = await fetch(`${API_BASE}/api/global-folders`, {
method: 'POST',
headers: authHeaders(token),
body: JSON.stringify({ name, parent_id: parentId ?? null }),
});
if (!res.ok) throw new Error('Failed to create global folder');
return res.json();
}
export async function renameGlobalFolder(token: string, id: string, name: string): Promise<GlobalBlockFolder> {
const res = await fetch(`${API_BASE}/api/global-folders/${id}`, {
method: 'PATCH',
headers: authHeaders(token),
body: JSON.stringify({ name }),
});
if (!res.ok) throw new Error('Failed to rename global folder');
return res.json();
}
export async function deleteGlobalFolder(token: string, id: string): Promise<void> {
await fetch(`${API_BASE}/api/global-folders/${id}`, {
method: 'DELETE',
headers: authHeaders(token),
});
}
// ─── Global Blocks ────────────────────────────────────────
export interface GlobalBlock {
id: string;
folder_id: string | null;
name: string;
block_data: string;
svg_data: string | null;
created_at: string;
updated_at: string;
}
export async function getGlobalBlocks(token: string, folderId?: string | null): Promise<GlobalBlock[]> {
const url = folderId !== undefined
? `${API_BASE}/api/global-blocks?folderId=${folderId === null ? 'null' : folderId}`
: `${API_BASE}/api/global-blocks`;
const res = await fetch(url, { headers: authHeaders(token) });
if (!res.ok) throw new Error('Failed to load global blocks');
return res.json();
}
export async function createGlobalBlock(token: string, data: {
name: string;
folder_id?: string | null;
block_data?: string;
svg_data?: string;
}): Promise<GlobalBlock> {
const res = await fetch(`${API_BASE}/api/global-blocks`, {
method: 'POST',
headers: authHeaders(token),
body: JSON.stringify(data),
});
if (!res.ok) throw new Error('Failed to create global block');
return res.json();
}
export async function renameGlobalBlock(token: string, id: string, name: string): Promise<GlobalBlock> {
const res = await fetch(`${API_BASE}/api/global-blocks/${id}`, {
method: 'PATCH',
headers: authHeaders(token),
body: JSON.stringify({ name }),
});
if (!res.ok) throw new Error('Failed to rename global block');
return res.json();
}
export async function deleteGlobalBlock(token: string, id: string): Promise<void> {
await fetch(`${API_BASE}/api/global-blocks/${id}`, {
method: 'DELETE',
headers: authHeaders(token),
});
}
export { API_BASE };
// ─── Settings (Key/Value) ────────────────────────────────
export interface Setting {
key: string;
value: string;
updated_at: string;
}
export async function getSetting(token: string, key: string): Promise<Setting | null> {
const res = await fetch(`${API_BASE}/api/settings/${encodeURIComponent(key)}`, {
method: 'GET',
headers: authHeaders(token),
});
if (res.status === 404) return null;
if (!res.ok) throw new Error(`Failed to fetch setting: ${key}`);
return res.json();
}
export async function setSetting(token: string, key: string, value: string): Promise<Setting> {
const res = await fetch(`${API_BASE}/api/settings/${encodeURIComponent(key)}`, {
method: 'PUT',
headers: authHeaders(token),
body: JSON.stringify({ value }),
});
if (!res.ok) throw new Error(`Failed to save setting: ${key}`);
return res.json();
}
// ─── AI Copilot ─────────────────────────────────────────
export interface AIChatMessage {
role: string;
@@ -175,6 +175,11 @@ export class BackgroundService {
this.image = null;
}
/** Dispose of resources — called on component unmount */
dispose(): void {
this.clear();
}
/** Export to ProjectData.background format */
toProjectData(): ProjectData['background'] | undefined {
if (!this.isLoaded()) return undefined;
+3 -2
View File
@@ -15,7 +15,7 @@ export interface DimensionConfig {
y2: number;
offsetX: number;
offsetY: number;
unit: 'm' | 'cm' | 'mm';
unit: 'm' | 'cm' | 'mm' | 'deg';
precision: number;
}
@@ -105,7 +105,7 @@ export class DimensionService {
vx: number, vy: number, x1: number, y1: number, x2: number, y2: number,
layerId: string, config: Partial<DimensionConfig> = {},
): CADElement {
const cfg = { type: 'angular' as DimensionType, x1: vx, y1: vy, x2, y2, offsetX: 0, offsetY: -30, unit: 'deg' as any, precision: 1, ...config };
const cfg = { type: 'angular' as DimensionType, x1: vx, y1: vy, x2, y2, offsetX: 0, offsetY: -30, unit: 'deg' as const, precision: 1, ...config };
const a1 = Math.atan2(y1 - vy, x1 - vx);
const a2 = Math.atan2(y2 - vy, x2 - vx);
let angle = Math.abs(a2 - a1) * 180 / Math.PI;
@@ -232,6 +232,7 @@ export class DimensionService {
case 'm': val = dist / 100; suffix = ' m'; break;
case 'cm': val = dist / 10; suffix = ' cm'; break;
case 'mm': val = dist; suffix = ' mm'; break;
case 'deg': val = dist; suffix = '°'; break;
default: suffix = '';
}
return `${val.toFixed(precision)}${suffix}`;
+1 -1
View File
@@ -156,7 +156,7 @@ function elementToSVG(el: CADElement, layerColor?: string): string | null {
const stroke = (p.stroke as string) || layerColor || '#000000';
const sw = p.strokeWidth ?? 1;
const fill = p.fill as string || 'none';
const style = `stroke="${stroke}" stroke-width="${sw}" fill="${fill}"`;
const style = `stroke="${stroke}" strokeWidth="${sw}" fill="${fill}"`;
switch (el.type) {
case 'line': {