Repository: terraware/terraware-web
Pull request #3957: SW-6820 Make Project Profile images display consistently with aspect ratio of 3:2
Author: login: ice1080
Created: 2025-05-06T15:42:53Z | Merged: 2025-05-06T21:36:26Z

Make Project Profile images display consistently with aspect ratio of 3:2.

Fix Edit page to also display 3:2.


Diff:
diff --git a/src/components/DocumentProducer/EditImagesModal/PhotoSelector.tsx b/src/components/DocumentProducer/EditImagesModal/PhotoSelector.tsx
index d580c4484dbf..386805767a01 100644
--- a/src/components/DocumentProducer/EditImagesModal/PhotoSelector.tsx
+++ b/src/components/DocumentProducer/EditImagesModal/PhotoSelector.tsx
@@ -20,6 +20,7 @@ export type PhotoChooserProps = Omit<PhotoDragDropProps, 'files' | 'setFiles'> &
   error?: PhotoChooserErrorType;
   includeCaption?: boolean;
   includeCitation?: boolean;
+  previewWidth?: number;
 };
 
 export type PhotoWithAttributes = {
@@ -41,6 +42,7 @@ export default function PhotoChooser(props: PhotoChooserProps): JSX.Element {
     error,
     includeCaption = true,
     includeCitation = true,
+    previewWidth = 120,
     ...dragDropProps
   } = props;
   const [files, setFiles] = useState<File[]>([]);
@@ -150,6 +152,7 @@ export default function PhotoChooser(props: PhotoChooserProps): JSX.Element {
                   <PhotoPreview
                     imgUrl={fileData.url}
                     includeTrashIcon={true}
+                    imageWidth={previewWidth}
                     onTrashClick={() => removeFileAtIndex(index)}
                     imgAlt={files[index]?.name}
                   />
diff --git a/src/components/Photo/PhotoPreview.tsx b/src/components/Photo/PhotoPreview.tsx
index a91fb82fdb38..5d09674f4b43 100644
--- a/src/components/Photo/PhotoPreview.tsx
+++ b/src/components/Photo/PhotoPreview.tsx
@@ -1,6 +1,6 @@
-import React from 'react';
+import React, { useMemo } from 'react';
 
-import { Box, useTheme } from '@mui/material';
+import { Box, SxProps, useTheme } from '@mui/material';
 import { Button } from '@terraware/web-components';
 
 type PhotoPreviewProps = {
@@ -8,13 +8,46 @@ type PhotoPreviewProps = {
   imgAlt?: string;
   includeTrashIcon: boolean;
   onTrashClick: () => void;
+  imageWidth?: number;
+  aspectRatio?: number; // height / width
 };
 
-const PhotoPreview = ({ imgUrl, imgAlt, includeTrashIcon, onTrashClick }: PhotoPreviewProps) => {
+const PhotoPreview = ({
+  imgUrl,
+  imgAlt,
+  imageWidth,
+  aspectRatio,
+  includeTrashIcon,
+  onTrashClick,
+}: PhotoPreviewProps) => {
   const theme = useTheme();
 
+  const imgSx = useMemo((): SxProps => {
+    if (aspectRatio) {
+      return {
+        position: 'absolute',
+        top: '50%',
+        left: '50%',
+        transform: 'translate(-50%, -50%)',
+        width: '100%',
+        objectFit: 'cover',
+      };
+    } else {
+      return {
+        objectFit: 'contain',
+        width: imageWidth ? imageWidth : '100%',
+        display: 'flex',
+      };
+    }
+  }, [aspectRatio, imageWidth]);
+
   return (
-    <Box position='relative' height={122} width={122} border={`1px solid ${theme.palette.TwClrBrdrTertiary}`}>
+    <Box
+      position='relative'
+      width={imageWidth ? imageWidth + 2 : 122}
+      height={aspectRatio && imageWidth ? aspectRatio * imageWidth + 2 : 122}
+      border={`1px solid ${theme.palette.TwClrBrdrTertiary}`}
+    >
       {includeTrashIcon && (
         <Button
           icon='iconTrashCan'
@@ -25,21 +58,30 @@ const PhotoPreview = ({ imgUrl, imgAlt, includeTrashIcon, onTrashClick }: PhotoP
             top: -10,
             right: -10,
             backgroundColor: theme.palette.TwClrBgDanger,
+            zIndex: 1000,
           }}
         />
       )}
-      <img
-        height='120px'
-        src={imgUrl}
-        alt={imgAlt}
-        style={{
-          margin: 'auto auto',
-          objectFit: 'contain',
-          display: 'flex',
-          maxWidth: '120px',
-          maxHeight: '120px',
+      <Box
+        sx={{
+          overflow: 'hidden',
+          position: 'relative',
+          width: '100%',
+          height: '100%',
+          paddingTop: aspectRatio && `${aspectRatio * 100}%`,
         }}
-      />
+      >
+        <Box
+          component={'img'}
+          src={imgUrl}
+          alt={imgAlt}
+          sx={{
+            margin: 'auto auto',
+            height: '100%',
+            ...imgSx,
+          }}
+        />
+      </Box>
     </Box>
   );
 };
diff --git a/src/components/Photo/PhotoSelectorWithPreview.tsx b/src/components/Photo/PhotoSelectorWithPreview.tsx
index 77e3a5c35429..de1ca5e40f5e 100644
--- a/src/components/Photo/PhotoSelectorWithPreview.tsx
+++ b/src/components/Photo/PhotoSelectorWithPreview.tsx
@@ -20,6 +20,8 @@ export type PhotoSelectorWithPreviewProps = Omit<PhotoDragDropProps, 'files' | '
   error?: PhotoSelectorWithPreviewErrorType;
   previewUrl?: string;
   previewPlacement?: TooltipProps['placement'];
+  previewWidth?: number;
+  previewAspectRatio?: number;
 };
 
 export type FileWithUrl = {
@@ -34,6 +36,8 @@ export default function PhotoSelectorWithPreview({
   error,
   previewUrl,
   previewPlacement = 'top-start',
+  previewWidth,
+  previewAspectRatio,
   ...dragDropProps
 }: PhotoSelectorWithPreviewProps): JSX.Element {
   const [fileData, setFileData] = useState<FileWithUrl | undefined>();
@@ -54,7 +58,7 @@ export default function PhotoSelectorWithPreview({
         URL.revokeObjectURL(fileData.url);
       }
       if (files.length > 0) {
-        const file = files[0];
+        const file = files[files.length - 1];
         const fileUrl = URL.createObjectURL(file);
         setFileData({ file, url: fileUrl });
       } else {
@@ -64,27 +68,18 @@ export default function PhotoSelectorWithPreview({
     [fileData]
   );
 
-  const preview = useMemo(
-    () =>
-      fileData ? (
-        <PhotoPreview
-          imgUrl={fileData.url}
-          imgAlt={fileData.file.name}
-          includeTrashIcon={true}
-          onTrashClick={() => handleSetFiles([])}
-        />
-      ) : (
-        previewUrl && (
-          <PhotoPreview
-            imgUrl={previewUrl}
-            imgAlt={''}
-            includeTrashIcon={false}
-            onTrashClick={() => handleSetFiles([])}
-          />
-        )
-      ),
-    [fileData, previewUrl, handleSetFiles]
-  );
+  const preview = useMemo(() => {
+    const args = {
+      imageWidth: previewWidth,
+      aspectRatio: previewAspectRatio,
+      onTrashClick: () => handleSetFiles([]),
+    };
+    return fileData ? (
+      <PhotoPreview imgUrl={fileData.url} imgAlt={fileData.file.name} includeTrashIcon={true} {...args} />
+    ) : (
+      previewUrl && <PhotoPreview imgUrl={previewUrl} imgAlt={''} includeTrashIcon={false} {...args} />
+    );
+  }, [fileData, previewUrl, handleSetFiles]);
 
   return (
     <Box
@@ -122,7 +117,12 @@ export default function PhotoSelectorWithPreview({
 
         <PlacementWrapper placedObject={preview} objectPlacement={previewPlacement || 'top-start'}>
           <Box width={'100%'}>
-            <PhotoDragDrop {...dragDropProps} files={fileData ? [fileData.file] : []} setFiles={handleSetFiles} />
+            <PhotoDragDrop
+              {...dragDropProps}
+              multipleSelection={false}
+              files={fileData ? [fileData.file] : []}
+              setFiles={handleSetFiles}
+            />
           </Box>
         </PlacementWrapper>
       </Box>
diff --git a/src/components/ProjectField/ProjectProfileImage.tsx b/src/components/ProjectField/ProjectProfileImage.tsx
index 720ebf056980..93f03d0e57e9 100644
--- a/src/components/ProjectField/ProjectProfileImage.tsx
+++ b/src/components/ProjectField/ProjectProfileImage.tsx
@@ -14,22 +14,34 @@ type ProjectProfileImageProps = {
 const ProjectProfileImage = ({ projectId, imageValueId, alt, label }: ProjectProfileImageProps) => {
   const theme = useTheme();
   return (
-    <Grid item md={6} maxHeight={400} paddingX={theme.spacing(1)}>
+    <Grid item md={6} sx={{ paddingX: theme.spacing(1) }}>
       <Box
-        maxHeight={400}
-        overflow={'hidden'}
-        borderRadius={theme.spacing(1)}
-        position={'relative'}
-        sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}
+        sx={{
+          overflow: 'hidden',
+          borderRadius: theme.spacing(1),
+          position: 'relative',
+          width: '100%',
+          paddingTop: '66.67%', // 3:2 ratio
+        }}
       >
-        <img
+        <Box
+          component={'img'}
           src={getImagePath(projectId, imageValueId)}
           alt={alt}
-          style={{ width: '100%', objectFit: 'cover', borderRadius: theme.spacing(1) }}
+          sx={{
+            position: 'absolute',
+            top: '50%',
+            left: '50%',
+            transform: 'translate(-50%, -50%)',
+            width: '100%',
+            height: '100%',
+            objectFit: 'cover',
+            borderRadius: theme.spacing(1),
+          }}
         />
         {label && (
-          <div
-            style={{
+          <Box
+            sx={{
               height: 'max-content',
               width: 'max-content',
               position: 'absolute',
@@ -39,7 +51,7 @@ const ProjectProfileImage = ({ projectId, imageValueId, alt, label }: ProjectPro
             }}
           >
             {label}
-          </div>
+          </Box>
         )}
       </Box>
     </Grid>
diff --git a/src/scenes/AcceleratorRouter/ParticipantProjects/EditView/ProjectProfileEdit.tsx b/src/scenes/AcceleratorRouter/ParticipantProjects/EditView/ProjectProfileEdit.tsx
index 6dd8882fe61a..5dd5d507b858 100644
--- a/src/scenes/AcceleratorRouter/ParticipantProjects/EditView/ProjectProfileEdit.tsx
+++ b/src/scenes/AcceleratorRouter/ParticipantProjects/EditView/ProjectProfileEdit.tsx
@@ -615,12 +615,13 @@ const ProjectProfileEdit = () => {
                 chooseFileText={strings.CHOOSE_FILE}
                 replaceFileText={strings.REPLACE_FILE}
                 previewUrl={
-                  mainPhoto?.url ||
                   (participantProject?.projectHighlightPhotoValueId &&
                     getImagePath(projectId, participantProject.projectHighlightPhotoValueId)) ||
                   undefined
                 }
                 previewPlacement={'right'}
+                previewWidth={375}
+                previewAspectRatio={2 / 3}
               />
             </Grid>
           </Grid>
@@ -634,12 +635,13 @@ const ProjectProfileEdit = () => {
                 chooseFileText={strings.CHOOSE_FILE}
                 replaceFileText={strings.REPLACE_FILE}
                 previewUrl={
-                  mapPhoto?.url ||
                   (participantProject?.projectZoneFigureValueId &&
                     getImagePath(projectId, participantProject.projectZoneFigureValueId)) ||
                   undefined
                 }
                 previewPlacement={'right'}
+                previewWidth={375}
+                previewAspectRatio={2 / 3}
               />
             </Grid>
           </Grid>


Reviews:
user: login: graphite-app[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-06T17:10:47Z
user: login: constanzauanini
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T18:56:19Z
user: login: constanzauanini
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T19:00:23Z
user: login: ice1080
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T20:41:55Z
user: login: ice1080
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T20:42:58Z
user: login: constanzauanini
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T20:55:01Z

Review comments:
diff_hunk: @@ -615,12 +615,13 @@ const ProjectProfileEdit = () => {
                 chooseFileText={strings.CHOOSE_FILE}
                 replaceFileText={strings.REPLACE_FILE}
                 previewUrl={
-                  mainPhoto?.url ||
                   (participantProject?.projectHighlightPhotoValueId &&
                     getImagePath(projectId, participantProject.projectHighlightPhotoValueId)) ||
                   undefined
                 }
                 previewPlacement={'right'}
+                previewWidth={375}
+                previewAspectRatio={2 / 3}
path: src/scenes/AcceleratorRouter/ParticipantProjects/EditView/ProjectProfileEdit.tsx
user: login: graphite-app[bot]
body: The `previewAspectRatio` is set to `2/3` (portrait orientation), but the PR title and description indicate the goal is to make images display with a consistent aspect ratio of `3:2` (landscape orientation). The display view already uses a `3:2` ratio as seen in the `ProjectProfileImage` component where `paddingTop: '66.67%'` creates this ratio. For consistency between edit and display views, consider changing this value to `3/2`.
```suggestion
                previewAspectRatio={3 / 2}
```
  

*Spotted by [Diamond](https://app.graphite.dev/diamond/?org=terraware&ref=ai-review-comment)*<i class='graphite__hidden'><br /><br />Is this helpful? React 👍 or 👎 to let us know.</i>
created_at: 2025-05-06T17:10:46Z
author_association: NONE
diff_hunk: @@ -150,6 +152,7 @@ export default function PhotoChooser(props: PhotoChooserProps): JSX.Element {
                   <PhotoPreview
                     imgUrl={fileData.url}
                     includeTrashIcon={true}
+                    imageWidth={previewWidth || 120}
path: src/components/DocumentProducer/EditImagesModal/PhotoSelector.tsx
user: login: constanzauanini
body: If previewWidth is initialized to 120 I don't think the `|| 120` is needed here
created_at: 2025-05-06T18:56:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -615,12 +615,13 @@ const ProjectProfileEdit = () => {
                 chooseFileText={strings.CHOOSE_FILE}
                 replaceFileText={strings.REPLACE_FILE}
                 previewUrl={
-                  mainPhoto?.url ||
                   (participantProject?.projectHighlightPhotoValueId &&
                     getImagePath(projectId, participantProject.projectHighlightPhotoValueId)) ||
                   undefined
                 }
                 previewPlacement={'right'}
+                previewWidth={375}
+                previewAspectRatio={2 / 3}
path: src/scenes/AcceleratorRouter/ParticipantProjects/EditView/ProjectProfileEdit.tsx
user: login: constanzauanini
body: Is this correct @ice1080 ?
created_at: 2025-05-06T19:00:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -615,12 +615,13 @@ const ProjectProfileEdit = () => {
                 chooseFileText={strings.CHOOSE_FILE}
                 replaceFileText={strings.REPLACE_FILE}
                 previewUrl={
-                  mainPhoto?.url ||
                   (participantProject?.projectHighlightPhotoValueId &&
                     getImagePath(projectId, participantProject.projectHighlightPhotoValueId)) ||
                   undefined
                 }
                 previewPlacement={'right'}
+                previewWidth={375}
+                previewAspectRatio={2 / 3}
path: src/scenes/AcceleratorRouter/ParticipantProjects/EditView/ProjectProfileEdit.tsx
user: login: ice1080
body: Nah it's backwards. In `PhotoPreview.tsx`, it's taking the 2/3 `aspectRatio` and multiplying by 100 to get the 66.67, and then adding `%`
created_at: 2025-05-06T20:41:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -150,6 +152,7 @@ export default function PhotoChooser(props: PhotoChooserProps): JSX.Element {
                   <PhotoPreview
                     imgUrl={fileData.url}
                     includeTrashIcon={true}
+                    imageWidth={previewWidth || 120}
path: src/components/DocumentProducer/EditImagesModal/PhotoSelector.tsx
user: login: ice1080
body: Fixed!
created_at: 2025-05-06T20:42:58Z
author_association: CONTRIBUTOR

PR comments:
user: login: vercel[bot]
created_at: 2025-05-06T15:42:54Z
body: [vc]: #Uv3TLdwpXiA145Eu7e/HFsEWYur+B1uwOtzJkLtvhXM=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJ0ZXJyYXdhcmUtd2ViLXN0b3J5Ym9vayIsInJvb3REaXJlY3RvcnkiOm51bGwsImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS90ZXJyYWZvcm1hdGlvbi90ZXJyYXdhcmUtd2ViLXN0b3J5Ym9vay9CSjlEZnhyd0NFVTdBSlBVQTlLeTRleVZIZGFTIiwicHJldmlld1VybCI6InRlcnJhd2FyZS13ZWItc3Rvcnlib29rLWdpdC1pc2FhYy1pbWFnZS1hLTAwYjQwZi10ZXJyYWZvcm1hdGlvbi52ZXJjZWwuYXBwIiwibmV4dENvbW1pdFN0YXR1cyI6IkRFUExPWUVEIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6InRlcnJhd2FyZS13ZWItc3Rvcnlib29rLWdpdC1pc2FhYy1pbWFnZS1hLTAwYjQwZi10ZXJyYWZvcm1hdGlvbi52ZXJjZWwuYXBwIn19LHsibmFtZSI6InRyZWUtbG9jYXRpb24td2ViIiwicm9vdERpcmVjdG9yeSI6bnVsbCwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL3RlcnJhZm9ybWF0aW9uL3RyZWUtbG9jYXRpb24td2ViL0RyUHlMTEg3WFdlVTdTNDRHNUt4UmU0dDVvUGMiLCJwcmV2aWV3VXJsIjoidHJlZS1sb2NhdGlvbi13ZWItZ2l0LWlzYWFjLWltYWdlLWFzcGVjdC1yYXRpby10ZXJyYWZvcm1hdGlvbi52ZXJjZWwuYXBwIiwibmV4dENvbW1pdFN0YXR1cyI6IkRFUExPWUVEIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6InRyZWUtbG9jYXRpb24td2ViLWdpdC1pc2FhYy1pbWFnZS1hc3BlY3QtcmF0aW8tdGVycmFmb3JtYXRpb24udmVyY2VsLmFwcCJ9fV19
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **terraware-web-storybook** | ✅ Ready ([Inspect](https://vercel.com/terraformation/terraware-web-storybook/BJ9DfxrwCEU7AJPUA9Ky4eyVHdaS)) | [Visit Preview](https://terraware-web-storybook-git-isaac-image-a-00b40f-terraformation.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/terraware-web-storybook-git-isaac-image-a-00b40f-terraformation.vercel.app?via=pr-comment-feedback-link) | May 6, 2025 9:31pm |
| **tree-location-web** | ✅ Ready ([Inspect](https://vercel.com/terraformation/tree-location-web/DrPyLLH7XWeU7S44G5KxRe4t5oPc)) | [Visit Preview](https://tree-location-web-git-isaac-image-aspect-ratio-terraformation.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/tree-location-web-git-isaac-image-aspect-ratio-terraformation.vercel.app?via=pr-comment-feedback-link) | May 6, 2025 9:31pm |



author_association: NONE
user: login: ice1080
created_at: 2025-05-06T15:43:14Z
body: * **#3958** <a href="https://app.graphite.dev/github/pr/terraware/terraware-web/3958?utm_source=stack-comment-icon" target="_blank"><img src="https://static.graphite.dev/graphite-32x32-black.png" alt="Graphite" width="10px" height="10px"/></a>
* **#3957** <a href="https://app.graphite.dev/github/pr/terraware/terraware-web/3957?utm_source=stack-comment-icon" target="_blank"><img src="https://static.graphite.dev/graphite-32x32-black.png" alt="Graphite" width="10px" height="10px"/></a> 👈 <a href="https://app.graphite.dev/github/pr/terraware/terraware-web/3957?utm_source=stack-comment-view-in-graphite" target="_blank">(View in Graphite)</a>
* **#3951** <a href="https://app.graphite.dev/github/pr/terraware/terraware-web/3951?utm_source=stack-comment-icon" target="_blank"><img src="https://static.graphite.dev/graphite-32x32-black.png" alt="Graphite" width="10px" height="10px"/></a>
* `main`


<h2></h2>

This stack of pull requests is managed by <a href="https://graphite.dev?utm-source=stack-comment"><b>Graphite</b></a>. Learn more about <a href="https://stacking.dev/?utm_source=stack-comment">stacking</a>.
<!-- Current dependencies on/for this PR: -->
author_association: CONTRIBUTOR
user: login: ice1080
created_at: 2025-05-06T21:23:51Z
body: ### Merge activity

* **May 6, 5:23 PM EDT**: A user started a stack merge that includes this pull request via [Graphite](https://app.graphite.dev/github/pr/terraware/terraware-web/3957).
* **May 6, 5:25 PM EDT**: [Graphite](https://app.graphite.dev/github/pr/terraware/terraware-web/3957) rebased this pull request as part of a merge.
* **May 6, 5:36 PM EDT**: @ice1080 merged this pull request with [Graphite](https://app.graphite.dev/github/pr/terraware/terraware-web/3957).

author_association: CONTRIBUTOR