Skip to content
This repository has been archived by the owner on Nov 13, 2024. It is now read-only.

Commit

Permalink
dex-1017 - individual photo gallery (#444)
Browse files Browse the repository at this point in the history
* Restore individual gallery link, add route and page component

* Add basic individual gallery page states, without gallery

* Add asset gallery

* Handle clamped gallery images, display info alert

* Add asset metadata

* Justify gallery left instead of center

* Handle image load error

* Add alt text to gallery images and description to image error

* Move const outside of component, memoize transformToAssets, replace transform with reduce
  • Loading branch information
Emily-Ke authored Aug 16, 2022
1 parent 8e88638 commit 0104ac0
Show file tree
Hide file tree
Showing 12 changed files with 490 additions and 28 deletions.
11 changes: 10 additions & 1 deletion locale/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,8 @@
"IDENTIFICATION": "Identification",
"WAITING_ELLIPSES": "Waiting...",
"REPORTED_BY": "Reported by ",
"REPORTED_BY_USER": "Reported by <link>{name}</link>",
"REPORTED_BY_UNNAMED_USER": "Reported by <link>Unnamed User</link>",
"GENERAL_SETTINGS": "General settings",
"ONE_SIGHTING_TOGGLE_DESCRIPTION": "This sighting has only one individual and I would like to report metadata about it now.",
"FRONT_PAGE": "Front page",
Expand Down Expand Up @@ -1028,6 +1030,7 @@
"COLLAB_REVOKE_ERROR_SUPPLEMENTAL": "Note that collaborations cannot be revoked unless they are mutually approved.",
"PENDING_SIGHTINGS": "Pending Sightings",
"UNKNOWN_DATE": "Unknown date",
"UNKNOWN_ENCOUNTER_DATE": "unknown encounter date",
"CREATED_ON_DATE": "Created on {createdDate}",
"SENT_YOU_A_COLLABORATION_REQUEST": "{userName} sent you a collaboration request",
"A_COLLABORATION_WAS_CREATED_ON_YOUR_BEHALF": "A collaboration was created on your behalf",
Expand Down Expand Up @@ -1181,5 +1184,11 @@
"STATUS_IDENTIFICATION_SKIPPED_NO_IMAGES": "Identification skipped - no images to match.",
"STATUS_IDENTIFICATION_SKIPPED_MIGRATED_FROM_SITE": "Identification skipped - this sighting was migrated from {migratedSiteName}.",
"STATUS_IDENTIFICATION_SKIPPED_MIGRATED_FROM_UNKNOWN_SITE": "Identification skipped - this sighting was migrated from a Wildbook database.",
"STATUS_IDENTIFICATION_FAILED": "Identification failed."
"STATUS_IDENTIFICATION_FAILED": "Identification failed.",
"INDIVIDUAL_GALLERY_SEE_ALL": "See all",
"INDIVIDUAL_GALLERY_TITLE": "{name}'s gallery",
"INDIVIDUAL_GALLERY_IMAGE_ALT": "{annotationCount} {annotationCount, plural, one {annotation} other {annotations}} with {assetClassCount, plural, one {class} other {classes}} {assetClassesList}",
"INDIVIDUAL_GALLERY_UNABLE_TO_DISPLAY_ANNOTATIONS": "We are currently unable to show annotations for uploaded images with either a width or a height exceeding 4,096 pixels.",
"INDIVIDUAL_BACK_TO_PROFILE": "Return to {name}'s profile",
"ERROR_FETCHING_IMAGE": "Error fetching image"
}
4 changes: 4 additions & 0 deletions src/AuthenticatedSwitch.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import ControlPanel from './pages/controlPanel/ControlPanel';
import AssignEncounters from './pages/assignEncounters/AssignEncounters';
import CreateIndividual from './pages/createIndividual/CreateIndividual';
import Individual from './pages/individual/Individual';
import IndividualGallery from './pages/individualGallery/IndividualGallery';
// import PictureBook from './pages/individual/PictureBook';
import Sighting from './pages/sighting/Sighting';
import AssetGroupSighting from './pages/sighting/AssetGroupSighting';
Expand Down Expand Up @@ -141,6 +142,9 @@ export default function AuthenticatedSwitch({
{/* <Route path="/individuals/picturebook">
<PictureBook />
</Route> */}
<Route path="/individuals/:guid/gallery">
<IndividualGallery />
</Route>
<Route path="/individuals/:id">
<Individual />
</Route>
Expand Down
13 changes: 11 additions & 2 deletions src/components/cards/GalleryCard.jsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';

import { useTheme } from '@material-ui/core/styles';
import useMediaQuery from '@material-ui/core/useMediaQuery';
import ImageList from '@material-ui/core/ImageList';
import ImageListItem from '@material-ui/core/ImageListItem';
// import Link from '../Link';
import Link from '../Link';
import Card from './Card';

export default function GalleryCard({
title,
titleId = 'GALLERY',
individualGuid,
assets,
}) {
const theme = useTheme();
Expand All @@ -20,7 +23,13 @@ export default function GalleryCard({
<Card
title={title}
titleId={titleId}
// renderActions={<Link>See all</Link>}
renderActions={
individualGuid ? (
<Link to={`/individuals/${individualGuid}/gallery`}>
<FormattedMessage id="INDIVIDUAL_GALLERY_SEE_ALL" />
</Link>
) : null
}
overflow="hidden"
overflowX="hidden"
>
Expand Down
30 changes: 5 additions & 25 deletions src/components/dataDisplays/cellRenderers/LocationRenderer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,11 @@ import React, { forwardRef, useState } from 'react';
import { get } from 'lodash-es';
import { useIntl } from 'react-intl';

import DialogContent from '@material-ui/core/DialogContent';
import DialogActions from '@material-ui/core/DialogActions';

import useOptions from '../../../hooks/useOptions';
import StandardDialog from '../../StandardDialog';
import SinglePoint from '../../maps/SinglePoint';
import Button from '../../Button';
import Text from '../../Text';
import OverflowController from './OverflowController';
import SinglePointDialog from '../../dialogs/SinglePointDialog';

function Core({ value, lat, lng, ...rest }, ref) {
const [dialogOpen, setDialogOpen] = useState(false);
Expand All @@ -24,28 +20,12 @@ function Core({ value, lat, lng, ...rest }, ref) {

return (
<>
<StandardDialog
<SinglePointDialog
open={dialogOpen}
onClose={() => setDialogOpen(false)}
titleId="GPS_TITLE"
>
{dialogOpen && (
<>
<DialogContent style={{ marginBottom: 24 }}>
<div style={{ height: 400, width: 480 }}>
<SinglePoint lat={lat} lng={lng} />
</div>
</DialogContent>
<DialogActions style={{ padding: '0px 24px 24px 24px' }}>
<Button
id="CLOSE"
display="basic"
onClick={() => setDialogOpen(false)}
/>
</DialogActions>
</>
)}
</StandardDialog>
lat={lat}
lng={lng}
/>
<Button
display="link"
onClick={() => setDialogOpen(true)}
Expand Down
32 changes: 32 additions & 0 deletions src/components/dialogs/SinglePointDialog.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React from 'react';

import DialogContent from '@material-ui/core/DialogContent';
import DialogActions from '@material-ui/core/DialogActions';

import Button from '../Button';
import SinglePoint from '../maps/SinglePoint';
import StandardDialog from '../StandardDialog';

export default function SinglePointDialog({
open,
onClose,
lat,
lng,
}) {
return (
<StandardDialog open={open} onClose={onClose} titleId="GPS_TITLE">
{open && (
<>
<DialogContent style={{ marginBottom: 24 }}>
<div style={{ height: 400, width: 480 }}>
<SinglePoint lat={lat} lng={lng} />
</div>
</DialogContent>
<DialogActions style={{ padding: '0px 24px 24px 24px' }}>
<Button id="CLOSE" display="basic" onClick={onClose} />
</DialogActions>
</>
)}
</StandardDialog>
);
}
1 change: 1 addition & 0 deletions src/pages/individual/Individual.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,7 @@ export default function Individual() {
{ id: 'PHOTOS_OF' },
{ name: firstName },
)}
individualGuid={id}
assets={assetSources}
/>
<MetadataCard
Expand Down
169 changes: 169 additions & 0 deletions src/pages/individualGallery/IndividualGallery.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import React, { useMemo } from 'react';
import { useIntl } from 'react-intl';
import { useParams } from 'react-router-dom';
import { capitalize } from 'lodash-es';

import { useTheme } from '@material-ui/core/styles';

import useDocumentTitle from '../../hooks/useDocumentTitle';
import useIndividual from '../../models/individual/useIndividual';
import { deriveIndividualName } from '../../utils/nameUtils';
import errorTypes from '../../constants/errorTypes';
import CustomAlert from '../../components/Alert';
import Button from '../../components/Button';
import Link from '../../components/Link';
import LoadingScreen from '../../components/LoadingScreen';
import MainColumn from '../../components/MainColumn';
import SadScreen from '../../components/SadScreen';
import Text from '../../components/Text';
import GalleryItem from './components/GalleryItem';

const gridColumnWidth = 300;

function transformToAssets(individualData) {
if (!individualData?.encounters) return [];

const dataByAsset = individualData.encounters.reduce(
(memo, encounter) => {
const sharedEncounterData = {
owner: {
guid: encounter?.owner?.guid,
fullName: encounter?.owner?.full_name,
},
time: {
time: encounter?.time,
timeSpecificity: encounter?.timeSpecificity,
},
location: {
label: encounter?.locationId_value,
decimalLatitude: encounter?.decimalLatitude,
decimalLongitude: encounter?.decimalLongitude,
},
};

// Each asset needs information from the annotation that it belongs to.
// Every encounter annotation will be used, but multiple annotations may have the same asset.
if (encounter?.annotations) {
encounter.annotations.forEach(annotation => {
const {
guid,
bounds,
ia_class: iAClass,
} = annotation || {};
const annotationData = { guid, bounds, iAClass };
const assetGuid = annotation?.asset_guid;

if (assetGuid) {
if (!memo[assetGuid]) {
const assetMetadata = { src: annotation.asset_src };

memo[assetGuid] = {
guid: assetGuid,
metadata: assetMetadata,
annotations: [],
...sharedEncounterData,
};
}
memo[assetGuid].annotations.push(annotationData);
}
});
}

return memo;
},
{},
);

return Object.values(dataByAsset);
}

export default function IndividualGallery() {
const intl = useIntl();
const theme = useTheme();
const { guid } = useParams();

const { data, statusCode, loading, error } = useIndividual(guid);

const galleryAssets = useMemo(
() => transformToAssets(data),
[data],
);

const name = deriveIndividualName(
data,
'FirstName',
intl.formatMessage({ id: 'UNNAMED_INDIVIDUAL' }),
);

useDocumentTitle('INDIVIDUAL_GALLERY_TITLE', {
messageValues: { name: capitalize(name) },
refreshKey: name,
});

if (error) {
return (
<SadScreen
statusCode={statusCode}
variantOverrides={{
[errorTypes.notFound]: {
subtitleId: 'INDIVIDUAL_NOT_FOUND',
descriptionId: 'INDIVIDUAL_NOT_FOUND_DESCRIPTION',
},
}}
/>
);
}

if (loading) return <LoadingScreen />;

return (
<MainColumn style={{ maxWidth: theme.breakpoints.values.xl }}>
<Button
display="back"
id="INDIVIDUAL_BACK_TO_PROFILE"
values={{ name }}
style={{ marginTop: 8 }}
component={Link}
to={`/individuals/${guid}`}
noUnderline
/>
<Text
variant="h3"
style={{ padding: '4px 0 16px 16px' }}
id="PHOTOS_OF"
values={{ name }}
/>
<div
style={{
display: 'grid',
gap: 12,
gridTemplateColumns: `repeat(auto-fit, ${gridColumnWidth}px)`,
margin: '0 16px 16px 16px',
}}
>
<CustomAlert
style={{ gridColumn: '1 / -1' }}
severity="info"
descriptionId="INDIVIDUAL_GALLERY_UNABLE_TO_DISPLAY_ANNOTATIONS"
/>
</div>
<ul
style={{
listStyle: 'none',
display: 'grid',
gap: '24px 12px',
padding: '0 16px',
gridTemplateColumns: `repeat(auto-fit, ${gridColumnWidth}px)`,
}}
>
{galleryAssets.map(galleryAsset => (
<GalleryItem
key={galleryAsset.guid}
{...galleryAsset}
width={gridColumnWidth}
/>
))}
</ul>
</MainColumn>
);
}
Loading

0 comments on commit 0104ac0

Please sign in to comment.