Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ImagePane: Gallery and refactor #539

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
4 changes: 2 additions & 2 deletions pages/api/og-image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,12 @@ export default async (req: NextApiRequest, res: NextApiResponse) => {

const t2 = Date.now();
const image = await getImageFromApi(def);
if (!image) {
if (image.length === 0) {
throw new Error(`Image failed to load from API: ${JSON.stringify(def)}`);
}

const t3 = Date.now();
const svg = await renderSvg(feature, def, image);
const svg = await renderSvg(feature, def, image[0]);

if (req.query.svg) {
sendImageResponse(res, feature, svg, SVG_TYPE);
Expand Down
44 changes: 19 additions & 25 deletions src/components/FeaturePanel/CragsInArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ import { Feature, isInstant, OsmId } from '../../services/types';
import { useMobileMode } from '../helpers';
import { getLabel } from '../../helpers/featureLabel';

import { Slider, Wrapper } from './ImagePane/FeatureImages';
import { Image } from './ImagePane/Image/Image';
import { FeatureImagesUi } from './ImagePane/FeatureImages';
import { getInstantImage } from '../../services/images/getImageDefs';

const ArrowIcon = styled(ArrowForwardIosIcon)`
Expand Down Expand Up @@ -90,19 +89,7 @@ const Header = ({
</HeadingRow>
);

const Gallery = ({ images }) => {
return (
<Wrapper>
<Slider>
{images.map((item) => (
<Image key={item.image.imageUrl} def={item.def} image={item.image} />
))}
</Slider>
</Wrapper>
);
};

const getOnClickWithHash = (apiId: OsmId) => (e) => {
const getOnClickWithHash = (apiId: OsmId) => (e: React.MouseEvent) => {
e.preventDefault();
Router.push(`/${getUrlOsmId(apiId)}${window.location.hash}`);
};
Expand All @@ -119,21 +106,28 @@ const CragItem = ({ feature }: { feature: Feature }) => {
})) ?? [];

return (
<Link
href={`/${getUrlOsmId(feature.osmMeta)}`}
onClick={getOnClickWithHash(feature.osmMeta)}
onMouseEnter={mobileMode ? undefined : handleHover}
onMouseLeave={() => setPreview(null)}
>
<Container>
<Container>
<Link
href={`/${getUrlOsmId(feature.osmMeta)}`}
onClick={getOnClickWithHash(feature.osmMeta)}
onMouseEnter={mobileMode ? undefined : handleHover}
onMouseLeave={() => setPreview(null)}
>
<Header
label={getLabel(feature)}
routesCount={feature.members?.length}
imagesCount={images.length}
/>
{images.length ? <Gallery images={images} /> : null}
</Container>
</Link>
</Link>
{!!images.length && (
<FeatureImagesUi
groups={images.map(({ def, image }) => ({
def,
images: [image],
}))}
/>
)}
</Container>
);
};

Expand Down
103 changes: 84 additions & 19 deletions src/components/FeaturePanel/ImagePane/FeatureImages.tsx
Original file line number Diff line number Diff line change
@@ -1,48 +1,113 @@
import React from 'react';
import styled from '@emotion/styled';
import { Scrollbars } from 'react-custom-scrollbars';
import { Image } from './Image/Image';
import { useLoadImages } from './useLoadImages';
import { ImageGroup, useLoadImages } from './useLoadImages';
import { NoImage } from './NoImage';
import { HEIGHT, ImageSkeleton } from './helpers';
import { HEIGHT, ImageSkeleton, isElementVisible } from './helpers';
import { Gallery } from './Gallery';
import { SwitchImageArrows } from './SwitchImageArrow';
import { ImageDef } from '../../../services/types';
import { ImageType } from '../../../services/images/getImageDefs';

export const Wrapper = styled.div`
width: 100%;
height: calc(${HEIGHT}px + 10px); // 10px for scrollbar
min-height: calc(${HEIGHT}px + 10px); // otherwise it shrinks b/c of flex
width: 100%;
position: relative;
`;

const StyledScrollbars = styled(Scrollbars)`
const StyledScrollbars = styled.div`
width: 100%;
height: 100%;

white-space: nowrap;
text-align: center; // one image centering

overflow-y: hidden;
display: flex;
gap: 3px;
overflow: hidden;
overflow-x: auto;
scroll-snap-type: x mandatory;

scroll-behavior: smooth;
-webkit-overflow-scrolling: touch;
`;
export const Slider = ({ children }) => (
<StyledScrollbars universal autoHide>

export const Slider = ({
children,
onScroll,
}: {
children: React.ReactNode;
onScroll?: () => void;
}) => (
<StyledScrollbars
onScroll={() => {
if (onScroll) onScroll();
}}
>
{children}
</StyledScrollbars>
);

export const FeatureImages = () => {
const { loading, images } = useLoadImages();

if (images.length === 0) {
return <Wrapper>{loading ? <ImageSkeleton /> : <NoImage />}</Wrapper>;
}
export const FeatureImagesUi: React.FC<{ groups: ImageGroup[] }> = ({
groups,
}) => {
const [rightBouncing, setRightBouncing] = React.useState(true);
const [visibleIndex, setVisibleIndex] = React.useState(0);
const galleryRefs = React.useRef<React.RefObject<HTMLDivElement>[]>([]);
galleryRefs.current = groups.map(
(_, i) => galleryRefs.current[i] ?? React.createRef(),
);

return (
<Wrapper>
<Slider>
{images.map((item) => (
<Image key={item.image.imageUrl} def={item.def} image={item.image} />
<SwitchImageArrows
rightBouncing={rightBouncing}
showLeft={visibleIndex !== 0}
showRight={visibleIndex !== groups.length - 1}
onClick={(pos) => {
setRightBouncing(false);

const target =
pos === 'right'
? galleryRefs.current[visibleIndex + 1]
: galleryRefs.current[visibleIndex - 1];

target?.current.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'center',
});
}}
/>
<Slider
onScroll={() => {
const currentIndex = galleryRefs.current.findIndex(
({ current: el }) => isElementVisible(el),
);
setVisibleIndex(currentIndex);
setRightBouncing(false);
}}
>
{groups.map((group, i) => (
<>
<Gallery
key={i}
ref={galleryRefs.current[i]}
def={group.def}
images={group.images}
/>
</>
))}
</Slider>
</Wrapper>
);
};

export const FeatureImages = () => {
const { loading, groups } = useLoadImages();

if (groups.length === 0) {
return <Wrapper>{loading ? <ImageSkeleton /> : <NoImage />}</Wrapper>;
}

return <FeatureImagesUi groups={groups} />;
};
147 changes: 147 additions & 0 deletions src/components/FeaturePanel/ImagePane/Gallery.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import React from 'react';
import { ImageDef, isTag } from '../../../services/types';
import { ImageType } from '../../../services/images/getImageDefs';
import styled from '@emotion/styled';
import { PanoramaImg } from './Image/PanoramaImg';
import { GalleryDialog } from './GalleryDialog';
import { UncertainCover } from './Image/UncertainCover';
import { InfoButton } from './Image/InfoButton';
import { PathsSvg } from './PathsSvg';
import { Image } from './Image';
import { SeeMoreButton } from './SeeMore';
import { calculateImageSize } from './helpers';

type GalleryProps = {
def: ImageDef;
images: ImageType[];
};

const GalleryWrapper = styled.div`
scroll-snap-align: center;
width: 100%;
height: 100%;
flex: none;
position: relative;
overflow: hidden;
`;

type GallerySlotProps = {
images: ImageType[];
def: ImageDef;
onSeeMore: () => void;
index: number;
};

const GallerySlot: React.FC<GallerySlotProps> = ({
images,
def,
onSeeMore,
index,
}) => {
const image = images[index];
const isLastImg = images.length - 1 === index;
const isBlurredButton = !isLastImg && images.length > 4 && index === 3;
const hasPaths =
isTag(def) && !!(def.path?.length || def.memberPaths?.length);

const imgRef = React.useRef<HTMLImageElement>();

if (isBlurredButton) {
return (
<SeeMoreButton
key={image.imageUrl}
image={image}
more={images.length - 3}
onClick={() => {
onSeeMore();
}}
/>
);
}

return (
<Image
key={image.imageUrl}
image={image}
def={def}
high={images.length <= 2 || (images.length === 3 && index === 1)}
wide={images.length === 1}
ref={imgRef}
>
{hasPaths && imgRef.current && (
<PathsSvg def={def} size={calculateImageSize(imgRef.current)} />
)}
</Image>
);
};

const GalleryInner: React.FC<GalleryProps> = ({ def, images }) => {
const [opened, setOpened] = React.useState(false);

const panorama = images.find(({ panoramaUrl }) => panoramaUrl);
if (panorama) {
return <PanoramaImg url={panorama.panoramaUrl} />;
}

return (
<>
<GalleryDialog
images={images}
def={def}
opened={opened}
onClose={() => {
setOpened(false);
}}
/>

<div
style={{
display: 'grid',
gridTemplateRows: '1fr 1fr',
gridTemplateColumns: '1fr 1fr',
height: '100%',
width: '100%',
}}
>
{images.slice(0, 4).map((_, i) => (
<GallerySlot
key={i}
images={images}
def={def}
index={i}
onSeeMore={() => {
setOpened(true);
}}
/>
))}
</div>
</>
);
};

export const Gallery = React.forwardRef<HTMLDivElement, GalleryProps>(
({ def, images }, ref) => {
const showUncertainCover =
images.some(({ uncertainImage }) => uncertainImage) &&
!images.some(({ panoramaUrl }) => panoramaUrl);

const internalRef = React.useRef<HTMLDivElement>();

return (
<GalleryWrapper ref={ref}>
<div
ref={internalRef}
style={{
display: 'contents',
}}
>
<GalleryInner def={def} images={images} />
<InfoButton images={images} />
{showUncertainCover && <UncertainCover />}
</div>
</GalleryWrapper>
);
},
);

Gallery.displayName = 'Gallery';
Loading
Loading