From 207af4cc73098e90400f9bcf37a1487d4a89ff71 Mon Sep 17 00:00:00 2001 From: chaaanuwu Date: Fri, 24 Apr 2026 21:09:37 +0530 Subject: [PATCH 1/4] feat: Add QR code generation --- .../generateReviewImage.controller.js | 6 +- server/package-lock.json | 230 +++++++++++++++++- server/package.json | 3 +- server/utils/generateQR.util.js | 17 ++ server/utils/generateReviewImage.util.js | 10 +- 5 files changed, 253 insertions(+), 13 deletions(-) create mode 100644 server/utils/generateQR.util.js diff --git a/server/controllers/features/generateReviewImage.controller.js b/server/controllers/features/generateReviewImage.controller.js index dbf0913..be37893 100644 --- a/server/controllers/features/generateReviewImage.controller.js +++ b/server/controllers/features/generateReviewImage.controller.js @@ -2,6 +2,7 @@ import generateReviewImage from '../../utils/generateReviewImage.util.js'; import Review from '../../models/review.model.js'; import History from '../../models/history.model.js'; import { CLIENT_URL, TMDB_BACKDROP_BASE_URL, TMDB_POSTER_BASE_URL } from '../../config/env.js'; +import { generateQR } from '../../utils/generateQR.util.js'; export const shareReviewImage = async (req, res) => { try { @@ -23,6 +24,8 @@ export const shareReviewImage = async (req, res) => { const reviewLink = `${clientURL}/reviews/${reviewId}`; + const qrCode = await generateQR(reviewLink); + const imageBuffer = await generateReviewImage({ backdropUrl: TMDB_BACKDROP_BASE_URL + review.movieId.backdropPath, posterUrl: TMDB_POSTER_BASE_URL + review.movieId.posterPath, @@ -35,7 +38,8 @@ export const shareReviewImage = async (req, res) => { userAvatarUrl: review.userId.pfp, rating: history.rating ? history.rating.toString() : "0", reviewDate: review.createdAt.toDateString().split(" ").slice(1, 3).join(" ") + ", " + review.createdAt.getFullYear(), - linkToReview: reviewLink + linkToReview: reviewLink, + qrCodeToReview: qrCode }); res.set('Content-Type', 'image/png'); diff --git a/server/package-lock.json b/server/package-lock.json index d265e8e..d4d4c62 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -19,7 +19,8 @@ "jsonwebtoken": "^9.0.3", "mongoose": "^9.3.0", "morgan": "^1.10.1", - "node-cron": "^4.2.1" + "node-cron": "^4.2.1", + "qrcode": "^1.5.4" }, "devDependencies": { "@babel/core": "^7.29.0", @@ -3326,7 +3327,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -3931,7 +3931,6 @@ "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -4181,7 +4180,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -4194,7 +4192,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, "node_modules/combined-stream": { @@ -4445,6 +4442,15 @@ "ms": "2.0.0" } }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", @@ -4595,6 +4601,12 @@ "wrappy": "1" } }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", + "license": "MIT" + }, "node_modules/dotenv": { "version": "17.3.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", @@ -5514,7 +5526,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" @@ -6253,7 +6264,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -8354,7 +8364,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -8422,7 +8431,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -8604,6 +8612,15 @@ "node": ">=4" } }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -8744,6 +8761,182 @@ ], "license": "MIT" }, + "node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "license": "MIT", + "dependencies": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/qrcode/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/qrcode/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/qrcode/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/qrcode/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "license": "ISC" + }, + "node_modules/qrcode/node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/qs": { "version": "6.14.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", @@ -8967,12 +9160,17 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" } }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "license": "ISC" + }, "node_modules/resolve": { "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", @@ -9163,6 +9361,12 @@ "node": ">= 0.8.0" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -10469,6 +10673,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "license": "ISC" + }, "node_modules/which-typed-array": { "version": "1.1.20", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", diff --git a/server/package.json b/server/package.json index 5da8201..c9c229c 100644 --- a/server/package.json +++ b/server/package.json @@ -19,7 +19,8 @@ "jsonwebtoken": "^9.0.3", "mongoose": "^9.3.0", "morgan": "^1.10.1", - "node-cron": "^4.2.1" + "node-cron": "^4.2.1", + "qrcode": "^1.5.4" }, "devDependencies": { "@babel/core": "^7.29.0", diff --git a/server/utils/generateQR.util.js b/server/utils/generateQR.util.js new file mode 100644 index 0000000..10894fa --- /dev/null +++ b/server/utils/generateQR.util.js @@ -0,0 +1,17 @@ +import QRCode from 'qrcode'; + +export const generateQR = async (link) => { + try { + const qrCode = await QRCode.toDataURL(link, { errorCorrectionLevel: 'H' }); + + if (!qrCode) { + throw new Error("Failed to generate QR code."); + } + + return qrCode; + + } catch (error) { + console.error("Generate QR code error:", error); + throw error; + } +}; \ No newline at end of file diff --git a/server/utils/generateReviewImage.util.js b/server/utils/generateReviewImage.util.js index 938a76b..02d5f44 100644 --- a/server/utils/generateReviewImage.util.js +++ b/server/utils/generateReviewImage.util.js @@ -67,7 +67,8 @@ export default async function generateReviewImage(data) { reviewDate = "Today", likeCount = "0", commentCount = "0", - linkToReview="#" + linkToReview="#", + qrCodeToReview } = data; const width = 1920; @@ -228,6 +229,13 @@ export default async function generateReviewImage(data) { ctx.fillStyle = 'rgba(255, 255, 255, 0.3)'; ctx.fillText(`🔗 ${linkToReview}`, paddingLeft, height - 80); + // --- 9. QR Code (Placed at the Bottom Right) --- + if (qrCodeToReview) { + const qrImg = await loadImage(qrCodeToReview); + const qrSize = 125; + ctx.drawImage(qrImg, width - paddingLeft - qrSize, height - 80 - qrSize, qrSize, qrSize); + } + return canvas.toBuffer('image/png'); } catch (error) { From 62f57ea017eba45be86e4d21dfe6b98ce29b6a6c Mon Sep 17 00:00:00 2001 From: chaaanuwu Date: Thu, 30 Apr 2026 11:11:44 +0530 Subject: [PATCH 2/4] feat: Enhance user history and profile features - Added `getHistoryBanner` API to fetch user's history for banner selection. - Introduced `setProfileCover` API to allow users to update their profile cover. - Updated `HistoryTab` component to improve loading states and animations using Framer Motion. - Enhanced `Navbar` with improved styling and modal for logout confirmation. - Refactored `Tabs` component to include loading states and animations for better user experience. - Improved `MovieCard` component with hover effects and updated rating display. - Updated `SearchBar` for better focus handling. - Enhanced `MoviePage` and `Profile` components with improved styling and functionality. - Added modal for selecting profile cover with movie backdrops. - Updated backend to include `backdropPath` in the history population for better data retrieval. --- client/package-lock.json | 74 +++- client/package.json | 3 + client/src/api/share.api.js | 11 + client/src/components/ui/Modal.jsx | 50 +-- client/src/components/ui/ReviewCard.jsx | 151 +++++++- client/src/pages/Profile.jsx | 335 ++++++++++-------- .../generateReviewImage.controller.js | 8 +- 7 files changed, 443 insertions(+), 189 deletions(-) create mode 100644 client/src/api/share.api.js diff --git a/client/package-lock.json b/client/package-lock.json index 9835505..165172c 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -8,6 +8,9 @@ "name": "Plotline", "version": "0.0.0", "dependencies": { + "@fortawesome/free-brands-svg-icons": "^7.2.0", + "@fortawesome/free-solid-svg-icons": "^7.2.0", + "@fortawesome/react-fontawesome": "^3.3.1", "@headlessui/react": "^2.2.9", "@heroicons/react": "^2.2.0", "@tailwindcss/vite": "^4.2.1", @@ -939,6 +942,65 @@ "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", "license": "MIT" }, + "node_modules/@fortawesome/fontawesome-common-types": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-7.2.0.tgz", + "integrity": "sha512-IpR0bER9FY25p+e7BmFH25MZKEwFHTfRAfhOyJubgiDnoJNsSvJ7nigLraHtp4VOG/cy8D7uiV0dLkHOne5Fhw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/fontawesome-svg-core": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-7.2.0.tgz", + "integrity": "sha512-6639htZMjEkwskf3J+e6/iar+4cTNM9qhoWuRfj9F3eJD6r7iCzV1SWnQr2Mdv0QT0suuqU8BoJCZUyCtP9R4Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "@fortawesome/fontawesome-common-types": "7.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-brands-svg-icons": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-7.2.0.tgz", + "integrity": "sha512-VNG8xqOip1JuJcC3zsVsKRQ60oXG9+oYNDCosjoU/H9pgYmLTEwWw8pE0jhPz/JWdHeUuK6+NQ3qsM4gIbdbYQ==", + "license": "(CC-BY-4.0 AND MIT)", + "dependencies": { + "@fortawesome/fontawesome-common-types": "7.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-solid-svg-icons": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-7.2.0.tgz", + "integrity": "sha512-YTVITFGN0/24PxzXrwqCgnyd7njDuzp5ZvaCx5nq/jg55kUYd94Nj8UTchBdBofi/L0nwRfjGOg0E41d2u9T1w==", + "license": "(CC-BY-4.0 AND MIT)", + "dependencies": { + "@fortawesome/fontawesome-common-types": "7.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/react-fontawesome": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-3.3.1.tgz", + "integrity": "sha512-wGnAPhfzivDwBWYmEG8MSrEXPruoiMMo48NnsRkj1NZkoaawgOijPNAiSHKMYEoCsqTBSgLTzL6EqTTWGaUR4w==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@fortawesome/fontawesome-svg-core": "~6 || ~7", + "react": "^18.0.0 || ^19.0.0" + } + }, "node_modules/@headlessui/react": { "version": "2.2.9", "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.9.tgz", @@ -3274,9 +3336,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", + "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", "funding": [ { "type": "opencollective", @@ -3635,9 +3697,9 @@ } }, "node_modules/vite": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", - "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", + "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", "license": "MIT", "dependencies": { "esbuild": "^0.27.0", diff --git a/client/package.json b/client/package.json index b98719d..9c93b9d 100644 --- a/client/package.json +++ b/client/package.json @@ -10,6 +10,9 @@ "preview": "vite preview" }, "dependencies": { + "@fortawesome/free-brands-svg-icons": "^7.2.0", + "@fortawesome/free-solid-svg-icons": "^7.2.0", + "@fortawesome/react-fontawesome": "^3.3.1", "@headlessui/react": "^2.2.9", "@heroicons/react": "^2.2.0", "@tailwindcss/vite": "^4.2.1", diff --git a/client/src/api/share.api.js b/client/src/api/share.api.js new file mode 100644 index 0000000..3ec57b0 --- /dev/null +++ b/client/src/api/share.api.js @@ -0,0 +1,11 @@ +import axiosInstance from "./axiosInstance"; + +export const shareReview = async (reviewId) => { + try { + const res = await axiosInstance.get(`/reviews/${reviewId}/share`); + return res; + } catch (error) { + console.error("Error sharing review: ", error); + throw error; + } +} \ No newline at end of file diff --git a/client/src/components/ui/Modal.jsx b/client/src/components/ui/Modal.jsx index 716853e..7b7e438 100644 --- a/client/src/components/ui/Modal.jsx +++ b/client/src/components/ui/Modal.jsx @@ -1,36 +1,36 @@ import { motion, AnimatePresence } from "framer-motion"; +import { useEffect } from "react"; export default function Modal({ open, setOpen, children }) { + + // Disable background scrolling when modal is open + useEffect(() => { + if (open) { + document.body.style.overflow = "hidden"; + } else { + document.body.style.overflow = "unset"; + } + return () => { + document.body.style.overflow = "unset"; + }; + }, [open]); + return ( {open && ( - /* Added [ ] around 200 and pointer-events-none */ -
- - {/* BACKDROP - Added pointer-events-auto so you can click to close */} + /* Backdrop */ +
setOpen(false)} + > + {/* Content Container (Stop propagation so clicking inside doesn't close) */} setOpen(false)} - className="absolute inset-0 bg-stone-950/60 backdrop-blur-sm pointer-events-auto" - /> - - {/* MODAL CONTENT - Added pointer-events-auto so you can click buttons */} - e.stopPropagation()} + className="w-full max-w-7xl pointer-events-auto" > - {/* CLOSE BUTTON */} - - {children}
diff --git a/client/src/components/ui/ReviewCard.jsx b/client/src/components/ui/ReviewCard.jsx index 081de85..ba28a91 100644 --- a/client/src/components/ui/ReviewCard.jsx +++ b/client/src/components/ui/ReviewCard.jsx @@ -1,6 +1,10 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; import useUserStore from "../../store/userStore"; import { toggleLikeReview } from "../../api/reviews.api"; +import { shareReview } from "../../api/share.api"; +import Modal from "./Modal"; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faShareNodes, faXmark } from '@fortawesome/free-solid-svg-icons'; export default function ReviewCard({ userId, @@ -30,6 +34,10 @@ export default function ReviewCard({ Array.isArray(reviewLikes) ? reviewLikes.length : (reviewLikes ?? 0) ); + const [isReviewShareModalOpen, setIsReviewShareModalOpen] = useState(false); + const [reviewImage, setReviewImage] = useState(null); + const [reviewUrl, setReviewUrl] = useState(null); + const [loading, setLoading] = useState(false); const formattedLikes = likes >= 1000 ? (likes / 1000).toFixed(1) + "k" : likes; @@ -47,7 +55,6 @@ export default function ReviewCard({ await toggleLikeReview(reviewId); } catch (err) { - // rollback only on failure setIsLiked(alreadyLiked); setLikes((prev) => (alreadyLiked ? prev + 1 : prev - 1)); } finally { @@ -55,6 +62,44 @@ export default function ReviewCard({ } }; + const handleReviewShare = async () => { + try { + setIsReviewShareModalOpen(true); + const res = await shareReview(reviewId); + const bufferArray = new Uint8Array(res.data.image.data); + const blob = new Blob([bufferArray], { type: "image/png" }); + const blobUrl = URL.createObjectURL(blob); + + setReviewImage(blobUrl); + setReviewUrl(res.data.reviewToLink); + + } catch (error) { + console.error("Failed to fetch review image:", error); + } + }; + + const handleShareReviewImage = async () => { + try { + const response = await fetch(reviewImage); + const blob = await response.blob(); + const file = new File([blob], `${movieTitle}-review.png`, { type: "image/png" }); + + const shareText = `Just posted my review for "${movieTitle}" on PlotLine!\n${reviewUrl}`; + + if (navigator.canShare && navigator.canShare({ files: [file], text: shareText })) { + await navigator.share({ + files: [file], + title: `${movieTitle} Review`, + text: shareText + }); + } else { + window.open(`https://wa.me/?text=${encodeURIComponent(shareText)}`, "_blank"); + } + } catch (err) { + console.error("Share failed:", err); + } + } + return (
@@ -143,10 +188,8 @@ export default function ReviewCard({
- {/* ACTIONS */}
- - {/* LIKE BUTTON */} + {/* Like Button */} -
+ + +
+ + {/* Close Button */} + + + {/* Image Preview Area */} +
+
+ + {reviewImage ? ( +
+ Review Share +
+ ) : ( +
+
+

Generating...

+
+ )} +
+ + {/* Action Area */} +
+
+
+
+ Studio Export +
+

+ Share
+ Review. +

+
+ +
+ + + +
+ + {/* Footer hint for mobile */} +
+

+ PlotLine +

+
+
+
+
+
+
+
+
); diff --git a/client/src/pages/Profile.jsx b/client/src/pages/Profile.jsx index 7491f85..592e933 100644 --- a/client/src/pages/Profile.jsx +++ b/client/src/pages/Profile.jsx @@ -37,7 +37,6 @@ export default function Profile() { try { const res = await getProfile(userId); setProfileData(res); - console.log(res); } catch (err) { console.error("Failed to fetch profile", err); } finally { @@ -70,11 +69,16 @@ export default function Profile() { } const handlePostComment = async () => { - console.log(commentInputRef.current.value); + const content = commentInputRef.current.value; + if (!content.trim()) return; + try { - const res = await postComment(selectedReview._id, commentInputRef.current.value); + const res = await postComment(selectedReview._id, content); if (res.data.success) { - console.log("Comment posted successfully"); + // Refresh comments locally + setComments(prev => [...prev, res.data.comment]); + commentInputRef.current.value = ""; + commentInputRef.current.style.height = 'auto'; } } catch (error) { console.error("Failed to post comment", error); @@ -179,8 +183,8 @@ export default function Profile() {
- - + +
calendar_month @@ -215,7 +219,6 @@ export default function Profile() { )} - {/* Dropdown */}
- {/* Director Statement */} - {/* Tabs */}
- {/* Comments Modal */} - -
- -
-

- Discussion -

-

- Responses -

-
- -
- {comments?.length > 0 ? ( - comments.map((c) => ( -
- -
-
-
-

{profileData?.user.firstName} {profileData?.user.lastName}

- {new Date(c.createdAt).toLocaleString()} - - {isMyProfile && -
- - - -
- - -
-
-
- } -
-

{c.comment}

-
-
- - -
-
-
- )) - ) : ( -
-

No thoughts shared yet. Be the first to break the silence.

-
- )} -
- -
-
- -