Compare commits
9 Commits
v0.4.1-dev
...
v0.4.5-dev
| Author | SHA1 | Date | |
|---|---|---|---|
| dd0f381ed1 | |||
| c40a5f46a0 | |||
| 8fcca43b0d | |||
| f20412cdfb | |||
| a366acfedc | |||
| 154dcbb151 | |||
| 2e74d2ad1a | |||
| 173c050f58 | |||
| 8161a563f0 |
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "backend",
|
"name": "backend",
|
||||||
"version": "0.4.0",
|
"version": "0.4.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "backend",
|
"name": "backend",
|
||||||
"version": "0.4.0",
|
"version": "0.4.1",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/client": "^5.22.0",
|
"@prisma/client": "^5.22.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "backend",
|
"name": "backend",
|
||||||
"version": "0.4.1",
|
"version": "0.4.5",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -259,8 +259,12 @@ app.use((req, res, next) => {
|
|||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
// HTTPS enforcement in production
|
// HTTPS enforcement in production only when configured frontend origins use HTTPS.
|
||||||
if (config.nodeEnv === "production") {
|
const shouldEnforceHttps =
|
||||||
|
config.nodeEnv === "production" &&
|
||||||
|
allowedOrigins.some((origin) => origin.toLowerCase().startsWith("https://"));
|
||||||
|
|
||||||
|
if (shouldEnforceHttps) {
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
if (req.header("x-forwarded-proto") !== "https") {
|
if (req.header("x-forwarded-proto") !== "https") {
|
||||||
res.redirect(`https://${req.header("host")}${req.url}`);
|
res.redirect(`https://${req.header("host")}${req.url}`);
|
||||||
|
|||||||
@@ -334,7 +334,20 @@ export const registerDashboardRoutes = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedDrawing = await prisma.drawing.update({ where: { id }, data });
|
const updateResult = await prisma.drawing.updateMany({
|
||||||
|
where: { id, userId: req.user.id },
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
if (updateResult.count === 0) {
|
||||||
|
return res.status(404).json({ error: "Drawing not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedDrawing = await prisma.drawing.findFirst({
|
||||||
|
where: { id, userId: req.user.id },
|
||||||
|
});
|
||||||
|
if (!updatedDrawing) {
|
||||||
|
return res.status(404).json({ error: "Drawing not found" });
|
||||||
|
}
|
||||||
invalidateDrawingsCache();
|
invalidateDrawingsCache();
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
@@ -352,7 +365,12 @@ export const registerDashboardRoutes = (
|
|||||||
const drawing = await prisma.drawing.findFirst({ where: { id, userId: req.user.id } });
|
const drawing = await prisma.drawing.findFirst({ where: { id, userId: req.user.id } });
|
||||||
if (!drawing) return res.status(404).json({ error: "Drawing not found" });
|
if (!drawing) return res.status(404).json({ error: "Drawing not found" });
|
||||||
|
|
||||||
await prisma.drawing.delete({ where: { id } });
|
const deleteResult = await prisma.drawing.deleteMany({
|
||||||
|
where: { id, userId: req.user.id },
|
||||||
|
});
|
||||||
|
if (deleteResult.count === 0) {
|
||||||
|
return res.status(404).json({ error: "Drawing not found" });
|
||||||
|
}
|
||||||
invalidateDrawingsCache();
|
invalidateDrawingsCache();
|
||||||
|
|
||||||
if (config.enableAuditLogging) {
|
if (config.enableAuditLogging) {
|
||||||
@@ -375,6 +393,9 @@ export const registerDashboardRoutes = (
|
|||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const original = await prisma.drawing.findFirst({ where: { id, userId: req.user.id } });
|
const original = await prisma.drawing.findFirst({ where: { id, userId: req.user.id } });
|
||||||
if (!original) return res.status(404).json({ error: "Original drawing not found" });
|
if (!original) return res.status(404).json({ error: "Original drawing not found" });
|
||||||
|
if (original.collectionId === "trash") {
|
||||||
|
await ensureTrashCollection(prisma, req.user.id);
|
||||||
|
}
|
||||||
|
|
||||||
const newDrawing = await prisma.drawing.create({
|
const newDrawing = await prisma.drawing.create({
|
||||||
data: {
|
data: {
|
||||||
@@ -443,10 +464,19 @@ export const registerDashboardRoutes = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const sanitizedName = sanitizeText(parsed.data, 100);
|
const sanitizedName = sanitizeText(parsed.data, 100);
|
||||||
const updatedCollection = await prisma.collection.update({
|
const updateResult = await prisma.collection.updateMany({
|
||||||
where: { id },
|
where: { id, userId: req.user.id },
|
||||||
data: { name: sanitizedName },
|
data: { name: sanitizedName },
|
||||||
});
|
});
|
||||||
|
if (updateResult.count === 0) {
|
||||||
|
return res.status(404).json({ error: "Collection not found" });
|
||||||
|
}
|
||||||
|
const updatedCollection = await prisma.collection.findFirst({
|
||||||
|
where: { id, userId: req.user.id },
|
||||||
|
});
|
||||||
|
if (!updatedCollection) {
|
||||||
|
return res.status(404).json({ error: "Collection not found" });
|
||||||
|
}
|
||||||
return res.json(updatedCollection);
|
return res.json(updatedCollection);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -464,7 +494,7 @@ export const registerDashboardRoutes = (
|
|||||||
where: { collectionId: id, userId: req.user.id },
|
where: { collectionId: id, userId: req.user.id },
|
||||||
data: { collectionId: null },
|
data: { collectionId: null },
|
||||||
}),
|
}),
|
||||||
prisma.collection.delete({ where: { id } }),
|
prisma.collection.deleteMany({ where: { id, userId: req.user.id } }),
|
||||||
]);
|
]);
|
||||||
invalidateDrawingsCache();
|
invalidateDrawingsCache();
|
||||||
|
|
||||||
|
|||||||
@@ -131,6 +131,21 @@ const makeUniqueName = (base: string, used: Set<string>): string => {
|
|||||||
return candidate;
|
return candidate;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const findFirstDuplicate = (values: string[]): string | null => {
|
||||||
|
const seen = new Set<string>();
|
||||||
|
for (const value of values) {
|
||||||
|
if (seen.has(value)) return value;
|
||||||
|
seen.add(value);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeNonEmptyId = (value: unknown): string | null => {
|
||||||
|
if (typeof value !== "string") return null;
|
||||||
|
const trimmed = value.trim();
|
||||||
|
return trimmed.length > 0 ? trimmed : null;
|
||||||
|
};
|
||||||
|
|
||||||
const findSqliteTable = (tables: string[], candidates: string[]): string | null => {
|
const findSqliteTable = (tables: string[], candidates: string[]): string | null => {
|
||||||
const byLower = new Map(tables.map((t) => [t.toLowerCase(), t]));
|
const byLower = new Map(tables.map((t) => [t.toLowerCase(), t]));
|
||||||
for (const candidate of candidates) {
|
for (const candidate of candidates) {
|
||||||
@@ -439,6 +454,28 @@ Drawings: ${drawings.length}
|
|||||||
message: `Too many drawings (max ${MAX_IMPORT_DRAWINGS})`,
|
message: `Too many drawings (max ${MAX_IMPORT_DRAWINGS})`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const duplicateCollectionId = findFirstDuplicate(manifest.collections.map((c) => c.id));
|
||||||
|
if (duplicateCollectionId) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: "Invalid backup manifest",
|
||||||
|
message: `Duplicate collection id in manifest: ${duplicateCollectionId}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const duplicateDrawingId = findFirstDuplicate(manifest.drawings.map((d) => d.id));
|
||||||
|
if (duplicateDrawingId) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: "Invalid backup manifest",
|
||||||
|
message: `Duplicate drawing id in manifest: ${duplicateDrawingId}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const duplicateDrawingPath = findFirstDuplicate(manifest.drawings.map((d) => d.filePath));
|
||||||
|
if (duplicateDrawingPath) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: "Invalid backup manifest",
|
||||||
|
message: `Duplicate drawing file path in manifest: ${duplicateDrawingPath}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
for (const drawing of manifest.drawings) {
|
for (const drawing of manifest.drawings) {
|
||||||
if (!getSafeZipEntry(zip, drawing.filePath)) {
|
if (!getSafeZipEntry(zip, drawing.filePath)) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
@@ -532,6 +569,28 @@ Drawings: ${drawings.length}
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const duplicateCollectionId = findFirstDuplicate(manifest.collections.map((c) => c.id));
|
||||||
|
if (duplicateCollectionId) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: "Invalid backup manifest",
|
||||||
|
message: `Duplicate collection id in manifest: ${duplicateCollectionId}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const duplicateDrawingId = findFirstDuplicate(manifest.drawings.map((d) => d.id));
|
||||||
|
if (duplicateDrawingId) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: "Invalid backup manifest",
|
||||||
|
message: `Duplicate drawing id in manifest: ${duplicateDrawingId}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const duplicateDrawingPath = findFirstDuplicate(manifest.drawings.map((d) => d.filePath));
|
||||||
|
if (duplicateDrawingPath) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: "Invalid backup manifest",
|
||||||
|
message: `Duplicate drawing file path in manifest: ${duplicateDrawingPath}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
type PreparedImportDrawing = {
|
type PreparedImportDrawing = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -772,6 +831,31 @@ Drawings: ${drawings.length}
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const duplicateDrawingIdRow = db
|
||||||
|
.prepare(
|
||||||
|
`SELECT id FROM "${drawingTable}" WHERE id IS NOT NULL GROUP BY id HAVING COUNT(1) > 1 LIMIT 1`
|
||||||
|
)
|
||||||
|
.get();
|
||||||
|
if (duplicateDrawingIdRow?.id) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: "Invalid legacy DB",
|
||||||
|
message: `Duplicate drawing id in legacy DB: ${String(duplicateDrawingIdRow.id)}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (collectionTable) {
|
||||||
|
const duplicateCollectionIdRow = db
|
||||||
|
.prepare(
|
||||||
|
`SELECT id FROM "${collectionTable}" WHERE id IS NOT NULL GROUP BY id HAVING COUNT(1) > 1 LIMIT 1`
|
||||||
|
)
|
||||||
|
.get();
|
||||||
|
if (duplicateCollectionIdRow?.id) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: "Invalid legacy DB",
|
||||||
|
message: `Duplicate collection id in legacy DB: ${String(duplicateCollectionIdRow.id)}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let latestMigration: string | null = null;
|
let latestMigration: string | null = null;
|
||||||
const migrationsTable = findSqliteTable(tables, ["_prisma_migrations"]);
|
const migrationsTable = findSqliteTable(tables, ["_prisma_migrations"]);
|
||||||
if (migrationsTable) {
|
if (migrationsTable) {
|
||||||
@@ -862,6 +946,28 @@ Drawings: ${drawings.length}
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const importedCollectionIds = importedCollections
|
||||||
|
.map((c) => normalizeNonEmptyId(c?.id))
|
||||||
|
.filter((id): id is string => id !== null);
|
||||||
|
const duplicateCollectionId = findFirstDuplicate(importedCollectionIds);
|
||||||
|
if (duplicateCollectionId) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: "Invalid legacy DB",
|
||||||
|
message: `Duplicate collection id in legacy DB: ${duplicateCollectionId}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const importedDrawingIds = importedDrawings
|
||||||
|
.map((d) => normalizeNonEmptyId(d?.id))
|
||||||
|
.filter((id): id is string => id !== null);
|
||||||
|
const duplicateDrawingId = findFirstDuplicate(importedDrawingIds);
|
||||||
|
if (duplicateDrawingId) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: "Invalid legacy DB",
|
||||||
|
message: `Duplicate drawing id in legacy DB: ${duplicateDrawingId}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
type PreparedLegacyDrawing = {
|
type PreparedLegacyDrawing = {
|
||||||
importedId: string | null;
|
importedId: string | null;
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
Generated
+12
-25
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"version": "0.3.2",
|
"version": "0.4.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"version": "0.3.2",
|
"version": "0.4.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
@@ -162,7 +162,6 @@
|
|||||||
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.27.1",
|
"@babel/code-frame": "^7.27.1",
|
||||||
"@babel/generator": "^7.28.5",
|
"@babel/generator": "^7.28.5",
|
||||||
@@ -517,7 +516,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
},
|
},
|
||||||
@@ -561,7 +559,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
@@ -2612,7 +2609,8 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
|
||||||
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
|
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/@types/babel__core": {
|
"node_modules/@types/babel__core": {
|
||||||
"version": "7.20.5",
|
"version": "7.20.5",
|
||||||
@@ -2777,7 +2775,6 @@
|
|||||||
"integrity": "sha512-V0kuGBX3+prX+DQ/7r2qsv1NsdfnCLnTgnRJ1pYnxykBhGMz+qj+box5lq7XsO5mtZsBqpjwwTu/7wszPfMBcw==",
|
"integrity": "sha512-V0kuGBX3+prX+DQ/7r2qsv1NsdfnCLnTgnRJ1pYnxykBhGMz+qj+box5lq7XsO5mtZsBqpjwwTu/7wszPfMBcw==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/prop-types": "*",
|
"@types/prop-types": "*",
|
||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
@@ -2789,7 +2786,6 @@
|
|||||||
"integrity": "sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==",
|
"integrity": "sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/react": "*"
|
"@types/react": "*"
|
||||||
}
|
}
|
||||||
@@ -2860,7 +2856,6 @@
|
|||||||
"integrity": "sha512-lJi3PfxVmo0AkEY93ecfN+r8SofEqZNGByvHAI3GBLrvt1Cw6H5k1IM02nSzu0RfUafr2EvFSw0wAsZgubNplQ==",
|
"integrity": "sha512-lJi3PfxVmo0AkEY93ecfN+r8SofEqZNGByvHAI3GBLrvt1Cw6H5k1IM02nSzu0RfUafr2EvFSw0wAsZgubNplQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.47.0",
|
"@typescript-eslint/scope-manager": "8.47.0",
|
||||||
"@typescript-eslint/types": "8.47.0",
|
"@typescript-eslint/types": "8.47.0",
|
||||||
@@ -3224,7 +3219,6 @@
|
|||||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@@ -3275,6 +3269,7 @@
|
|||||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
@@ -3504,7 +3499,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.8.25",
|
"baseline-browser-mapping": "^2.8.25",
|
||||||
"caniuse-lite": "^1.0.30001754",
|
"caniuse-lite": "^1.0.30001754",
|
||||||
@@ -3840,7 +3834,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz",
|
"resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz",
|
||||||
"integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==",
|
"integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10"
|
"node": ">=0.10"
|
||||||
}
|
}
|
||||||
@@ -4214,7 +4207,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
||||||
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
@@ -4454,7 +4446,8 @@
|
|||||||
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
|
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
|
||||||
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
|
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/dompurify": {
|
"node_modules/dompurify": {
|
||||||
"version": "3.1.6",
|
"version": "3.1.6",
|
||||||
@@ -4673,7 +4666,6 @@
|
|||||||
"integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
|
"integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.8.0",
|
"@eslint-community/eslint-utils": "^4.8.0",
|
||||||
"@eslint-community/regexpp": "^4.12.1",
|
"@eslint-community/regexpp": "^4.12.1",
|
||||||
@@ -5507,7 +5499,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/jotai/-/jotai-2.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/jotai/-/jotai-2.11.0.tgz",
|
||||||
"integrity": "sha512-zKfoBBD1uDw3rljwHkt0fWuja1B76R7CjznuBO+mSX6jpsO1EBeWNRKpeaQho9yPI/pvCv4recGfgOXGxwPZvQ==",
|
"integrity": "sha512-zKfoBBD1uDw3rljwHkt0fWuja1B76R7CjznuBO+mSX6jpsO1EBeWNRKpeaQho9yPI/pvCv4recGfgOXGxwPZvQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12.20.0"
|
"node": ">=12.20.0"
|
||||||
},
|
},
|
||||||
@@ -5855,6 +5846,7 @@
|
|||||||
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
|
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"lz-string": "bin/bin.js"
|
"lz-string": "bin/bin.js"
|
||||||
}
|
}
|
||||||
@@ -6884,7 +6876,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.7",
|
"nanoid": "^3.3.7",
|
||||||
"picocolors": "^1.0.0",
|
"picocolors": "^1.0.0",
|
||||||
@@ -7050,6 +7041,7 @@
|
|||||||
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
|
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ansi-regex": "^5.0.1",
|
"ansi-regex": "^5.0.1",
|
||||||
"ansi-styles": "^5.0.0",
|
"ansi-styles": "^5.0.0",
|
||||||
@@ -7065,6 +7057,7 @@
|
|||||||
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
},
|
},
|
||||||
@@ -7120,7 +7113,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||||
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"loose-envify": "^1.1.0"
|
"loose-envify": "^1.1.0"
|
||||||
},
|
},
|
||||||
@@ -7133,7 +7125,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||||
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"loose-envify": "^1.1.0",
|
"loose-envify": "^1.1.0",
|
||||||
"scheduler": "^0.23.2"
|
"scheduler": "^0.23.2"
|
||||||
@@ -7147,7 +7138,8 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||||
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/react-refresh": {
|
"node_modules/react-refresh": {
|
||||||
"version": "0.18.0",
|
"version": "0.18.0",
|
||||||
@@ -7862,7 +7854,6 @@
|
|||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -8001,7 +7992,6 @@
|
|||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@@ -8191,7 +8181,6 @@
|
|||||||
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
|
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
"fdir": "^6.5.0",
|
"fdir": "^6.5.0",
|
||||||
@@ -8285,7 +8274,6 @@
|
|||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -8612,7 +8600,6 @@
|
|||||||
"integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==",
|
"integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.4.1",
|
"version": "0.4.5",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --port 6767",
|
"dev": "vite --port 6767",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React, { useState, useRef, useEffect } from 'react';
|
|||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import { Menu, X } from 'lucide-react';
|
import { Menu, X } from 'lucide-react';
|
||||||
import { Sidebar } from './Sidebar';
|
import { Sidebar } from './Sidebar';
|
||||||
|
import { Logo } from './Logo';
|
||||||
import { UploadStatus } from './UploadStatus';
|
import { UploadStatus } from './UploadStatus';
|
||||||
import type { Collection } from '../types';
|
import type { Collection } from '../types';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
@@ -35,6 +36,8 @@ export const Layout: React.FC<LayoutProps> = ({
|
|||||||
const sidebarRef = useRef<HTMLDivElement>(null);
|
const sidebarRef = useRef<HTMLDivElement>(null);
|
||||||
const startXRef = useRef(0);
|
const startXRef = useRef(0);
|
||||||
const startWidthRef = useRef(0);
|
const startWidthRef = useRef(0);
|
||||||
|
const resizeMouseMoveHandlerRef = useRef<((e: MouseEvent) => void) | null>(null);
|
||||||
|
const resizeMouseUpHandlerRef = useRef<(() => void) | null>(null);
|
||||||
|
|
||||||
// Handle mouse down on resize handle
|
// Handle mouse down on resize handle
|
||||||
const handleMouseDown = (e: React.MouseEvent) => {
|
const handleMouseDown = (e: React.MouseEvent) => {
|
||||||
@@ -43,6 +46,13 @@ export const Layout: React.FC<LayoutProps> = ({
|
|||||||
startXRef.current = e.clientX;
|
startXRef.current = e.clientX;
|
||||||
startWidthRef.current = sidebarWidth;
|
startWidthRef.current = sidebarWidth;
|
||||||
|
|
||||||
|
if (resizeMouseMoveHandlerRef.current) {
|
||||||
|
document.removeEventListener('mousemove', resizeMouseMoveHandlerRef.current);
|
||||||
|
}
|
||||||
|
if (resizeMouseUpHandlerRef.current) {
|
||||||
|
document.removeEventListener('mouseup', resizeMouseUpHandlerRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
const handleMouseMove = (e: MouseEvent) => {
|
const handleMouseMove = (e: MouseEvent) => {
|
||||||
const diff = e.clientX - startXRef.current;
|
const diff = e.clientX - startXRef.current;
|
||||||
const newWidth = Math.max(200, Math.min(600, startWidthRef.current + diff));
|
const newWidth = Math.max(200, Math.min(600, startWidthRef.current + diff));
|
||||||
@@ -53,8 +63,12 @@ export const Layout: React.FC<LayoutProps> = ({
|
|||||||
setIsResizing(false);
|
setIsResizing(false);
|
||||||
document.removeEventListener('mousemove', handleMouseMove);
|
document.removeEventListener('mousemove', handleMouseMove);
|
||||||
document.removeEventListener('mouseup', handleMouseUp);
|
document.removeEventListener('mouseup', handleMouseUp);
|
||||||
|
resizeMouseMoveHandlerRef.current = null;
|
||||||
|
resizeMouseUpHandlerRef.current = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
resizeMouseMoveHandlerRef.current = handleMouseMove;
|
||||||
|
resizeMouseUpHandlerRef.current = handleMouseUp;
|
||||||
document.addEventListener('mousemove', handleMouseMove);
|
document.addEventListener('mousemove', handleMouseMove);
|
||||||
document.addEventListener('mouseup', handleMouseUp);
|
document.addEventListener('mouseup', handleMouseUp);
|
||||||
};
|
};
|
||||||
@@ -62,8 +76,14 @@ export const Layout: React.FC<LayoutProps> = ({
|
|||||||
// Cleanup event listeners on unmount
|
// Cleanup event listeners on unmount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('mousemove', () => {});
|
if (resizeMouseMoveHandlerRef.current) {
|
||||||
document.removeEventListener('mouseup', () => {});
|
document.removeEventListener('mousemove', resizeMouseMoveHandlerRef.current);
|
||||||
|
resizeMouseMoveHandlerRef.current = null;
|
||||||
|
}
|
||||||
|
if (resizeMouseUpHandlerRef.current) {
|
||||||
|
document.removeEventListener('mouseup', resizeMouseUpHandlerRef.current);
|
||||||
|
resizeMouseUpHandlerRef.current = null;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -89,16 +109,22 @@ export const Layout: React.FC<LayoutProps> = ({
|
|||||||
{isMobile ? (
|
{isMobile ? (
|
||||||
<div className="relative h-full min-w-0">
|
<div className="relative h-full min-w-0">
|
||||||
<main className="h-full min-w-0 bg-white/40 dark:bg-neutral-900/40 backdrop-blur-sm rounded-2xl border border-white/50 dark:border-neutral-800/50 shadow-sm transition-colors duration-200 overflow-hidden flex flex-col">
|
<main className="h-full min-w-0 bg-white/40 dark:bg-neutral-900/40 backdrop-blur-sm rounded-2xl border border-white/50 dark:border-neutral-800/50 shadow-sm transition-colors duration-200 overflow-hidden flex flex-col">
|
||||||
<div className="px-3 pt-3 flex-shrink-0">
|
<div className="h-16 flex-shrink-0 flex items-center px-4 border-b border-black/5 dark:border-white/5 bg-white/50 dark:bg-neutral-900/50 backdrop-blur-md">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setIsSidebarOpen(v => !v)}
|
onClick={() => setIsSidebarOpen(v => !v)}
|
||||||
className="inline-flex items-center justify-center h-11 w-11 rounded-xl border-2 border-black dark:border-neutral-700 bg-white/90 dark:bg-neutral-900/90 shadow-[2px_2px_0px_0px_rgba(0,0,0,1)] dark:shadow-[2px_2px_0px_0px_rgba(255,255,255,0.2)] backdrop-blur-sm text-slate-900 dark:text-neutral-200 hover:-translate-y-0.5 transition-all"
|
className="inline-flex items-center justify-center h-11 w-11 rounded-xl border-2 border-black dark:border-neutral-700 bg-white/90 dark:bg-neutral-900/90 shadow-[2px_2px_0px_0px_rgba(0,0,0,1)] dark:shadow-[2px_2px_0px_0px_rgba(255,255,255,0.2)] text-slate-900 dark:text-neutral-200 hover:-translate-y-0.5 transition-all active:translate-y-0 active:shadow-none"
|
||||||
title={isSidebarOpen ? 'Close menu' : 'Open menu'}
|
title={isSidebarOpen ? 'Close menu' : 'Open menu'}
|
||||||
aria-label={isSidebarOpen ? 'Close menu' : 'Open menu'}
|
aria-label={isSidebarOpen ? 'Close menu' : 'Open menu'}
|
||||||
>
|
>
|
||||||
{isSidebarOpen ? <X size={20} /> : <Menu size={20} />}
|
{isSidebarOpen ? <X size={20} /> : <Menu size={20} />}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<div className="ml-auto flex items-center gap-2">
|
||||||
|
<Logo className="w-8 h-8" />
|
||||||
|
<span className="text-xl text-slate-900 dark:text-white mt-1" style={{ fontFamily: 'Excalifont' }}>ExcaliDash</span>
|
||||||
|
<span className="text-[10px] font-bold text-red-500 mt-2" style={{ fontFamily: 'sans-serif' }}>BETA</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 min-w-0 overflow-y-auto">
|
<div className="flex-1 min-w-0 overflow-y-auto">
|
||||||
|
|||||||
@@ -60,12 +60,10 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// If status fails (backend down / schema mismatch), avoid locking the UI
|
// If status fails, default to auth-enabled mode to avoid exposing
|
||||||
// behind login. Backend still enforces auth when enabled.
|
// single-user UI paths accidentally. Backend remains the source of truth.
|
||||||
setAuthEnabled(false);
|
setAuthEnabled(true);
|
||||||
setBootstrapRequired(false);
|
setBootstrapRequired(false);
|
||||||
setUser(null);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const storedUser = localStorage.getItem(USER_KEY);
|
const storedUser = localStorage.getItem(USER_KEY);
|
||||||
|
|||||||
@@ -73,12 +73,14 @@ export const Dashboard: React.FC = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const listRequestVersionRef = useRef(0);
|
||||||
|
|
||||||
const { uploadFiles } = useUpload();
|
const { uploadFiles } = useUpload();
|
||||||
|
|
||||||
const hasMore = drawings.length < totalCount;
|
const hasMore = drawings.length < totalCount;
|
||||||
|
|
||||||
const refreshData = useCallback(async () => {
|
const refreshData = useCallback(async () => {
|
||||||
|
const requestVersion = ++listRequestVersionRef.current;
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
const [drawingsRes, collectionsData] = await Promise.all([
|
const [drawingsRes, collectionsData] = await Promise.all([
|
||||||
@@ -90,6 +92,7 @@ export const Dashboard: React.FC = () => {
|
|||||||
}),
|
}),
|
||||||
api.getCollections()
|
api.getCollections()
|
||||||
]);
|
]);
|
||||||
|
if (requestVersion !== listRequestVersionRef.current) return;
|
||||||
setDrawings(drawingsRes.drawings);
|
setDrawings(drawingsRes.drawings);
|
||||||
setTotalCount(drawingsRes.totalCount);
|
setTotalCount(drawingsRes.totalCount);
|
||||||
setCollections(collectionsData);
|
setCollections(collectionsData);
|
||||||
@@ -97,12 +100,15 @@ export const Dashboard: React.FC = () => {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to fetch data:', err);
|
console.error('Failed to fetch data:', err);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
if (requestVersion === listRequestVersionRef.current) {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [debouncedSearch, selectedCollectionId, sortConfig.field, sortConfig.direction]);
|
}, [debouncedSearch, selectedCollectionId, sortConfig.field, sortConfig.direction]);
|
||||||
|
|
||||||
const fetchMore = useCallback(async () => {
|
const fetchMore = useCallback(async () => {
|
||||||
if (isFetchingMore || !hasMore || isLoading) return;
|
if (isFetchingMore || !hasMore || isLoading) return;
|
||||||
|
const requestVersion = listRequestVersionRef.current;
|
||||||
setIsFetchingMore(true);
|
setIsFetchingMore(true);
|
||||||
try {
|
try {
|
||||||
const drawingsRes = await api.getDrawings(debouncedSearch, selectedCollectionId, {
|
const drawingsRes = await api.getDrawings(debouncedSearch, selectedCollectionId, {
|
||||||
@@ -111,7 +117,12 @@ export const Dashboard: React.FC = () => {
|
|||||||
sortField: sortConfig.field,
|
sortField: sortConfig.field,
|
||||||
sortDirection: sortConfig.direction,
|
sortDirection: sortConfig.direction,
|
||||||
});
|
});
|
||||||
setDrawings(prev => [...prev, ...drawingsRes.drawings]);
|
if (requestVersion !== listRequestVersionRef.current) return;
|
||||||
|
setDrawings(prev => {
|
||||||
|
const seen = new Set(prev.map((d) => d.id));
|
||||||
|
const nextPage = drawingsRes.drawings.filter((d) => !seen.has(d.id));
|
||||||
|
return [...prev, ...nextPage];
|
||||||
|
});
|
||||||
setTotalCount(drawingsRes.totalCount);
|
setTotalCount(drawingsRes.totalCount);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to fetch more data:', err);
|
console.error('Failed to fetch more data:', err);
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ export const Editor: React.FC = () => {
|
|||||||
const latestFilesRef = useRef<any>(null);
|
const latestFilesRef = useRef<any>(null);
|
||||||
const lastSyncedFilesRef = useRef<Record<string, any>>({});
|
const lastSyncedFilesRef = useRef<Record<string, any>>({});
|
||||||
const latestAppStateRef = useRef<any>(null);
|
const latestAppStateRef = useRef<any>(null);
|
||||||
const debouncedSaveRef = useRef<((elements: readonly any[], appState: any) => void) | null>(null);
|
const debouncedSaveRef = useRef<((drawingId: string, elements: readonly any[], appState: any) => void) | null>(null);
|
||||||
|
|
||||||
const emitFilesDeltaIfNeeded = useCallback(
|
const emitFilesDeltaIfNeeded = useCallback(
|
||||||
(nextFiles: Record<string, any>) => {
|
(nextFiles: Record<string, any>) => {
|
||||||
@@ -361,13 +361,13 @@ export const Editor: React.FC = () => {
|
|||||||
const didEmit = emitFilesDeltaIfNeeded(nextFiles);
|
const didEmit = emitFilesDeltaIfNeeded(nextFiles);
|
||||||
|
|
||||||
// Persist after file data becomes available so new tabs (tab3) load correctly.
|
// Persist after file data becomes available so new tabs (tab3) load correctly.
|
||||||
if (didEmit && latestAppStateRef.current && debouncedSaveRef.current) {
|
if (didEmit && id && latestAppStateRef.current && debouncedSaveRef.current) {
|
||||||
debouncedSaveRef.current(latestElementsRef.current, latestAppStateRef.current);
|
debouncedSaveRef.current(id, latestElementsRef.current, latestAppStateRef.current);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
setIsReady(true);
|
setIsReady(true);
|
||||||
}, [emitFilesDeltaIfNeeded]);
|
}, [emitFilesDeltaIfNeeded, id]);
|
||||||
|
|
||||||
// Handle #addLibrary URL hash parameter for importing libraries from links
|
// Handle #addLibrary URL hash parameter for importing libraries from links
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -428,12 +428,12 @@ export const Editor: React.FC = () => {
|
|||||||
scrollToContent: true,
|
scrollToContent: true,
|
||||||
}), []);
|
}), []);
|
||||||
|
|
||||||
const saveDataRef = useRef<((elements: readonly any[], appState: any) => Promise<void>) | null>(null);
|
const saveDataRef = useRef<((drawingId: string, elements: readonly any[], appState: any) => Promise<void>) | null>(null);
|
||||||
const savePreviewRef = useRef<((elements: readonly any[], appState: any, files: any) => Promise<void>) | null>(null);
|
const savePreviewRef = useRef<((drawingId: string, elements: readonly any[], appState: any, files: any) => Promise<void>) | null>(null);
|
||||||
const saveLibraryRef = useRef<((items: any[]) => Promise<void>) | null>(null);
|
const saveLibraryRef = useRef<((items: any[]) => Promise<void>) | null>(null);
|
||||||
|
|
||||||
saveDataRef.current = async (elements: readonly any[], appState: any) => {
|
saveDataRef.current = async (drawingId: string, elements: readonly any[], appState: any) => {
|
||||||
if (!id) return;
|
if (!drawingId) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const persistableAppState = {
|
const persistableAppState = {
|
||||||
@@ -446,27 +446,27 @@ export const Editor: React.FC = () => {
|
|||||||
const persistableElements = Array.isArray(snapshot) ? snapshot : [];
|
const persistableElements = Array.isArray(snapshot) ? snapshot : [];
|
||||||
|
|
||||||
console.log("[Editor] Saving drawing", {
|
console.log("[Editor] Saving drawing", {
|
||||||
drawingId: id,
|
drawingId,
|
||||||
elementCount: persistableElements.length,
|
elementCount: persistableElements.length,
|
||||||
hasRenderableElements: persistableElements.some((el: any) => !el?.isDeleted),
|
hasRenderableElements: persistableElements.some((el: any) => !el?.isDeleted),
|
||||||
appState: persistableAppState,
|
appState: persistableAppState,
|
||||||
});
|
});
|
||||||
|
|
||||||
await api.updateDrawing(id, {
|
await api.updateDrawing(drawingId, {
|
||||||
elements: persistableElements,
|
elements: persistableElements,
|
||||||
appState: persistableAppState,
|
appState: persistableAppState,
|
||||||
files: latestFilesRef.current || {},
|
files: latestFilesRef.current || {},
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("[Editor] Save complete", { drawingId: id });
|
console.log("[Editor] Save complete", { drawingId });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to save drawing', err);
|
console.error('Failed to save drawing', err);
|
||||||
toast.error("Failed to save changes");
|
toast.error("Failed to save changes");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
savePreviewRef.current = async (elements: readonly any[], appState: any, files: any) => {
|
savePreviewRef.current = async (drawingId: string, elements: readonly any[], appState: any, files: any) => {
|
||||||
if (!id) return;
|
if (!drawingId) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const currentSnapshot = latestElementsRef.current ?? elements;
|
const currentSnapshot = latestElementsRef.current ?? elements;
|
||||||
@@ -484,13 +484,13 @@ export const Editor: React.FC = () => {
|
|||||||
const preview = svg.outerHTML;
|
const preview = svg.outerHTML;
|
||||||
|
|
||||||
console.log("[Editor] Saving preview", {
|
console.log("[Editor] Saving preview", {
|
||||||
drawingId: id,
|
drawingId,
|
||||||
elementCount: currentSnapshot.length,
|
elementCount: currentSnapshot.length,
|
||||||
});
|
});
|
||||||
|
|
||||||
await api.updateDrawing(id, { preview });
|
await api.updateDrawing(drawingId, { preview });
|
||||||
|
|
||||||
console.log("[Editor] Preview save complete", { drawingId: id });
|
console.log("[Editor] Preview save complete", { drawingId });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to save preview', err);
|
console.error('Failed to save preview', err);
|
||||||
}
|
}
|
||||||
@@ -509,9 +509,9 @@ export const Editor: React.FC = () => {
|
|||||||
|
|
||||||
|
|
||||||
const debouncedSave = useCallback(
|
const debouncedSave = useCallback(
|
||||||
debounce((elements, appState) => {
|
debounce((drawingId, elements, appState) => {
|
||||||
if (saveDataRef.current) {
|
if (saveDataRef.current) {
|
||||||
saveDataRef.current(elements, appState);
|
saveDataRef.current(drawingId, elements, appState);
|
||||||
}
|
}
|
||||||
}, 1000),
|
}, 1000),
|
||||||
[] // Empty dependency array = Stable across renders
|
[] // Empty dependency array = Stable across renders
|
||||||
@@ -519,9 +519,9 @@ export const Editor: React.FC = () => {
|
|||||||
// Allow non-hook code (e.g., Excalidraw API wrappers) to trigger debounced saves.
|
// Allow non-hook code (e.g., Excalidraw API wrappers) to trigger debounced saves.
|
||||||
debouncedSaveRef.current = debouncedSave;
|
debouncedSaveRef.current = debouncedSave;
|
||||||
const debouncedSavePreview = useCallback(
|
const debouncedSavePreview = useCallback(
|
||||||
debounce((elements, appState, files) => {
|
debounce((drawingId, elements, appState, files) => {
|
||||||
if (savePreviewRef.current) {
|
if (savePreviewRef.current) {
|
||||||
savePreviewRef.current(elements, appState, files);
|
savePreviewRef.current(drawingId, elements, appState, files);
|
||||||
}
|
}
|
||||||
}, 10000),
|
}, 10000),
|
||||||
[]
|
[]
|
||||||
@@ -536,6 +536,13 @@ export const Editor: React.FC = () => {
|
|||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
debouncedSave.cancel();
|
||||||
|
debouncedSavePreview.cancel();
|
||||||
|
};
|
||||||
|
}, [debouncedSave, debouncedSavePreview]);
|
||||||
|
|
||||||
const broadcastChanges = useCallback(
|
const broadcastChanges = useCallback(
|
||||||
throttle((elements: readonly any[], currentFiles?: Record<string, any>) => {
|
throttle((elements: readonly any[], currentFiles?: Record<string, any>) => {
|
||||||
if (!socketRef.current || !id) return;
|
if (!socketRef.current || !id) return;
|
||||||
@@ -670,8 +677,9 @@ export const Editor: React.FC = () => {
|
|||||||
const files = excalidrawAPI.current.getFiles() || {};
|
const files = excalidrawAPI.current.getFiles() || {};
|
||||||
latestElementsRef.current = elements;
|
latestElementsRef.current = elements;
|
||||||
latestFilesRef.current = files;
|
latestFilesRef.current = files;
|
||||||
await saveDataRef.current(elements, appState);
|
if (!id) return;
|
||||||
savePreviewRef.current(elements, appState, files);
|
await saveDataRef.current(id, elements, appState);
|
||||||
|
savePreviewRef.current(id, elements, appState, files);
|
||||||
toast.success("Saved changes to server");
|
toast.success("Saved changes to server");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -739,7 +747,9 @@ export const Editor: React.FC = () => {
|
|||||||
elementCount: allElements.length,
|
elementCount: allElements.length,
|
||||||
hasRenderableElements,
|
hasRenderableElements,
|
||||||
});
|
});
|
||||||
debouncedSave(allElements, appState);
|
if (id) {
|
||||||
|
debouncedSave(id, allElements, appState);
|
||||||
|
}
|
||||||
|
|
||||||
// Trigger Slow Preview Gen
|
// Trigger Slow Preview Gen
|
||||||
const filesSnapshot = currentFiles;
|
const filesSnapshot = currentFiles;
|
||||||
@@ -748,8 +758,10 @@ export const Editor: React.FC = () => {
|
|||||||
drawingId: id,
|
drawingId: id,
|
||||||
fileCount: Object.keys(filesSnapshot).length,
|
fileCount: Object.keys(filesSnapshot).length,
|
||||||
});
|
});
|
||||||
debouncedSavePreview(allElements, appState, filesSnapshot);
|
if (id) {
|
||||||
}, [debouncedSave, debouncedSavePreview, broadcastChanges]);
|
debouncedSavePreview(id, allElements, appState, filesSnapshot);
|
||||||
|
}
|
||||||
|
}, [debouncedSave, debouncedSavePreview, broadcastChanges, id]);
|
||||||
|
|
||||||
// Ensure file-only updates (e.g. pasted image dataURL arriving asynchronously)
|
// Ensure file-only updates (e.g. pasted image dataURL arriving asynchronously)
|
||||||
// are still broadcast to collaborators AND persisted to the server.
|
// are still broadcast to collaborators AND persisted to the server.
|
||||||
@@ -767,7 +779,7 @@ export const Editor: React.FC = () => {
|
|||||||
|
|
||||||
// Persist after file data becomes available (covers the "tab 3" case).
|
// Persist after file data becomes available (covers the "tab 3" case).
|
||||||
if (didEmit && latestAppStateRef.current && debouncedSaveRef.current) {
|
if (didEmit && latestAppStateRef.current && debouncedSaveRef.current) {
|
||||||
debouncedSaveRef.current(latestElementsRef.current, latestAppStateRef.current);
|
debouncedSaveRef.current(id, latestElementsRef.current, latestAppStateRef.current);
|
||||||
}
|
}
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
@@ -803,6 +815,7 @@ export const Editor: React.FC = () => {
|
|||||||
// Save drawing and generate preview before navigating
|
// Save drawing and generate preview before navigating
|
||||||
try {
|
try {
|
||||||
if (excalidrawAPI.current && saveDataRef.current && savePreviewRef.current) {
|
if (excalidrawAPI.current && saveDataRef.current && savePreviewRef.current) {
|
||||||
|
if (!id) return;
|
||||||
const elements = excalidrawAPI.current.getSceneElementsIncludingDeleted();
|
const elements = excalidrawAPI.current.getSceneElementsIncludingDeleted();
|
||||||
const appState = excalidrawAPI.current.getAppState();
|
const appState = excalidrawAPI.current.getAppState();
|
||||||
const files = excalidrawAPI.current.getFiles() || {};
|
const files = excalidrawAPI.current.getFiles() || {};
|
||||||
@@ -810,8 +823,8 @@ export const Editor: React.FC = () => {
|
|||||||
latestFilesRef.current = files;
|
latestFilesRef.current = files;
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
saveDataRef.current(elements, appState),
|
saveDataRef.current(id, elements, appState),
|
||||||
savePreviewRef.current(elements, appState, files)
|
savePreviewRef.current(id, elements, appState, files)
|
||||||
]);
|
]);
|
||||||
console.log("[Editor] Saved on back navigation", { drawingId: id });
|
console.log("[Editor] Saved on back navigation", { drawingId: id });
|
||||||
}
|
}
|
||||||
@@ -827,7 +840,7 @@ export const Editor: React.FC = () => {
|
|||||||
<div className="h-screen flex flex-col bg-white dark:bg-neutral-950 overflow-hidden">
|
<div className="h-screen flex flex-col bg-white dark:bg-neutral-950 overflow-hidden">
|
||||||
<header
|
<header
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"h-14 bg-white dark:bg-neutral-900 border-b border-gray-200 dark:border-neutral-800 flex items-center px-4 justify-between z-10 fixed top-0 left-0 right-0 transition-transform duration-300",
|
"h-16 bg-white dark:bg-neutral-900 border-b border-gray-200 dark:border-neutral-800 flex items-center px-4 justify-between z-10 fixed top-0 left-0 right-0 transition-transform duration-300",
|
||||||
isHeaderVisible ? "translate-y-0" : "-translate-y-full"
|
isHeaderVisible ? "translate-y-0" : "-translate-y-full"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -945,8 +958,8 @@ export const Editor: React.FC = () => {
|
|||||||
<div
|
<div
|
||||||
className="flex-1 w-full relative transition-all duration-300"
|
className="flex-1 w-full relative transition-all duration-300"
|
||||||
style={{
|
style={{
|
||||||
height: isHeaderVisible ? 'calc(100vh - 3.5rem)' : '100vh',
|
height: isHeaderVisible ? 'calc(100vh - 4rem)' : '100vh',
|
||||||
marginTop: isHeaderVisible ? '3.5rem' : '0'
|
marginTop: isHeaderVisible ? '4rem' : '0'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{loadError ? (
|
{loadError ? (
|
||||||
|
|||||||
Reference in New Issue
Block a user