diff --git a/client/src/components/ImageVideoDisplay.tsx b/client/src/components/ImageVideoDisplay.tsx index 3a79f443a..5c7085a11 100644 --- a/client/src/components/ImageVideoDisplay.tsx +++ b/client/src/components/ImageVideoDisplay.tsx @@ -8,6 +8,7 @@ import { ProjectImageUploader } from "./ImageUploader"; import { getYouTubeEmbedURL } from "../functions/parseYoutube"; import { Popup, PopupButton, PopupContent } from "./Popup"; import { DeleteProjectButton } from "./ProjectCreatorEditor/DeleteProjectButton"; +import { MAX_GALLERY_IMAGES, MAX_GALLERY_VIDEOS } from "../constants/mediaLimits"; interface ImageVideoDisplayProps { thumbnail?: ProjectImage | PendingProjectImage, @@ -65,12 +66,41 @@ const ImageVideoDisplay = = MAX_GALLERY_IMAGES; + const videoLimitReached = videoCount >= MAX_GALLERY_VIDEOS; + + /** + * Accepts an uploaded image only while there's room left. The uploader is + * swapped out at the limit, but a queued multi-select can still deliver + * files after the last slot fills, so this backs it up. + * @param file the uploaded image + * @param altText optional caption entered in the crop popup + */ + const handleImageUploadWithinLimit = (file: File, altText?: string) => { + if (imageLimitReached) return; + handleImageUpload(file, altText); + }; + + /** + * Accepts a linked video only while there's room left. + * @param video the video to add + */ + const handleAddVideoWithinLimit = (video: Video) => { + if (videoLimitReached) return; + handleAddVideo(video); + }; + return (
Upload images for showcasing. {thumbnail ? `Star an image for it to be used as this project's thumbnail on the Discover and My Projects pages.` : ""} + {" "}({imageCount} of {MAX_GALLERY_IMAGES} used)
{/* Display warning upon duplicate image */} @@ -157,15 +187,29 @@ const ImageVideoDisplay = ))} - {/* Image uploader */} + {/* Image uploader, replaced by a notice once the gallery is full */}
- + {imageLimitReached ? ( +
+
+

+ Image limit reached ({MAX_GALLERY_IMAGES}) +

+

+ Delete an image to add another +

+
+
+ ) : ( + + )}
Link YouTube videos to be embedded. + {" "}({videoCount} of {MAX_GALLERY_VIDEOS} used)
@@ -245,7 +289,7 @@ const ImageVideoDisplay = { - handleAddVideo(newVideo); + handleAddVideoWithinLimit(newVideo); setVideoPopupOpen(false); }} > @@ -262,17 +306,31 @@ const ImageVideoDisplay =
- : -
- -
+ + : +
+ +
} diff --git a/client/src/components/Styles/imageUploader.css b/client/src/components/Styles/imageUploader.css index ef4419e1c..a255c408f 100644 --- a/client/src/components/Styles/imageUploader.css +++ b/client/src/components/Styles/imageUploader.css @@ -55,6 +55,17 @@ border: 2px dashed #424242; border-radius: 10px; } +/* Stands in for the uploader once a gallery hits its limit (see + constants/mediaLimits.ts). Same dashed box, but inert. */ +.media-limit-reached { + cursor: default; + opacity: 0.6; + display: flex; + align-items: center; + justify-content: center; + text-align: center; +} + .drop-area-drag-over { height: 100%; width: 100%; diff --git a/client/src/constants/mediaLimits.ts b/client/src/constants/mediaLimits.ts new file mode 100644 index 000000000..5f1b27e2b --- /dev/null +++ b/client/src/constants/mediaLimits.ts @@ -0,0 +1,30 @@ +/** + * Upload limits for the media galleries. + * + * WHERE THESE APPLY + * Both the project editor's Media tab and the profile editor's Gallery tab + * render through `ImageVideoDisplay`, and that component is the single place + * these are enforced. Changing a number here changes it for both galleries; + * there is no separate project/profile limit. If the two ever need to differ, + * add a prop to `ImageVideoDisplay` rather than re-checking in each tab, or + * the two will drift apart. + * + * HOW THEY'RE ENFORCED + * At the limit the uploader is swapped for a "limit reached" notice, and the + * upload handlers refuse anything further as a second line of defence. + * + * IMPORTANT — CLIENT-SIDE ONLY + * The API does not reject uploads past these counts. Anything calling the + * endpoints directly can still exceed them. If that matters, the same limits + * need enforcing server-side in the project image/video and gallery routes. + * + * These are deliberately generous. They exist to stop one account filling + * storage, not to constrain normal use — raise them freely if they get in + * people's way. + */ + +/** Maximum images in a single project gallery or profile gallery. */ +export const MAX_GALLERY_IMAGES = 30; + +/** Maximum linked videos in a single project gallery or profile gallery. */ +export const MAX_GALLERY_VIDEOS = 15;