diff --git a/backend/dev.db b/backend/dev.db new file mode 100644 index 0000000..e69de29 diff --git a/backend/prisma/dev.db.backup b/backend/prisma/dev.db.backup index b42221a..8c402fb 100644 Binary files a/backend/prisma/dev.db.backup and b/backend/prisma/dev.db.backup differ diff --git a/backend/prisma/prisma/dev.db b/backend/prisma/prisma/dev.db new file mode 100644 index 0000000..d745181 Binary files /dev/null and b/backend/prisma/prisma/dev.db differ diff --git a/backend/src/generated/client/edge.js b/backend/src/generated/client/edge.js index 8d67c5b..beaceeb 100644 --- a/backend/src/generated/client/edge.js +++ b/backend/src/generated/client/edge.js @@ -144,6 +144,10 @@ const config = { "fromEnvVar": null, "value": "darwin-arm64", "native": true + }, + { + "fromEnvVar": null, + "value": "linux-musl-arm64-openssl-3.0.x" } ], "previewFeatures": [], @@ -169,8 +173,8 @@ const config = { } } }, - "inlineSchema": "// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\ngenerator client {\n provider = \"prisma-client-js\"\n output = \"../src/generated/client\"\n}\n\ndatasource db {\n provider = \"sqlite\"\n url = env(\"DATABASE_URL\")\n}\n\nmodel Collection {\n id String @id @default(uuid())\n name String\n drawings Drawing[]\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n\nmodel Drawing {\n id String @id @default(uuid())\n name String\n elements String // Stored as JSON string\n appState String // Stored as JSON string\n files String @default(\"{}\") // Stored as JSON string\n preview String? // SVG string for thumbnail\n version Int @default(1)\n collectionId String?\n collection Collection? @relation(fields: [collectionId], references: [id])\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n", - "inlineSchemaHash": "e43b17bb99e7f864b5a0f2c54951015f0a3d998329d0577afda58e7c0dee4919", + "inlineSchema": "// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\ngenerator client {\n provider = \"prisma-client-js\"\n output = \"../src/generated/client\"\n binaryTargets = [\"native\", \"linux-musl-arm64-openssl-3.0.x\"]\n}\n\ndatasource db {\n provider = \"sqlite\"\n url = env(\"DATABASE_URL\")\n}\n\nmodel Collection {\n id String @id @default(uuid())\n name String\n drawings Drawing[]\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n\nmodel Drawing {\n id String @id @default(uuid())\n name String\n elements String // Stored as JSON string\n appState String // Stored as JSON string\n files String @default(\"{}\") // Stored as JSON string\n preview String? // SVG string for thumbnail\n version Int @default(1)\n collectionId String?\n collection Collection? @relation(fields: [collectionId], references: [id])\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n", + "inlineSchemaHash": "9864a039193c73ddda01fd51751788fa5729bb0a603a9379a3fa314a4aced64f", "copyEngine": true } config.dirname = '/' diff --git a/backend/src/generated/client/index.js b/backend/src/generated/client/index.js index 90a7b40..3fb5da4 100644 --- a/backend/src/generated/client/index.js +++ b/backend/src/generated/client/index.js @@ -145,6 +145,10 @@ const config = { "fromEnvVar": null, "value": "darwin-arm64", "native": true + }, + { + "fromEnvVar": null, + "value": "linux-musl-arm64-openssl-3.0.x" } ], "previewFeatures": [], @@ -170,8 +174,8 @@ const config = { } } }, - "inlineSchema": "// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\ngenerator client {\n provider = \"prisma-client-js\"\n output = \"../src/generated/client\"\n}\n\ndatasource db {\n provider = \"sqlite\"\n url = env(\"DATABASE_URL\")\n}\n\nmodel Collection {\n id String @id @default(uuid())\n name String\n drawings Drawing[]\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n\nmodel Drawing {\n id String @id @default(uuid())\n name String\n elements String // Stored as JSON string\n appState String // Stored as JSON string\n files String @default(\"{}\") // Stored as JSON string\n preview String? // SVG string for thumbnail\n version Int @default(1)\n collectionId String?\n collection Collection? @relation(fields: [collectionId], references: [id])\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n", - "inlineSchemaHash": "e43b17bb99e7f864b5a0f2c54951015f0a3d998329d0577afda58e7c0dee4919", + "inlineSchema": "// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\ngenerator client {\n provider = \"prisma-client-js\"\n output = \"../src/generated/client\"\n binaryTargets = [\"native\", \"linux-musl-arm64-openssl-3.0.x\"]\n}\n\ndatasource db {\n provider = \"sqlite\"\n url = env(\"DATABASE_URL\")\n}\n\nmodel Collection {\n id String @id @default(uuid())\n name String\n drawings Drawing[]\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n\nmodel Drawing {\n id String @id @default(uuid())\n name String\n elements String // Stored as JSON string\n appState String // Stored as JSON string\n files String @default(\"{}\") // Stored as JSON string\n preview String? // SVG string for thumbnail\n version Int @default(1)\n collectionId String?\n collection Collection? @relation(fields: [collectionId], references: [id])\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n", + "inlineSchemaHash": "9864a039193c73ddda01fd51751788fa5729bb0a603a9379a3fa314a4aced64f", "copyEngine": true } @@ -211,6 +215,10 @@ Object.assign(exports, Prisma) // file annotations for bundling tools to include these files path.join(__dirname, "libquery_engine-darwin-arm64.dylib.node"); path.join(process.cwd(), "src/generated/client/libquery_engine-darwin-arm64.dylib.node") + +// file annotations for bundling tools to include these files +path.join(__dirname, "libquery_engine-linux-musl-arm64-openssl-3.0.x.so.node"); +path.join(process.cwd(), "src/generated/client/libquery_engine-linux-musl-arm64-openssl-3.0.x.so.node") // file annotations for bundling tools to include these files path.join(__dirname, "schema.prisma"); path.join(process.cwd(), "src/generated/client/schema.prisma") diff --git a/backend/src/generated/client/libquery_engine-linux-musl-arm64-openssl-3.0.x.so.node b/backend/src/generated/client/libquery_engine-linux-musl-arm64-openssl-3.0.x.so.node new file mode 100755 index 0000000..3d5bed5 Binary files /dev/null and b/backend/src/generated/client/libquery_engine-linux-musl-arm64-openssl-3.0.x.so.node differ diff --git a/backend/src/generated/client/package.json b/backend/src/generated/client/package.json index 737cd96..9f2f9a3 100644 --- a/backend/src/generated/client/package.json +++ b/backend/src/generated/client/package.json @@ -1,5 +1,5 @@ { - "name": "prisma-client-0eff54f2002515791cbb1f1a5b1632637c7e46544d107e5ff89b7092d9540808", + "name": "prisma-client-04007c5051869a2f5298bd562ab2fb60a423747e0d5699dd1a73a4757b2657b6", "main": "index.js", "types": "index.d.ts", "browser": "index-browser.js", diff --git a/backend/src/generated/client/schema.prisma b/backend/src/generated/client/schema.prisma index 1a6ccfc..e4c370a 100644 --- a/backend/src/generated/client/schema.prisma +++ b/backend/src/generated/client/schema.prisma @@ -2,8 +2,9 @@ // learn more about it in the docs: https://pris.ly/d/prisma-schema generator client { - provider = "prisma-client-js" - output = "../src/generated/client" + provider = "prisma-client-js" + output = "../src/generated/client" + binaryTargets = ["native", "linux-musl-arm64-openssl-3.0.x"] } datasource db { diff --git a/backend/src/index.ts b/backend/src/index.ts index b49433b..ecc21ff 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -12,10 +12,28 @@ import { PrismaClient } from "./generated/client"; dotenv.config(); -// Ensure DATABASE_URL is absolute to avoid relative path issues with generated client -// Point to the same DB file as Prisma CLI (relative to schema.prisma, usually in prisma/ folder) -const dbPath = path.resolve(__dirname, "../prisma/dev.db"); -process.env.DATABASE_URL = `file:${dbPath}`; +// Ensure DATABASE_URL always points to an absolute path when using SQLite. +// Respect externally provided values and only fall back to the dev database when unset. +const backendRoot = path.resolve(__dirname, "../"); +const defaultDbPath = path.resolve(backendRoot, "prisma/dev.db"); +const resolveDatabaseUrl = (rawUrl?: string) => { + if (!rawUrl || rawUrl.trim().length === 0) { + return `file:${defaultDbPath}`; + } + + if (!rawUrl.startsWith("file:")) { + return rawUrl; + } + + const filePath = rawUrl.replace(/^file:/, ""); + const absolutePath = path.isAbsolute(filePath) + ? filePath + : path.resolve(backendRoot, filePath); + + return `file:${absolutePath}`; +}; + +process.env.DATABASE_URL = resolveDatabaseUrl(process.env.DATABASE_URL); console.log("Resolved DATABASE_URL:", process.env.DATABASE_URL); const app = express(); diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index a109ada..158ca02 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -1,27 +1,50 @@ -import axios from 'axios'; -import type { Drawing, Collection } from '../types'; +import axios from "axios"; +import type { Drawing, Collection } from "../types"; -export const API_URL = import.meta.env.VITE_API_URL || '/api'; +export const API_URL = import.meta.env.VITE_API_URL || "/api"; export const api = axios.create({ baseURL: API_URL, }); -export const getDrawings = async (search?: string, collectionId?: string | null) => { +const coerceTimestamp = (value: string | number | Date): number => { + if (typeof value === "number") return value; + if (value instanceof Date) return value.getTime(); + const parsed = Date.parse(value); + return Number.isNaN(parsed) ? Date.now() : parsed; +}; + +const deserializeDrawing = (drawing: any): Drawing => ({ + ...drawing, + createdAt: coerceTimestamp(drawing.createdAt), + updatedAt: coerceTimestamp(drawing.updatedAt), +}); + +export const getDrawings = async ( + search?: string, + collectionId?: string | null +) => { const params: any = {}; if (search) params.search = search; - if (collectionId !== undefined) params.collectionId = collectionId === null ? 'null' : collectionId; - const response = await api.get('/drawings', { params }); - return response.data; + if (collectionId !== undefined) + params.collectionId = collectionId === null ? "null" : collectionId; + const response = await api.get("/drawings", { params }); + return response.data.map(deserializeDrawing); }; export const getDrawing = async (id: string) => { const response = await api.get(`/drawings/${id}`); - return response.data; + return deserializeDrawing(response.data); }; -export const createDrawing = async (name?: string, collectionId?: string | null) => { - const response = await api.post<{ id: string }>('/drawings', { name, collectionId }); +export const createDrawing = async ( + name?: string, + collectionId?: string | null +) => { + const response = await api.post<{ id: string }>("/drawings", { + name, + collectionId, + }); return response.data; }; @@ -36,22 +59,24 @@ export const deleteDrawing = async (id: string) => { }; export const duplicateDrawing = async (id: string) => { - const response = await api.post<{ id: string }>(`/drawings/${id}/duplicate`); - return response.data; + const response = await api.post(`/drawings/${id}/duplicate`); + return deserializeDrawing(response.data); }; export const getCollections = async () => { - const response = await api.get('/collections'); + const response = await api.get("/collections"); return response.data; }; export const createCollection = async (name: string) => { - const response = await api.post('/collections', { name }); + const response = await api.post("/collections", { name }); return response.data; }; export const updateCollection = async (id: string, name: string) => { - const response = await api.put<{ success: true }>(`/collections/${id}`, { name }); + const response = await api.put<{ success: true }>(`/collections/${id}`, { + name, + }); return response.data; }; diff --git a/frontend/src/components/DrawingCard.tsx b/frontend/src/components/DrawingCard.tsx index 45ebf75..a874341 100644 --- a/frontend/src/components/DrawingCard.tsx +++ b/frontend/src/components/DrawingCard.tsx @@ -69,7 +69,7 @@ export const DrawingCard: React.FC = ({ exportBackground: true, viewBackgroundColor: drawing.appState.viewBackgroundColor || "#ffffff" }, - files: null, + files: drawing.files || {}, exportPadding: 10 }); const previewHtml = svg.outerHTML; diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index 20af1a0..2fd1be5 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -9,7 +9,7 @@ import type { Drawing, Collection } from '../types'; import { useDebounce } from '../hooks/useDebounce'; import clsx from 'clsx'; import { ConfirmModal } from '../components/ConfirmModal'; -import { importDrawings, importLibrary } from '../utils/importUtils'; +import { importDrawings } from '../utils/importUtils'; type Point = { x: number; y: number }; @@ -542,17 +542,14 @@ export const Dashboard: React.FC = () => { setIsLoading(true); const libFiles = files.filter(f => f.name.endsWith('.excalidrawlib')); - const drawingFiles = files.filter(f => !f.name.endsWith('.excalidrawlib')); - if (libFiles.length > 0) { - for (const file of libFiles) { - const res = await importLibrary(file); - if (!res.success) { - alert(`Failed to import library ${file.name}: ${res.error}`); - } - } + setShowImportError({ + isOpen: true, + message: 'Library (.excalidrawlib) imports are not supported in this build. Please import drawings (.excalidraw/.json) instead.' + }); } + const drawingFiles = files.filter(f => !f.name.endsWith('.excalidrawlib')); if (drawingFiles.length > 0) { const result = await importDrawings(drawingFiles, targetCollectionId, refreshData); if (result.failed > 0) { diff --git a/frontend/src/utils/importUtils.ts b/frontend/src/utils/importUtils.ts index 8091041..78b7e6f 100644 --- a/frontend/src/utils/importUtils.ts +++ b/frontend/src/utils/importUtils.ts @@ -76,43 +76,3 @@ export const importDrawings = async ( return { success: successCount, failed: failCount, errors }; }; - -export const importLibrary = async (file: File) => { - try { - const text = await file.text(); - const data = JSON.parse(text); - - let newItems = []; - if (data.libraryItems) { - newItems = data.libraryItems; - } else if (Array.isArray(data)) { - newItems = data; - } else { - throw new Error("Invalid library file format"); - } - - // Fetch existing - const existingRes = await fetch(`${API_URL}/library`); - let existingItems = []; - if (existingRes.ok) { - const existingData = await existingRes.json(); - existingItems = existingData.libraryItems || []; - } - - // Merge (simple concat) - const mergedItems = [...existingItems, ...newItems]; - - // Save - const saveRes = await fetch(`${API_URL}/library`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ libraryItems: mergedItems }), - }); - - if (!saveRes.ok) throw new Error("Failed to save library"); - return { success: true, count: newItems.length }; - } catch (err: any) { - console.error("Library import failed:", err); - return { success: false, error: err.message }; - } -};