Replace Firebase Storage with MinIO and add user account features

- Storage Integration:
  * Remove Firebase Storage dependency and useFirebaseStorage composable
  * Implement direct MinIO uploads via POST /storage/upload with multipart/form-data
  * Upload canvas JSON, preview PNG, and production PNG as separate objects
  * Store public URLs and metadata in design records

- Authentication & Registration:
  * Add email/password registration page with validation
  * Integrate backend user session via /auth/login endpoint
  * Store backendUser.id as ownerId in design records
  * Auto-sync backend session on Firebase auth state changes

- User Account Pages:
  * Create profile page showing user details and backend session info
  * Create orders page with transaction history filtered by customerEmail
  * Add server proxy /api/orders to forward GET /transactions queries

- Navigation Improvements:
  * Replace inline auth buttons with avatar dropdown menu
  * Add Profile, Orders, and Logout options to dropdown
  * Implement outside-click and route-change handlers for dropdown
  * Display user initials in avatar badge

- API Updates:
  * Update transactions endpoint to accept amount as string
  * Format amount with .toFixed(2) in checkout success flow
  * Query orders by customerEmail instead of ownerId for consistency
This commit is contained in:
Frank John Begornia
2025-11-16 01:19:35 +08:00
parent 0ff41822af
commit bf701f8342
19 changed files with 1807 additions and 223 deletions

View File

@@ -13,15 +13,6 @@ export interface SlipmatTemplate {
backgroundColor: string;
}
export interface ExportedDesign {
previewUrl: string;
previewBlob: Blob;
productionUrl: string;
productionBlob: Blob;
templateId: string;
createdAt: string;
}
const DISPLAY_SIZE = 720;
const PREVIEW_SIZE = 1024;
const MIN_ZOOM = 0.5;
@@ -59,6 +50,7 @@ const TEMPLATE_PRESETS: SlipmatTemplate[] = [
];
type FabricCanvas = FabricNamespace.Canvas;
type FabricSerializedCanvas = ReturnType<FabricCanvas["toJSON"]>;
type FabricCircle = FabricNamespace.Circle;
type FabricRect = FabricNamespace.Rect;
type FabricTextbox = FabricNamespace.Textbox;
@@ -69,6 +61,16 @@ type CanvasReadyPayload = {
fabric: FabricModule;
};
export interface ExportedDesign {
previewUrl: string;
previewBlob: Blob;
productionUrl: string;
productionBlob: Blob;
templateId: string;
createdAt: string;
canvasJson: FabricSerializedCanvas;
}
const FALLBACK_TEMPLATE: SlipmatTemplate = TEMPLATE_PRESETS[0] ?? {
id: "custom",
name: "Custom",
@@ -605,6 +607,77 @@ export const useSlipmatDesigner = () => {
}
};
const waitForCanvasReady = async (): Promise<FabricCanvas> => {
if (canvas.value) {
return canvas.value;
}
await new Promise<void>((resolve) => {
const stop = watch(
canvas,
(value) => {
if (value) {
stop();
resolve();
}
},
{ immediate: false }
);
});
if (!canvas.value) {
throw new Error("Canvas not ready");
}
return canvas.value;
};
const loadDesign = async (payload: {
templateId?: string | null;
canvasJson: string | FabricSerializedCanvas;
previewUrl?: string | null;
}) => {
if (payload.templateId) {
selectTemplate(payload.templateId);
}
const currentCanvas = await waitForCanvasReady();
if (!fabricApi.value) {
throw new Error("Fabric API not ready");
}
const parsedJson =
typeof payload.canvasJson === "string"
? (JSON.parse(payload.canvasJson) as FabricSerializedCanvas)
: payload.canvasJson;
await new Promise<void>((resolve, reject) => {
currentCanvas.loadFromJSON(parsedJson, () => {
maintainStaticLayerOrder();
updateSelectedStyleState();
currentCanvas.renderAll();
schedulePreviewRefresh();
resolve();
});
}).catch((error) => {
throw error;
});
// Reset cached assets; caller can provide preview if available.
previewBlob.value = null;
productionBlob.value = null;
if (productionObjectUrl.value) {
URL.revokeObjectURL(productionObjectUrl.value);
productionObjectUrl.value = null;
}
if (payload.previewUrl) {
previewUrl.value = payload.previewUrl;
} else {
await refreshPreview();
}
};
const exportDesign = async (): Promise<ExportedDesign | null> => {
const currentCanvas = canvas.value;
if (!currentCanvas) {
@@ -629,6 +702,8 @@ export const useSlipmatDesigner = () => {
});
const productionDataBlob = await dataUrlToBlob(productionDataUrl);
const canvasJson = currentCanvas.toJSON();
previewUrl.value = previewDataUrl;
previewBlob.value = previewDataBlob;
productionBlob.value = productionDataBlob;
@@ -645,6 +720,7 @@ export const useSlipmatDesigner = () => {
productionBlob: productionDataBlob,
templateId: selectedTemplate.value.id,
createdAt: new Date().toISOString(),
canvasJson,
};
} finally {
isExporting.value = false;
@@ -689,6 +765,7 @@ export const useSlipmatDesigner = () => {
templates,
selectedTemplate,
selectTemplate,
loadDesign,
displaySize,
productionPixelSize,
templateLabel,
@@ -697,9 +774,9 @@ export const useSlipmatDesigner = () => {
productionBlob,
productionObjectUrl,
isExporting,
activeFillColor,
activeStrokeColor,
canStyleSelection,
activeFillColor,
activeStrokeColor,
canStyleSelection,
zoomLevel,
zoomPercent,
minZoom: MIN_ZOOM,
@@ -709,8 +786,8 @@ export const useSlipmatDesigner = () => {
addTextbox,
addShape,
addImageFromFile,
setActiveFillColor,
setActiveStrokeColor,
setActiveFillColor,
setActiveStrokeColor,
setZoom,
zoomIn,
zoomOut,