Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 71 additions & 13 deletions client/src/components/ImageVideoDisplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<Image, Video> {
thumbnail?: ProjectImage | PendingProjectImage,
Expand Down Expand Up @@ -65,12 +66,41 @@ const ImageVideoDisplay = <Image extends (ProjectImage | PendingProjectImage) |

const [confirm, setConfirm] = useState(false);

// Upload limits live here rather than in MediaTab and GalleryTab separately,
// since both render through this component. See constants/mediaLimits.ts.
const imageCount = images?.length ?? 0;
const videoCount = videos?.length ?? 0;
const imageLimitReached = imageCount >= 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 (
<div id="project-editor-media">
<label>{thumbnail ? "Project" : "Gallery"} Images</label>
<div className="project-editor-extra-info">
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)
</div>

{/* Display warning upon duplicate image */}
Expand Down Expand Up @@ -157,15 +187,29 @@ const ImageVideoDisplay = <Image extends (ProjectImage | PendingProjectImage) |
</div>
))}

{/* Image uploader */}
{/* Image uploader, replaced by a notice once the gallery is full */}
<div id="project-editor-add-image">
<ProjectImageUploader onFileSelected={handleImageUpload} />
{imageLimitReached ? (
<div className="drop-area media-limit-reached">
<div id="img-view" className="project-uploader">
<p className="project-editor-extra-info">
Image limit reached ({MAX_GALLERY_IMAGES})
</p>
<p className="project-editor-extra-info">
Delete an image to add another
</p>
</div>
</div>
) : (
<ProjectImageUploader onFileSelected={handleImageUploadWithinLimit} />
)}
</div>
</div>

<label>{thumbnail ? "Project" : "Gallery"} Videos</label>
<div className="project-editor-extra-info">
Link YouTube videos to be embedded.
{" "}({videoCount} of {MAX_GALLERY_VIDEOS} used)
</div>

<div id="project-editor-image-ui">
Expand Down Expand Up @@ -245,7 +289,7 @@ const ImageVideoDisplay = <Image extends (ProjectImage | PendingProjectImage) |
<button
className="confirm-btn"
onClick={() => {
handleAddVideo(newVideo);
handleAddVideoWithinLimit(newVideo);
setVideoPopupOpen(false);
}}
>
Expand All @@ -262,17 +306,31 @@ const ImageVideoDisplay = <Image extends (ProjectImage | PendingProjectImage) |
</button>
</div>
</div>
:
<div id="project-editor-add-image">
<button id="project-video-uploader" className="drop-area" onClick={() => setVideoPopupOpen(!videoPopupOpen)}>
<div id="img-view" className="project-uploader">
<svg xmlns="http://www.w3.org/2000/svg" width={38} height={39} viewBox="0 0 448 512">
<path d="M256 64c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 160-160 0c-17.7 0-32 14.3-32 32s14.3 32 32 32l160 0 0 160c0 17.7 14.3 32 32 32s32-14.3 32-32l0-160 160 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-160 0 0-160z" fill="var(--neutral-gray)" />
</svg>
<p className="project-editor-extra-info">Click here to add a new video</p>
: videoLimitReached
? /* Add-video button replaced by a notice once the gallery is full */
<div id="project-editor-add-image">
<div className="drop-area media-limit-reached">
<div id="img-view" className="project-uploader">
<p className="project-editor-extra-info">
Video limit reached ({MAX_GALLERY_VIDEOS})
</p>
<p className="project-editor-extra-info">
Delete a video to add another
</p>
</div>
</div>
</button>
</div>
</div>
:
<div id="project-editor-add-image">
<button id="project-video-uploader" className="drop-area" onClick={() => setVideoPopupOpen(!videoPopupOpen)}>
<div id="img-view" className="project-uploader">
<svg xmlns="http://www.w3.org/2000/svg" width={38} height={39} viewBox="0 0 448 512">
<path d="M256 64c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 160-160 0c-17.7 0-32 14.3-32 32s14.3 32 32 32l160 0 0 160c0 17.7 14.3 32 32 32s32-14.3 32-32l0-160 160 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-160 0 0-160z" fill="var(--neutral-gray)" />
</svg>
<p className="project-editor-extra-info">Click here to add a new video</p>
</div>
</button>
</div>
}
</div>

Expand Down
11 changes: 11 additions & 0 deletions client/src/components/Styles/imageUploader.css
Original file line number Diff line number Diff line change
Expand Up @@ -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%;
Expand Down
30 changes: 30 additions & 0 deletions client/src/constants/mediaLimits.ts
Original file line number Diff line number Diff line change
@@ -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;
Loading