+
+
+ {sidebarOpen && (
+
setSidebarOpen(false)}
+ />
+ )}
+
+
+ setSidebarOpen(false)} />
+
+
+
+ setSidebarOpen(!sidebarOpen)} />
+
+
+
+
+
+ Éditeur d'images
+
+
+ Ajoutez des rubans promotionnels, badges prix et filtres à vos images
+
+
+
+
+ {/* Left Column - Controls */}
+
+ {/* Image Selection */}
+
+
+
+
+ Sélectionner une image
+
+
+
+ {mediaList && (mediaList as any[]).filter((m: any) => m.type === 'image').length > 0 ? (
+
+ {(mediaList as any[])
+ .filter((m: any) => m.type === 'image')
+ .map((media: any) => (
+
setSelectedMedia(media)}
+ data-testid={`select-image-${media.id}`}
+ >
+

+
+ ))}
+
+ ) : (
+
+ Aucune image disponible. Uploadez des images dans la médiathèque.
+
+ )}
+
+
+
+ {/* Ribbon Controls */}
+
+
+ Ruban promotionnel
+
+
+
+ setRibbon({ ...ribbon, enabled: checked as boolean })}
+ data-testid="checkbox-ribbon"
+ />
+
+
+
+ {ribbon.enabled && (
+ <>
+
+
+
setRibbon({ ...ribbon, text: value as any })}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
setRibbon({ ...ribbon, color: value as any })}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ )}
+
+
+
+ {/* Price Badge */}
+
+
+ Badge prix
+
+
+
+ setPriceBadge({ ...priceBadge, enabled: checked as boolean })}
+ data-testid="checkbox-price"
+ />
+
+
+
+ {priceBadge.enabled && (
+ <>
+
+
+ setPriceBadge({ ...priceBadge, price: e.target.value })}
+ data-testid="input-price"
+ />
+
+
+
+
+
setPriceBadge({ ...priceBadge, color: value as any })}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {priceBadge.size}px
+
+
setPriceBadge({ ...priceBadge, size: value[0] })}
+ min={12}
+ max={70}
+ step={1}
+ data-testid="slider-price-size"
+ />
+
+ >
+ )}
+
+
+
+ {/* Filters */}
+
+
+ Filtres
+
+
+
+
+
+ {filters.brightness}
+
+
setFilters({ ...filters, brightness: value })}
+ min={-50}
+ max={50}
+ step={5}
+ data-testid="slider-brightness"
+ />
+
+
+
+
+
+ {filters.contrast}
+
+
setFilters({ ...filters, contrast: value })}
+ min={-50}
+ max={50}
+ step={5}
+ data-testid="slider-contrast"
+ />
+
+
+
+
+
+ {filters.saturation}
+
+
setFilters({ ...filters, saturation: value })}
+ min={-100}
+ max={100}
+ step={10}
+ data-testid="slider-saturation"
+ />
+
+
+
+
+
+
+
+
+
+
+ {/* Right Column - Preview */}
+
+
+
+ Aperçu
+
+
+ {selectedMedia ? (
+
+
+
+

+
+ {/* CSS Triangle Ribbon Overlay - Auto-centered */}
+ {ribbon.enabled && ribbon.text && (
+
+ )}
+
+ {/* Price Badge Overlay (CSS) */}
+ {priceBadge.enabled && priceBadge.price && (
+
+ €{priceBadge.price}
+
+ )}
+
+
+
+
+
+
+
+
+
+
+ ) : (
+
+
Sélectionnez une image
+
+ )}
+
+
+
+
+
+
+
+ );
+}
diff --git a/package-lock.json b/package-lock.json
index b32a42f..45fd703 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -48,6 +48,7 @@
"@types/node-cron": "^3.0.11",
"@types/pg": "^8.15.5",
"bcrypt": "^6.0.0",
+ "canvas": "^3.2.0",
"class-variance-authority": "^0.7.1",
"cloudinary": "^2.7.0",
"clsx": "^2.1.1",
@@ -60,6 +61,8 @@
"express": "^4.21.2",
"express-session": "^1.18.1",
"framer-motion": "^11.13.1",
+ "html-to-image": "^1.11.13",
+ "html2canvas": "^1.4.1",
"input-otp": "^1.4.2",
"lucide-react": "^0.453.0",
"memorystore": "^1.6.7",
@@ -4375,6 +4378,35 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"license": "MIT"
},
+ "node_modules/base64-arraybuffer": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
+ "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6.0"
+ }
+ },
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
"node_modules/bcrypt": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz",
@@ -4401,6 +4433,17 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/bl": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
+ "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
+ "license": "MIT",
+ "dependencies": {
+ "buffer": "^5.5.0",
+ "inherits": "^2.0.4",
+ "readable-stream": "^3.4.0"
+ }
+ },
"node_modules/body-parser": {
"version": "1.20.3",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
@@ -4494,6 +4537,30 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
+ "node_modules/buffer": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+ "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.1.13"
+ }
+ },
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
@@ -4583,6 +4650,26 @@
],
"license": "CC-BY-4.0"
},
+ "node_modules/canvas": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/canvas/-/canvas-3.2.0.tgz",
+ "integrity": "sha512-jk0GxrLtUEmW/TmFsk2WghvgHe8B0pxGilqCL21y8lHkPUGa6FTsnCNtHPOzT8O3y+N+m3espawV80bbBlgfTA==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "dependencies": {
+ "node-addon-api": "^7.0.0",
+ "prebuild-install": "^7.1.3"
+ },
+ "engines": {
+ "node": "^18.12.0 || >= 20.9.0"
+ }
+ },
+ "node_modules/canvas/node_modules/node-addon-api": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
+ "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
+ "license": "MIT"
+ },
"node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
@@ -4619,6 +4706,12 @@
"node": ">= 6"
}
},
+ "node_modules/chownr": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
+ "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
+ "license": "ISC"
+ },
"node_modules/class-variance-authority": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
@@ -4777,6 +4870,15 @@
"node": ">= 8"
}
},
+ "node_modules/css-line-break": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
+ "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
+ "license": "MIT",
+ "dependencies": {
+ "utrie": "^1.0.2"
+ }
+ },
"node_modules/cssesc": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
@@ -4948,6 +5050,30 @@
"integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
"license": "MIT"
},
+ "node_modules/decompress-response": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
+ "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
+ "license": "MIT",
+ "dependencies": {
+ "mimic-response": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/deep-extend": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
+ "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
"node_modules/define-data-property": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
@@ -5229,6 +5355,15 @@
"node": ">= 0.8"
}
},
+ "node_modules/end-of-stream": {
+ "version": "1.4.5",
+ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
+ "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
+ "license": "MIT",
+ "dependencies": {
+ "once": "^1.4.0"
+ }
+ },
"node_modules/enhanced-resolve": {
"version": "5.18.1",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz",
@@ -5349,6 +5484,15 @@
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
"license": "MIT"
},
+ "node_modules/expand-template": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
+ "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
+ "license": "(MIT OR WTFPL)",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/express": {
"version": "4.21.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
@@ -5636,6 +5780,12 @@
"node": ">= 0.6"
}
},
+ "node_modules/fs-constants": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
+ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
+ "license": "MIT"
+ },
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -5709,6 +5859,12 @@
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
}
},
+ "node_modules/github-from-package": {
+ "version": "0.0.0",
+ "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
+ "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
+ "license": "MIT"
+ },
"node_modules/glob": {
"version": "10.4.5",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
@@ -5807,6 +5963,25 @@
"node": ">= 0.4"
}
},
+ "node_modules/html-to-image": {
+ "version": "1.11.13",
+ "resolved": "https://registry.npmjs.org/html-to-image/-/html-to-image-1.11.13.tgz",
+ "integrity": "sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==",
+ "license": "MIT"
+ },
+ "node_modules/html2canvas": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
+ "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
+ "license": "MIT",
+ "dependencies": {
+ "css-line-break": "^2.1.0",
+ "text-segmentation": "^1.0.3"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
"node_modules/http-errors": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
@@ -5835,12 +6010,38 @@
"node": ">=0.10.0"
}
},
+ "node_modules/ieee754": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
+ "node_modules/ini": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
+ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
+ "license": "ISC"
+ },
"node_modules/input-otp": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/input-otp/-/input-otp-1.4.2.tgz",
@@ -6418,6 +6619,18 @@
"node": ">= 0.6"
}
},
+ "node_modules/mimic-response": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
+ "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
@@ -6469,6 +6682,12 @@
"mkdirp": "bin/cmd.js"
}
},
+ "node_modules/mkdirp-classic": {
+ "version": "0.5.3",
+ "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
+ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
+ "license": "MIT"
+ },
"node_modules/modern-screenshot": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/modern-screenshot/-/modern-screenshot-4.6.0.tgz",
@@ -6537,6 +6756,12 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
+ "node_modules/napi-build-utils": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
+ "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
+ "license": "MIT"
+ },
"node_modules/negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
@@ -6555,6 +6780,30 @@
"react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc"
}
},
+ "node_modules/node-abi": {
+ "version": "3.78.0",
+ "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.78.0.tgz",
+ "integrity": "sha512-E2wEyrgX/CqvicaQYU3Ze1PFGjc4QYPGsjUrlYkqAE0WjHEZwgOsGMPMzkMse4LjJbDmaEuDX3CM036j5K2DSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.3.5"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/node-abi/node_modules/semver": {
+ "version": "7.7.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
+ "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/node-addon-api": {
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz",
@@ -6667,6 +6916,15 @@
"node": ">= 0.8"
}
},
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "license": "ISC",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
"node_modules/package-json-from-dist": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
@@ -7089,6 +7347,32 @@
"integrity": "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==",
"license": "MIT"
},
+ "node_modules/prebuild-install": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
+ "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
+ "license": "MIT",
+ "dependencies": {
+ "detect-libc": "^2.0.0",
+ "expand-template": "^2.0.3",
+ "github-from-package": "0.0.0",
+ "minimist": "^1.2.3",
+ "mkdirp-classic": "^0.5.3",
+ "napi-build-utils": "^2.0.0",
+ "node-abi": "^3.3.0",
+ "pump": "^3.0.0",
+ "rc": "^1.2.7",
+ "simple-get": "^4.0.0",
+ "tar-fs": "^2.0.0",
+ "tunnel-agent": "^0.6.0"
+ },
+ "bin": {
+ "prebuild-install": "bin.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
@@ -7123,6 +7407,16 @@
"integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==",
"license": "ISC"
},
+ "node_modules/pump": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
+ "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
+ "license": "MIT",
+ "dependencies": {
+ "end-of-stream": "^1.1.0",
+ "once": "^1.3.1"
+ }
+ },
"node_modules/q": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
@@ -7202,6 +7496,21 @@
"node": ">= 0.8"
}
},
+ "node_modules/rc": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
+ "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
+ "license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
+ "dependencies": {
+ "deep-extend": "^0.6.0",
+ "ini": "~1.3.0",
+ "minimist": "^1.2.0",
+ "strip-json-comments": "~2.0.1"
+ },
+ "bin": {
+ "rc": "cli.js"
+ }
+ },
"node_modules/react": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
@@ -7815,6 +8124,51 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/simple-concat": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
+ "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/simple-get": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
+ "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "decompress-response": "^6.0.0",
+ "once": "^1.3.1",
+ "simple-concat": "^1.0.0"
+ }
+ },
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@@ -7976,6 +8330,15 @@
"node": ">=8"
}
},
+ "node_modules/strip-json-comments": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
+ "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/sucrase": {
"version": "3.35.0",
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
@@ -8072,6 +8435,43 @@
"node": ">=6"
}
},
+ "node_modules/tar-fs": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz",
+ "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "chownr": "^1.1.1",
+ "mkdirp-classic": "^0.5.2",
+ "pump": "^3.0.0",
+ "tar-stream": "^2.1.4"
+ }
+ },
+ "node_modules/tar-stream": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
+ "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "bl": "^4.0.3",
+ "end-of-stream": "^1.4.1",
+ "fs-constants": "^1.0.0",
+ "inherits": "^2.0.3",
+ "readable-stream": "^3.1.1"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/text-segmentation": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
+ "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
+ "license": "MIT",
+ "dependencies": {
+ "utrie": "^1.0.2"
+ }
+ },
"node_modules/thenify": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
@@ -8152,6 +8552,18 @@
"fsevents": "~2.3.3"
}
},
+ "node_modules/tunnel-agent": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
+ "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "safe-buffer": "^5.0.1"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/tw-animate-css": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.2.5.tgz",
@@ -8316,6 +8728,15 @@
"node": ">= 0.4.0"
}
},
+ "node_modules/utrie": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
+ "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
+ "license": "MIT",
+ "dependencies": {
+ "base64-arraybuffer": "^1.0.2"
+ }
+ },
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
@@ -8969,6 +9390,12 @@
"node": ">=8"
}
},
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "license": "ISC"
+ },
"node_modules/ws": {
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
diff --git a/package.json b/package.json
index 81dab0c..e688d05 100644
--- a/package.json
+++ b/package.json
@@ -50,6 +50,7 @@
"@types/node-cron": "^3.0.11",
"@types/pg": "^8.15.5",
"bcrypt": "^6.0.0",
+ "canvas": "^3.2.0",
"class-variance-authority": "^0.7.1",
"cloudinary": "^2.7.0",
"clsx": "^2.1.1",
@@ -62,6 +63,8 @@
"express": "^4.21.2",
"express-session": "^1.18.1",
"framer-motion": "^11.13.1",
+ "html-to-image": "^1.11.13",
+ "html2canvas": "^1.4.1",
"input-otp": "^1.4.2",
"lucide-react": "^0.453.0",
"memorystore": "^1.6.7",
diff --git a/server/routes.ts b/server/routes.ts
index 9a666f2..2edb427 100644
--- a/server/routes.ts
+++ b/server/routes.ts
@@ -517,6 +517,196 @@ export async function registerRoutes(app: Express): Promise
{
}
});
+ // Apply overlays to image using Sharp (server-side processing)
+ // IMPORTANT: This must be BEFORE /api/media/:id to avoid being intercepted by :id parameter
+ app.post("/api/media/apply-overlays", requireAuth, async (req, res) => {
+ try {
+ const { imageUrl, ribbon, priceBadge } = req.body;
+ const user = req.user as User;
+ const userId = user.id;
+
+ if (!imageUrl) {
+ return res.status(400).json({ error: "Image URL required" });
+ }
+
+ console.log("🎨 Applying overlays server-side with Sharp...");
+ console.log("📥 Image URL:", imageUrl);
+ console.log("🎀 Ribbon:", ribbon);
+ console.log("💰 Price Badge:", priceBadge);
+
+ // Download image from URL
+ const imageResponse = await fetch(imageUrl);
+ const imageBuffer = Buffer.from(await imageResponse.arrayBuffer());
+ console.log("✅ Image downloaded:", imageBuffer.length, "bytes");
+
+ // Load with Sharp to process
+ const sharp = (await import("sharp")).default;
+ let image = sharp(imageBuffer);
+
+ // Get image metadata for dimensions
+ const metadata = await image.metadata();
+ const width = metadata.width!;
+ const height = metadata.height!;
+
+ console.log(`📐 Image dimensions: ${width}x${height}`);
+
+ // Create SVG overlays
+ const overlays: any[] = [];
+
+ // Add ribbon overlay
+ if (ribbon) {
+ const ribbonSize = 150;
+ const color = ribbon.color === 'red' ? '#dc2626' : '#eab308';
+ const fontSize = ribbon.text.length <= 5 ? 22 : ribbon.text.length <= 8 ? 18 : ribbon.text.length <= 11 ? 15 : 12;
+
+ // Position mapping for each corner
+ const positions: Record = {
+ north_west: {
+ x: 0,
+ y: 0,
+ polygon: `0,0 ${ribbonSize},0 0,${ribbonSize}`, // Top-left triangle
+ textX: ribbonSize * 0.3,
+ textY: ribbonSize * 0.3,
+ textRotation: -45
+ },
+ north_east: {
+ x: width - ribbonSize,
+ y: 0,
+ polygon: `0,0 ${ribbonSize},0 ${ribbonSize},${ribbonSize}`, // Top-right triangle
+ textX: ribbonSize * 0.7,
+ textY: ribbonSize * 0.3,
+ textRotation: 45
+ },
+ south_west: {
+ x: 0,
+ y: height - ribbonSize,
+ polygon: `0,0 0,${ribbonSize} ${ribbonSize},${ribbonSize}`, // Bottom-left triangle
+ textX: ribbonSize * 0.3,
+ textY: ribbonSize * 0.7,
+ textRotation: -135
+ },
+ south_east: {
+ x: width - ribbonSize,
+ y: height - ribbonSize,
+ polygon: `${ribbonSize},0 0,${ribbonSize} ${ribbonSize},${ribbonSize}`, // Bottom-right triangle
+ textX: ribbonSize * 0.7,
+ textY: ribbonSize * 0.7,
+ textRotation: 135
+ }
+ };
+
+ const pos = positions[ribbon.position] || positions.north_west;
+
+ const ribbonSvg = `
+
+ `;
+
+ overlays.push({
+ input: Buffer.from(ribbonSvg),
+ top: pos.y,
+ left: pos.x
+ });
+ }
+
+ // Add price badge overlay
+ if (priceBadge) {
+ const padding = 16;
+ const badgeText = `€${priceBadge.price}`;
+ const fontSize = priceBadge.size;
+ const textWidth = badgeText.length * fontSize * 0.6; // Approximate
+ const badgeWidth = textWidth + padding * 2;
+ const badgeHeight = fontSize + padding;
+ const color = priceBadge.color === 'red' ? '#dc2626' : '#eab308';
+
+ let x = padding;
+ let y = padding;
+
+ if (priceBadge.position === 'north_east') {
+ x = width - badgeWidth - padding;
+ } else if (priceBadge.position === 'south_west') {
+ y = height - badgeHeight - padding;
+ } else if (priceBadge.position === 'south_east') {
+ x = width - badgeWidth - padding;
+ y = height - badgeHeight - padding;
+ }
+
+ const badgeSvg = `
+
+ `;
+
+ overlays.push({
+ input: Buffer.from(badgeSvg),
+ top: y,
+ left: x
+ });
+ }
+
+ // Apply all overlays
+ if (overlays.length > 0) {
+ image = image.composite(overlays);
+ }
+
+ // Convert to buffer
+ const outputBuffer = await image.jpeg({ quality: 95 }).toBuffer();
+
+ console.log("✅ Overlays applied, uploading to Cloudinary...");
+
+ // Check if Cloudinary is configured
+ const cloudinaryConfig = await storage.getCloudinaryConfig(userId);
+ if (!cloudinaryConfig) {
+ return res.status(400).json({
+ error: "Cloudinary not configured. Please configure Cloudinary in Settings first."
+ });
+ }
+
+ // Upload to Cloudinary
+ const uploadResult = await cloudinaryService.uploadMedia(
+ outputBuffer,
+ `edited_${Date.now()}.jpg`,
+ userId,
+ 'image/jpeg'
+ );
+
+ // Save to database
+ // IMPORTANT: For edited images with overlays, use originalUrl for ALL formats
+ // because Cloudinary transformations would recreate versions WITHOUT the overlays
+ const mediaItem = await storage.createMedia({
+ userId,
+ type: "image",
+ cloudinaryPublicId: uploadResult.publicId,
+ originalUrl: uploadResult.originalUrl,
+ facebookFeedUrl: uploadResult.originalUrl, // Use original with overlays
+ instagramFeedUrl: uploadResult.originalUrl, // Use original with overlays
+ instagramStoryUrl: uploadResult.originalUrl, // Use original with overlays
+ fileName: `edited_${Date.now()}.jpg`,
+ fileSize: outputBuffer.length,
+ });
+
+ console.log("✅ Image saved with ID:", mediaItem.id);
+ res.json(mediaItem);
+ } catch (error) {
+ console.error("💥 Error applying overlays:", error);
+ const errorMessage = error instanceof Error ? error.message : "Failed to apply overlays";
+ res.status(500).json({ error: errorMessage });
+ }
+ });
+
// Delete media
app.delete("/api/media/:id", requireAuth, async (req, res) => {
try {
@@ -1151,6 +1341,26 @@ export async function registerRoutes(app: Express): Promise {
}
});
+ // Generate ribbon overlay
+ app.post("/api/media/generate-ribbon", requireAuth, async (req, res) => {
+ const { text, color, position } = req.body;
+
+ if (!text || !color || !position) {
+ return res.status(400).json({ error: "Text, color and position are required" });
+ }
+
+ try {
+ const { RibbonGenerator } = await import("./services/ribbonGenerator");
+ const generator = RibbonGenerator.getInstance();
+
+ const publicId = await generator.generateTriangularRibbon(text, color, position);
+ res.json({ publicId });
+ } catch (error) {
+ console.error("Error generating ribbon:", error);
+ res.status(500).json({ error: "Failed to generate ribbon overlay" });
+ }
+ });
+
const httpServer = createServer(app);
return httpServer;
diff --git a/server/services/ribbonGenerator.ts b/server/services/ribbonGenerator.ts
new file mode 100644
index 0000000..ffac041
--- /dev/null
+++ b/server/services/ribbonGenerator.ts
@@ -0,0 +1,87 @@
+import { createCanvas } from "canvas";
+import cloudinary from "cloudinary";
+
+export class RibbonGenerator {
+ private static instance: RibbonGenerator;
+
+ static getInstance(): RibbonGenerator {
+ if (!RibbonGenerator.instance) {
+ RibbonGenerator.instance = new RibbonGenerator();
+ }
+ return RibbonGenerator.instance;
+ }
+
+ // Generate a triangular corner ribbon
+ async generateTriangularRibbon(
+ text: string,
+ color: "red" | "yellow",
+ position: "north_west" | "north_east"
+ ): Promise {
+ const width = 200;
+ const height = 200;
+
+ const canvas = createCanvas(width, height);
+ const ctx = canvas.getContext("2d");
+
+ // Clear canvas with transparency
+ ctx.clearRect(0, 0, width, height);
+
+ // Set ribbon color
+ const ribbonColor = color === "red" ? "#FF0000" : "#FFC107";
+ ctx.fillStyle = ribbonColor;
+
+ // Draw triangle based on position
+ ctx.beginPath();
+ if (position === "north_west") {
+ // Triangle in top-left corner
+ ctx.moveTo(0, 0);
+ ctx.lineTo(150, 0);
+ ctx.lineTo(0, 150);
+ } else {
+ // Triangle in top-right corner
+ ctx.moveTo(width, 0);
+ ctx.lineTo(width - 150, 0);
+ ctx.lineTo(width, 150);
+ }
+ ctx.closePath();
+ ctx.fill();
+
+ // Add text on the diagonal
+ ctx.save();
+ ctx.fillStyle = "white";
+ ctx.font = "bold 24px Arial";
+ ctx.textAlign = "center";
+ ctx.textBaseline = "middle";
+
+ if (position === "north_west") {
+ // Rotate for northwest corner
+ ctx.translate(50, 50);
+ ctx.rotate(-Math.PI / 4);
+ } else {
+ // Rotate for northeast corner
+ ctx.translate(width - 50, 50);
+ ctx.rotate(Math.PI / 4);
+ }
+
+ ctx.fillText(text, 0, 0);
+ ctx.restore();
+
+ // Convert to base64
+ const buffer = canvas.toBuffer("image/png");
+ const base64 = `data:image/png;base64,${buffer.toString("base64")}`;
+
+ // Upload to Cloudinary
+ try {
+ const result = await cloudinary.v2.uploader.upload(base64, {
+ folder: "ribbon-overlays",
+ public_id: `ribbon_${text}_${color}_${position}_${Date.now()}`,
+ resource_type: "image"
+ });
+
+ return result.public_id;
+ } catch (error) {
+ console.error("Error uploading ribbon to Cloudinary:", error);
+ throw error;
+ }
+ }
+}
\ No newline at end of file