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..9bdd00b 100644 --- a/client/src/pages/Profile.jsx +++ b/client/src/pages/Profile.jsx @@ -35,9 +35,9 @@ export default function Profile() { useEffect(() => { const fetchProfile = async () => { try { + setProfileData(null); const res = await getProfile(userId); setProfileData(res); - console.log(res); } catch (err) { console.error("Failed to fetch profile", err); } finally { @@ -59,6 +59,7 @@ export default function Profile() { } const handleOpenReplyModal = async (review) => { + setComments([]); setSelectedReview(review); setIsReplyModalOpen(true); try { @@ -70,11 +71,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 +185,8 @@ export default function Profile() {
- - + +
calendar_month @@ -215,7 +221,6 @@ export default function Profile() { )} - {/* Dropdown */}
- {/* Director Statement */} - {/* Tabs */} -
- -
-

- Discussion -

-

- Responses -

+
+ +
+
+
+
+

+ Discussion +

+
+

+ PlotLine Community. +

+
+
-
+ {/* Comments Feed */} +
{comments?.length > 0 ? ( comments.map((c) => ( -
+ User -
-
-
-

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

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

+ {c.userId?.firstName} {c.userId?.lastName} +

+ + {new Date(c.createdAt).toLocaleDateString()} +
-

{c.comment}

+ + {user?.user?._id === c.userId?._id && ( + + )}
-
- - + +
+

+ {c.comment} +

-
+ )) ) : ( -
-

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

+
+ + forum + +

+ Be the first to speak +

)}
-
-
- -