diff --git a/README.md b/README.md index f43fa44..23abf2c 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,9 @@ GOOGLE_CLIENT_ID=your_google_client_id GOOGLE_CLIENT_SECRET=your_google_client_secret GOOGLE_CALLBACK_URL=http://localhost:5000/google/callback PORT=5000 +CLOUDINARY_CLOUD_NAME=your_cloudinary_name +CLOUDINARY_API_KEY=your_api_key +CLOUDINARY_API_SECRET=your_api_secret ``` 4. **Run the App** diff --git a/package-lock.json b/package-lock.json index df6ff9e..a3234f5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,9 +9,10 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "cloudinary": "^1.41.3", "axios": "^1.9.0", "connect-mongo": "^5.1.0", - "dotenv": "^16.4.5", + "dotenv": "^16.6.1", "ejs": "^3.1.10", "express": "^4.19.2", "express-ejs-layouts": "^2.5.1", @@ -19,6 +20,8 @@ "method-override": "^3.0.0", "mongodb": "^6.5.0", "mongoose": "^8.10.0", + "multer": "^2.0.1", + "multer-storage-cloudinary": "^4.0.0", "passport": "^0.7.0", "passport-google-oauth20": "^2.0.0" }, @@ -92,6 +95,12 @@ "node": ">= 8" } }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", @@ -223,6 +232,23 @@ "node": ">=16.20.1" } }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -300,6 +326,30 @@ "fsevents": "~2.3.2" } }, + "node_modules/cloudinary": { + "version": "1.41.3", + "resolved": "https://registry.npmjs.org/cloudinary/-/cloudinary-1.41.3.tgz", + "integrity": "sha512-4o84y+E7dbif3lMns+p3UW6w6hLHEifbX/7zBJvaih1E9QNMZITENQ14GPYJC4JmhygYXsuuBb9bRA3xWEoOfg==", + "license": "MIT", + "dependencies": { + "cloudinary-core": "^2.13.0", + "core-js": "^3.30.1", + "lodash": "^4.17.21", + "q": "^1.5.1" + }, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/cloudinary-core": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/cloudinary-core/-/cloudinary-core-2.13.1.tgz", + "integrity": "sha512-z53GPNWnvU0Zi+ns8CIVbZBfj7ps/++zDvwIyiFuq5p1MoK+KUCg0k5mBceDDHTnx1gHmHUd9aohS+gDxPNt6w==", + "license": "MIT", + "peerDependencies": { + "lodash": ">=4.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -332,6 +382,21 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, "node_modules/connect-mongo": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/connect-mongo/-/connect-mongo-5.1.0.tgz", @@ -380,6 +445,17 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, + "node_modules/core-js": { + "version": "3.43.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.43.0.tgz", + "integrity": "sha512-N6wEbTTZSYOY2rYAn85CuvWWkCK6QweMn7/4Nr3w+gDBeBhk/x4EJeY6FPo4QzDoJZxVTv8U7CMvgWk6pOHHqA==", + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -438,9 +514,10 @@ } }, "node_modules/dotenv": { - "version": "16.4.5", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", - "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", "engines": { "node": ">=12" }, @@ -1014,6 +1091,12 @@ "node": ">8" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -1133,6 +1216,27 @@ "node": "*" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, "node_modules/mongodb": { "version": "6.13.0", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.13.0.tgz", @@ -1237,6 +1341,33 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "node_modules/multer": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.1.tgz", + "integrity": "sha512-Ug8bXeTIUlxurg8xLTEskKShvcKDZALo1THEX5E41pYCD2sCVub5/kIRIGqWNoqV6szyLyQKV6mD4QUrWE5GCQ==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "mkdirp": "^0.5.6", + "object-assign": "^4.1.1", + "type-is": "^1.6.18", + "xtend": "^4.0.2" + }, + "engines": { + "node": ">= 10.16.0" + } + }, + "node_modules/multer-storage-cloudinary": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/multer-storage-cloudinary/-/multer-storage-cloudinary-4.0.0.tgz", + "integrity": "sha512-25lm9R6o5dWrHLqLvygNX+kBOxprzpmZdnVKH4+r68WcfCt8XV6xfQaMuAg+kUE5Xmr8mJNA4gE0AcBj9FJyWA==", + "license": "MIT", + "peerDependencies": { + "cloudinary": "^1.21.0" + } + }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -1323,6 +1454,15 @@ "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.10.0.tgz", "integrity": "sha512-1orQ9MT1vHFGQxhuy7E/0gECD3fd2fCC+PIX+/jgmU/gI3EpRocXtmtvxCO5x3WZ443FLTLFWNDjl5MPJf9u+Q==" }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", @@ -1466,6 +1606,17 @@ "node": ">=6" } }, + "node_modules/q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==", + "deprecated": "You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other.\n\n(For a CapTP with native promises, see @endo/eventual-send and @endo/captp)", + "license": "MIT", + "engines": { + "node": ">=0.6.0", + "teleport": ">=0.2.0" + } + }, "node_modules/qs": { "version": "6.11.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", @@ -1510,6 +1661,20 @@ "node": ">= 0.8" } }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -1687,6 +1852,23 @@ "node": ">= 0.8" } }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -1753,6 +1935,12 @@ "node": ">= 0.6" } }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, "node_modules/uid-safe": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", @@ -1783,6 +1971,12 @@ "node": ">= 0.8" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -1819,6 +2013,15 @@ "node": ">=16" } }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", diff --git a/package.json b/package.json index b1118e2..154bafe 100644 --- a/package.json +++ b/package.json @@ -11,9 +11,10 @@ "author": "", "license": "ISC", "dependencies": { + "cloudinary": "^1.41.3", "axios": "^1.9.0", "connect-mongo": "^5.1.0", - "dotenv": "^16.4.5", + "dotenv": "^16.6.1", "ejs": "^3.1.10", "express": "^4.19.2", "express-ejs-layouts": "^2.5.1", @@ -21,6 +22,8 @@ "method-override": "^3.0.0", "mongodb": "^6.5.0", "mongoose": "^8.10.0", + "multer": "^2.0.1", + "multer-storage-cloudinary": "^4.0.0", "passport": "^0.7.0", "passport-google-oauth20": "^2.0.0" }, diff --git a/public/css/main.css b/public/css/main.css index fa29670..21ec4e1 100644 --- a/public/css/main.css +++ b/public/css/main.css @@ -87,4 +87,15 @@ h1,h2,h3,h4,h5,h6{ position: absolute; right: -50px; bottom: -30px; +} + +/* Note image previews */ +#imagePreview img, .note-image { + border: 1px solid #ccc; + border-radius: 8px; + margin-right: 8px; + margin-bottom: 8px; + max-width: 120px; + max-height: 120px; + object-fit: cover; } \ No newline at end of file diff --git a/server/config/cloudinary.js b/server/config/cloudinary.js new file mode 100644 index 0000000..234f131 --- /dev/null +++ b/server/config/cloudinary.js @@ -0,0 +1,9 @@ +const cloudinary = require('cloudinary').v2; + +cloudinary.config({ + cloud_name: process.env.CLOUDINARY_CLOUD_NAME, + api_key: process.env.CLOUDINARY_API_KEY, + api_secret: process.env.CLOUDINARY_API_SECRET, +}); + +module.exports = cloudinary; \ No newline at end of file diff --git a/server/config/db.js b/server/config/db.js index 15ce26e..767d337 100644 --- a/server/config/db.js +++ b/server/config/db.js @@ -9,4 +9,6 @@ const connectDB = async() => { console.log(error); } } -module.exports = connectDB; \ No newline at end of file +module.exports = connectDB; + +// Make sure to load environment variables for Cloudinary in your main app entry point (app.js) \ No newline at end of file diff --git a/server/controllers/dashboardController.js b/server/controllers/dashboardController.js index 11440a6..8f609da 100644 --- a/server/controllers/dashboardController.js +++ b/server/controllers/dashboardController.js @@ -23,8 +23,10 @@ exports.dashboard = async (req, res) => { { $match: { user: new mongoose.Types.ObjectId(req.user.id) } }, { $project: { + _id: 1, title: { $substr: ["$title", 0, 30] }, body: { $substr: ["$body", 0, 100] }, + images: 1 }, }, ]) @@ -76,9 +78,20 @@ exports.dashboardViewNote = async(req,res) => { exports.dashboardUpdateNote = async(req,res) => { try { + let images = []; + if (req.body.imageUrls) { + try { + images = JSON.parse(req.body.imageUrls); + } catch (e) { images = []; } + } await Note.findOneAndUpdate( {_id: req.params.id}, - {title: req.body.title , body: req.body.body , updatedAt: Date.now() } + { + title: req.body.title, + body: req.body.body, + images: images, + updatedAt: Date.now() + } ).where({ user: req.user.id }); res.redirect('/dashboard'); } catch (error) { @@ -123,6 +136,7 @@ exports.dashboardAddNote = async(req,res) => { exports.dashboardAddNote = async(req,res) => { try { + console.log('Received body:', req.body); if (!req.body.title || req.body.title.trim() === "") { return res.render('dashboard/add', { layout: '../views/layouts/dashboard', @@ -131,9 +145,21 @@ exports.dashboardAddNote = async(req,res) => { }); } req.body.user = req.user.id; - await Note.create(req.body); + let images = []; + if (req.body.imageUrls) { + try { + images = JSON.parse(req.body.imageUrls); + } catch (e) { images = []; } + } + await Note.create({ + user: req.body.user, + title: req.body.title, + body: req.body.body, + images: images + }); res.redirect('/dashboard'); } catch (error) { + console.log('Error saving note:', error); let errorMsg = 'An error occurred while adding the note.'; if (error.name === 'ValidationError') { errorMsg = error.message; diff --git a/server/controllers/imageUploadController.js b/server/controllers/imageUploadController.js new file mode 100644 index 0000000..28b93a9 --- /dev/null +++ b/server/controllers/imageUploadController.js @@ -0,0 +1,27 @@ +const cloudinary = require('../config/cloudinary'); +const multer = require('multer'); +const { CloudinaryStorage } = require('multer-storage-cloudinary'); + +// Configure storage +const storage = new CloudinaryStorage({ + cloudinary: cloudinary, + params: { + folder: 'notesapp', + allowed_formats: ['jpg', 'jpeg', 'png', 'gif'], + }, +}); + +const upload = multer({ storage: storage }); + +// Middleware to use in routes +exports.uploadImage = upload.array('images', 5); // up to 5 images per note + +// Route handler +exports.handleUpload = (req, res) => { + try { + const imageUrls = req.files.map(file => file.path); + res.json({ imageUrls }); + } catch (err) { + res.status(500).json({ error: 'Image upload failed' }); + } +}; \ No newline at end of file diff --git a/server/models/Notes.js b/server/models/Notes.js index 0584f57..8245787 100644 --- a/server/models/Notes.js +++ b/server/models/Notes.js @@ -14,10 +14,9 @@ const NoteSchema = new schema({ type:String, required:true, }, - summary: { - type: String, - default: '', - }, + images: [{ + type: String + }], createdAt:{ type:Date, default: Date.now() diff --git a/server/routes/dashboard.js b/server/routes/dashboard.js index 7196451..b84397f 100644 --- a/server/routes/dashboard.js +++ b/server/routes/dashboard.js @@ -2,6 +2,9 @@ const express = require('express'); const router = express.Router(); const { isLoggedIn } = require('../middleware/checkAuth'); const dashboardController = require('../controllers/dashboardController'); +const imageUploadController = require('../controllers/imageUploadController'); +const multer = require('multer'); +const upload = multer(); /** * dashboard routes @@ -12,9 +15,11 @@ router.put('/dashboard/item/:id',isLoggedIn, dashboardController.dashboardUpdate router.delete('/dashboard/item-delete/:id',isLoggedIn, dashboardController.dashboardDeleteNote); router.post('/dashboard/summarize', dashboardController.dashboardSummarizeNote); router.get('/dashboard/add',isLoggedIn, dashboardController.dashboardAddNote); -router.post('/dashboard/add',isLoggedIn, dashboardController.dashboardAddNote); +router.post('/dashboard/add',isLoggedIn, upload.none(), dashboardController.dashboardAddNote); router.get('/dashboard/search',isLoggedIn, dashboardController.dashboardSearch); router.post('/dashboard/search',isLoggedIn, dashboardController.dashboardSearchSubmit); +// Image upload route +router.post('/dashboard/upload-image', isLoggedIn, imageUploadController.uploadImage, imageUploadController.handleUpload); module.exports = router; \ No newline at end of file diff --git a/views/dashboard/add.ejs b/views/dashboard/add.ejs index d7ab6da..d7f634a 100644 --- a/views/dashboard/add.ejs +++ b/views/dashboard/add.ejs @@ -1,4 +1,4 @@ -