From e70f06942edad379d4bbeb0c3502a40e958f1a0c Mon Sep 17 00:00:00 2001 From: Dlurak <84224239+Dlurak@users.noreply.github.com> Date: Fri, 13 Sep 2024 16:31:36 +0200 Subject: [PATCH 01/11] Add a simple gallery --- .../FeaturePanel/ImagePane/FeatureImages.tsx | 33 +++-- .../FeaturePanel/ImagePane/Gallery.tsx | 76 ++++++++++++ .../ImagePane/Image/PanoramaImg.tsx | 14 ++- .../FeaturePanel/ImagePane/useLoadImages.tsx | 56 ++++++--- src/services/images/getImageFromApi.ts | 117 +++++++++++------- 5 files changed, 219 insertions(+), 77 deletions(-) create mode 100644 src/components/FeaturePanel/ImagePane/Gallery.tsx diff --git a/src/components/FeaturePanel/ImagePane/FeatureImages.tsx b/src/components/FeaturePanel/ImagePane/FeatureImages.tsx index 0fb8cddc..558758cc 100644 --- a/src/components/FeaturePanel/ImagePane/FeatureImages.tsx +++ b/src/components/FeaturePanel/ImagePane/FeatureImages.tsx @@ -1,46 +1,53 @@ 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 { NoImage } from './NoImage'; import { HEIGHT, ImageSkeleton } from './helpers'; +import { Gallery } from './Gallery'; 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%; `; -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 }) => ( - - {children} - + {children} ); export const FeatureImages = () => { - const { loading, images } = useLoadImages(); + const { loading, groups } = useLoadImages(); - if (images.length === 0) { + if (groups.length === 0) { return {loading ? : }; } return ( - {images.map((item) => ( - + {groups.map((group, i) => ( + ))} diff --git a/src/components/FeaturePanel/ImagePane/Gallery.tsx b/src/components/FeaturePanel/ImagePane/Gallery.tsx new file mode 100644 index 00000000..25d1c7bd --- /dev/null +++ b/src/components/FeaturePanel/ImagePane/Gallery.tsx @@ -0,0 +1,76 @@ +import React from 'react'; +import { ImageDef } from '../../../services/types'; +import { + getImageDefId, + ImageType, +} from '../../../services/images/getImageDefs'; +import styled from '@emotion/styled'; +import { Image } from './Image/Image'; +import { PanoramaImg } from './Image/PanoramaImg'; +import { ImageList, ImageListItem } from '@mui/material'; + +type GalleryProps = { + def: ImageDef; + images: ImageType[]; + isFirst: boolean; +}; + +const GalleryWrapper = styled.div` + scroll-snap-align: center; + width: 100%; + height: 100%; + flex: none; + position: relative; + overflow: hidden; +`; + +const GalleryInner: React.FC = ({ def, images }) => { + const panorama = images.find(({ panoramaUrl }) => panoramaUrl); + if (panorama) { + return ; + } + + return ( +
+ {images.slice(0, 4).map((image, i) => { + const isLastImg = images.length - 1 === i; + const isBlurredButton = !isLastImg && images.length > 4 && i === 3; + + if (isBlurredButton) { + // TODO: styling and implementation + return ; + } + + return ( +
+ ); + })} +
+ ); +}; + +export const Gallery: React.FC = ({ def, images, isFirst }) => ( + + + +); diff --git a/src/components/FeaturePanel/ImagePane/Image/PanoramaImg.tsx b/src/components/FeaturePanel/ImagePane/Image/PanoramaImg.tsx index 1c67458c..3cc96029 100644 --- a/src/components/FeaturePanel/ImagePane/Image/PanoramaImg.tsx +++ b/src/components/FeaturePanel/ImagePane/Image/PanoramaImg.tsx @@ -1,8 +1,20 @@ import React from 'react'; import { encodeUrl } from '../../../../helpers/utils'; +import { useState, useEffect } from 'react'; + export const PanoramaImg = ({ url }: { url: string }) => { - const configUrl = `${window.location.protocol}//${window.location.host}/pannellum-config.json`; + const [configUrl, setConfigUrl] = useState(null); + + useEffect(() => { + const currentConfigUrl = `${window.location.protocol}//${window.location.host}/pannellum-config.json`; + setConfigUrl(currentConfigUrl); + }, []); + + if (!configUrl) { + return null; + } + const pannellumUrl = encodeUrl`https://cdn.pannellum.org/2.5/pannellum.htm#panorama=${url}&config=${configUrl}`; return ( diff --git a/src/components/FeaturePanel/ImagePane/useLoadImages.tsx b/src/components/FeaturePanel/ImagePane/useLoadImages.tsx index d5406366..cf3030d9 100644 --- a/src/components/FeaturePanel/ImagePane/useLoadImages.tsx +++ b/src/components/FeaturePanel/ImagePane/useLoadImages.tsx @@ -8,35 +8,57 @@ import { getImageFromApi } from '../../../services/images/getImageFromApi'; import { useFeatureContext } from '../../utils/FeatureContext'; import { ImageDef, isInstant } from '../../../services/types'; -export type ImagesType = { def: ImageDef; image: ImageType }[]; +export type ImageGroup = { def: ImageDef; images: ImageType[] }[]; export const mergeResultFn = - (def: ImageDef, image: ImageType, defs: ImageDef[]) => - (prevImages: ImagesType) => { - if (image == null) { + (def: ImageDef, images: ImageType[], defs: ImageDef[]) => + (prevImages: ImageGroup): ImageGroup => { + if (images.length === 0) { return prevImages; } - const found = prevImages.find( - (item) => item.image?.imageUrl === image.imageUrl, + const imageUrls = images.map(({ imageUrl }) => imageUrl); + + const found = prevImages.find((group) => + group.images.some((img) => imageUrls.includes(img.imageUrl)), ); + if (found) { - (found.image.sameUrlResolvedAlsoFrom ??= []).push(image); - return [...prevImages]; + // Update existing group + const updatedGroup = { + ...found, + images: found.images.map((img) => { + if (!imageUrls.includes(img.imageUrl)) return img; + + return { + ...img, + sameUrlResolvedAlsoFrom: [ + ...(img.sameUrlResolvedAlsoFrom ?? []), + ...images, + ], + }; + }), + }; + + return prevImages.map((group) => + group.def === found.def ? updatedGroup : group, + ); } - const sorted = [...prevImages, { def, image }].sort((a, b) => { + // Add new group + const sorted = [...prevImages, { def, images }].sort((a, b) => { const aIndex = defs.findIndex((item) => item === a.def); const bIndex = defs.findIndex((item) => item === b.def); return aIndex - bIndex; }); + return sorted; }; -const getInitialState = (defs: ImageDef[]) => +const getInitialState = (defs: ImageDef[]): ImageGroup => defs?.filter(isInstant)?.map((def) => ({ def, - image: getInstantImage(def), + images: getInstantImage(def) ? [getInstantImage(def)] : [], })) ?? []; export const useLoadImages = () => { @@ -46,13 +68,13 @@ export const useLoadImages = () => { const initialState = useMemo(() => getInitialState(defs), [defs]); const [loading, setLoading] = useState(apiDefs.length > 0); - const [images, setImages] = useState(initialState); + const [groups, setGroups] = useState(initialState); useEffect(() => { - setImages(initialState); + setGroups(initialState); const promises = apiDefs.map(async (def) => { - const image = await getImageFromApi(def); - setImages(mergeResultFn(def, image, defs)); + const images = await getImageFromApi(def); + setGroups(mergeResultFn(def, images, defs)); }); Promise.all(promises).then(() => { @@ -60,6 +82,6 @@ export const useLoadImages = () => { }); }, [apiDefs, defs, initialState]); - publishDbgObject('last images', images); - return { loading, images: images.filter((item) => item.image != null) }; + publishDbgObject('last images', groups); + return { loading, groups: groups.filter((group) => group.images.length > 0) }; }; diff --git a/src/services/images/getImageFromApi.ts b/src/services/images/getImageFromApi.ts index 3772226f..b648a7a5 100644 --- a/src/services/images/getImageFromApi.ts +++ b/src/services/images/getImageFromApi.ts @@ -1,63 +1,78 @@ -import { fetchJson } from '../fetch'; -import { isInstant, ImageDef, isCenter, isTag } from '../types'; -import { getMapillaryImage } from './getMapillaryImage'; -import { getFodyImage } from './getFodyImage'; -import { getInstantImage, WIDTH, ImageType } from './getImageDefs'; import { encodeUrl } from '../../helpers/utils'; +import { fetchJson } from '../fetch'; +import { ImageDef, isCenter, isInstant, isTag } from '../types'; + import { getCommonsImageUrl } from './getCommonsImageUrl'; +import { getFodyImage } from './getFodyImage'; +import { getInstantImage, ImageType, WIDTH } from './getImageDefs'; +import { getMapillaryImage } from './getMapillaryImage'; -type ImagePromise = Promise; +type ImagePromise = Promise; const getCommonsFileApiUrl = (title: string) => - encodeUrl`https://commons.wikimedia.org/w/api.php?action=query&prop=imageinfo&iiprop=url&iiurlwidth=${WIDTH}&format=json&titles=${title}&origin=*`; + encodeUrl`https://commons.wikimedia.org/w/api.php?action=query&prop=imageinfo&iiprop=url&iiurlwidth=${ + WIDTH + }&format=json&titles=${title}&origin=*`; const fetchCommonsFile = async (k: string, v: string): ImagePromise => { const url = getCommonsFileApiUrl(v); const data = await fetchJson(url); const page = Object.values(data.query.pages)[0] as any; if (!page.imageinfo?.length) { - return null; + return []; } const image = page.imageinfo[0]; - return { - imageUrl: decodeURI(image.thumburl), - description: `Wikimedia Commons (${k}=*)`, - link: page.title, - linkUrl: image.descriptionshorturl, - // portrait: images[0].thumbwidth < images[0].thumbheight, - }; + return [ + { + imageUrl: decodeURI(image.thumburl), + description: `Wikimedia Commons (${k}=*)`, + link: page.title, + linkUrl: image.descriptionshorturl, + // portrait: images[0].thumbwidth < images[0].thumbheight, + }, + ]; }; -// TODO perhaps fetch more images, or create a collage of first few images +const isAudioUrl = (url: string) => + url.endsWith('.ogg') || url.endsWith('.mp3'); + const getCommonsCategoryApiUrl = (title: string) => - encodeUrl`https://commons.wikimedia.org/w/api.php?action=query&generator=categorymembers&gcmtitle=${title}&gcmlimit=1&gcmtype=file&prop=imageinfo&&iiprop=url&iiurlwidth=${WIDTH}&format=json&origin=*`; + encodeUrl`https://commons.wikimedia.org/w/api.php?action=query&generator=categorymembers&gcmtitle=${ + title + }&gcmlimit=6&gcmtype=file&prop=imageinfo&iiprop=url&iiurlwidth=${ + WIDTH + }&format=json&origin=*`; const fetchCommonsCategory = async (k: string, v: string): ImagePromise => { const url = getCommonsCategoryApiUrl(v); const data = await fetchJson(url); - const page = Object.values(data.query.pages)[0] as any; - if (!page.imageinfo?.length) { - return null; - } - const image = page.imageinfo[0]; - return { - imageUrl: decodeURI(image.thumburl), + const pages = Object.values(data.query.pages); + const imageInfos = pages + .map((page: any) => page.imageinfo?.at(0)) + .filter((x) => x !== undefined) + .filter(({ url }) => !isAudioUrl(url)); + const thumbs = imageInfos.map(({ thumburl }) => thumburl); + + return thumbs.map((thumburl) => ({ + imageUrl: decodeURI(thumburl), description: `Wikimedia Commons category (${k}=*)`, link: v, linkUrl: `https://commons.wikimedia.org/wiki/${v}`, // portrait: images[0].thumbwidth < images[0].thumbheight, - }; + })); }; const getWikidataApiUrl = (entity: string) => - encodeUrl`https://www.wikidata.org/w/api.php?action=wbgetclaims&property=P18&format=json&entity=${entity}&origin=*`; + encodeUrl`https://www.wikidata.org/w/api.php?action=wbgetclaims&property=P18&format=json&entity=${ + entity + }&origin=*`; const fetchWikidata = async (entity: string): ImagePromise => { const url = getWikidataApiUrl(entity); const data = await fetchJson(url); if (!data.claims?.P18) { - return null; + return []; } const imagesP18 = data.claims.P18; @@ -67,12 +82,14 @@ const fetchWikidata = async (entity: string): ImagePromise => { : []; const entry = candidates.length > 0 ? candidates[0] : imagesP18[0]; const file = `File:${entry.mainsnak.datavalue.value}`; - return { - imageUrl: decodeURI(getCommonsImageUrl(file, WIDTH)), - description: 'Wikidata image (wikidata=*)', - link: entity, - linkUrl: `https://www.wikidata.org/wiki/${entity}`, - }; + return [ + { + imageUrl: decodeURI(getCommonsImageUrl(file, WIDTH)), + description: 'Wikidata image (wikidata=*)', + link: entity, + linkUrl: `https://www.wikidata.org/wiki/${entity}`, + }, + ]; }; const parseWikipedia = (k: string, v: string) => { @@ -88,7 +105,11 @@ const parseWikipedia = (k: string, v: string) => { }; const getWikipediaApiUrl = (country: string, title: string) => - encodeUrl`https://${country}.wikipedia.org/w/api.php?action=query&prop=pageimages&pithumbsize=${WIDTH}&format=json&titles=${title}&origin=*`; + encodeUrl`https://${ + country + }.wikipedia.org/w/api.php?action=query&prop=pageimages&pithumbsize=${ + WIDTH + }&format=json&titles=${title}&origin=*`; const fetchWikipedia = async (k: string, v: string): ImagePromise => { const { country, title } = parseWikipedia(k, v); @@ -96,26 +117,29 @@ const fetchWikipedia = async (k: string, v: string): ImagePromise => { const data = await fetchJson(url); const page = Object.values(data.query.pages)[0] as any; if (!page.pageimage) { - return null; + return []; } - return { - imageUrl: decodeURI(page.thumbnail.source), // it has to be decoded, because wikipedia encodes brackets (), but encodeURI doesnt - description: `Wikipedia (${k}=*)`, - link: `File:${page.pageimage}`, - linkUrl: `https://commons.wikimedia.org/wiki/File:${page.pageimage}`, - // portrait: page.thumbnail.width < page.thumbnail.height, - }; + return [ + { + imageUrl: decodeURI(page.thumbnail.source), // it has to be decoded, because wikipedia + // encodes brackets (), but encodeURI doesnt + description: `Wikipedia (${k}=*)`, + link: `File:${page.pageimage}`, + linkUrl: `https://commons.wikimedia.org/wiki/File:${page.pageimage}`, + // portrait: page.thumbnail.width < page.thumbnail.height, + }, + ]; }; export const getImageFromApiRaw = async (def: ImageDef): ImagePromise => { if (isCenter(def)) { const { service, center } = def; if (service === 'mapillary') { - return getMapillaryImage(center); + return getMapillaryImage(center).then((img) => [img]); } if (service === 'fody') { - return getFodyImage(center); + return getFodyImage(center).then((img) => [img]); } } @@ -144,12 +168,13 @@ export const getImageFromApiRaw = async (def: ImageDef): ImagePromise => { export const getImageFromApi = async (def: ImageDef): ImagePromise => { try { if (isInstant(def)) { - return getInstantImage(def); + const img = getInstantImage(def); + return img ? [img] : []; } return await getImageFromApiRaw(def); } catch (e) { console.warn(e); // eslint-disable-line no-console - return null; + return []; } }; From ca01c396bc34c2e6c354aa52eaee0b34ec2df450 Mon Sep 17 00:00:00 2001 From: Dlurak <84224239+Dlurak@users.noreply.github.com> Date: Fri, 13 Sep 2024 18:12:09 +0200 Subject: [PATCH 02/11] Make the design better --- .../FeaturePanel/ImagePane/FeatureImages.tsx | 2 +- .../FeaturePanel/ImagePane/Gallery.tsx | 110 +++++++++++++++--- .../FeaturePanel/ImagePane/useLoadImages.tsx | 3 +- 3 files changed, 97 insertions(+), 18 deletions(-) diff --git a/src/components/FeaturePanel/ImagePane/FeatureImages.tsx b/src/components/FeaturePanel/ImagePane/FeatureImages.tsx index 558758cc..fd932c5a 100644 --- a/src/components/FeaturePanel/ImagePane/FeatureImages.tsx +++ b/src/components/FeaturePanel/ImagePane/FeatureImages.tsx @@ -43,7 +43,7 @@ export const FeatureImages = () => { {groups.map((group, i) => ( ( + +); + +const MainImg = ({ url, alt }: { url: string; alt: string }) => ( + {alt} +); + +type ImageProps = { + image: ImageType; + def: ImageDef; + high: boolean; + wide: boolean; +}; + +const Image: React.FC = ({ image, def, high, wide }) => ( +
+ + +
+); + +type SeeMoreProps = { + image: ImageType; +}; + +const SeeMoreButton: React.FC = ({ image }) => ( + +); + const GalleryInner: React.FC = ({ def, images }) => { const panorama = images.find(({ panoramaUrl }) => panoramaUrl); if (panorama) { @@ -45,23 +130,16 @@ const GalleryInner: React.FC = ({ def, images }) => { const isBlurredButton = !isLastImg && images.length > 4 && i === 3; if (isBlurredButton) { - // TODO: styling and implementation - return ; + return ; } return ( -
); })} diff --git a/src/components/FeaturePanel/ImagePane/useLoadImages.tsx b/src/components/FeaturePanel/ImagePane/useLoadImages.tsx index cf3030d9..f1aaae2c 100644 --- a/src/components/FeaturePanel/ImagePane/useLoadImages.tsx +++ b/src/components/FeaturePanel/ImagePane/useLoadImages.tsx @@ -11,8 +11,9 @@ import { ImageDef, isInstant } from '../../../services/types'; export type ImageGroup = { def: ImageDef; images: ImageType[] }[]; export const mergeResultFn = - (def: ImageDef, images: ImageType[], defs: ImageDef[]) => + (def: ImageDef, imgs: (ImageType | null)[], defs: ImageDef[]) => (prevImages: ImageGroup): ImageGroup => { + const images = imgs.filter((x) => x); if (images.length === 0) { return prevImages; } From 32a43cfb33a9372d7cfb47ebaa1e3dd32d857dfd Mon Sep 17 00:00:00 2001 From: Dlurak <84224239+Dlurak@users.noreply.github.com> Date: Fri, 13 Sep 2024 19:06:05 +0200 Subject: [PATCH 03/11] Add a "view more" dialog --- .../FeaturePanel/ImagePane/Gallery.tsx | 141 ++++++++++++++---- src/helpers/featureLabel.ts | 4 +- src/services/images/getImageFromApi.ts | 2 +- 3 files changed, 114 insertions(+), 33 deletions(-) diff --git a/src/components/FeaturePanel/ImagePane/Gallery.tsx b/src/components/FeaturePanel/ImagePane/Gallery.tsx index bc8d3665..4db74563 100644 --- a/src/components/FeaturePanel/ImagePane/Gallery.tsx +++ b/src/components/FeaturePanel/ImagePane/Gallery.tsx @@ -6,6 +6,19 @@ import { } from '../../../services/images/getImageDefs'; import styled from '@emotion/styled'; import { PanoramaImg } from './Image/PanoramaImg'; +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + ImageList, + ImageListItem, + useTheme, + useMediaQuery, +} from '@mui/material'; +import { useFeatureContext } from '../../utils/FeatureContext'; +import { getLabel } from '../../../helpers/featureLabel'; type GalleryProps = { def: ImageDef; @@ -78,10 +91,13 @@ const Image: React.FC = ({ image, def, high, wide }) => ( type SeeMoreProps = { image: ImageType; + more: number; + onClick: () => void; }; -const SeeMoreButton: React.FC = ({ image }) => ( +const SeeMoreButton: React.FC = ({ image, more, onClick }) => ( ); +type GalleryDialogProps = { + images: ImageType[]; + def: ImageDef; + opened: boolean; + onClose: () => void; +}; + +const GalleryDialog: React.FC = ({ + images, + def, + opened, + onClose, +}) => { + const { feature } = useFeatureContext(); + const theme = useTheme(); + const fullScreen = useMediaQuery(theme.breakpoints.down('sm')); + const cols = useMediaQuery(theme.breakpoints.up('md')) ? 3 : 2; + + if (images.length <= 4) { + return null; + } + + return ( + + Images for {getLabel(feature)} + + + {images.map((img) => ( + + {getImageDefId(def)} + + ))} + + + + + + + ); +}; + const GalleryInner: React.FC = ({ def, images }) => { + const [opened, setOpened] = React.useState(false); + const panorama = images.find(({ panoramaUrl }) => panoramaUrl); if (panorama) { return ; } return ( -
- {images.slice(0, 4).map((image, i) => { - const isLastImg = images.length - 1 === i; - const isBlurredButton = !isLastImg && images.length > 4 && i === 3; - - if (isBlurredButton) { - return ; - } - - return ( - - ); - })} -
+ <> + { + setOpened(false); + }} + /> + +
+ {images.slice(0, 4).map((image, i) => { + const isLastImg = images.length - 1 === i; + const isBlurredButton = !isLastImg && images.length > 4 && i === 3; + + if (isBlurredButton) { + return ( + { + setOpened(true); + }} + /> + ); + } + + return ( + + ); + })} +
+ ); }; diff --git a/src/helpers/featureLabel.ts b/src/helpers/featureLabel.ts index b2da3c46..edf35c29 100644 --- a/src/helpers/featureLabel.ts +++ b/src/helpers/featureLabel.ts @@ -1,6 +1,6 @@ import { Feature } from '../services/types'; import { roundedToDeg } from '../utils'; -import { t } from '../services/intl'; +import { intl, t } from '../services/intl'; export const getSubclass = ({ layer, osmMeta, properties, schema }: Feature) => schema?.label || @@ -11,7 +11,7 @@ export const getSubclass = ({ layer, osmMeta, properties, schema }: Feature) => const getRefLabel = (feature: Feature) => feature.tags.ref ? `${getSubclass(feature)} ${feature.tags.ref}` : ''; -const getName = ({ tags }: Feature) => tags.name; // TODO choose a name according to locale +const getName = ({ tags }: Feature) => tags[`name:${intl.lang}`] || tags.name; export const hasName = (feature: Feature) => feature.point || getName(feature); // we dont want to show "No name" for point diff --git a/src/services/images/getImageFromApi.ts b/src/services/images/getImageFromApi.ts index b648a7a5..2286f44b 100644 --- a/src/services/images/getImageFromApi.ts +++ b/src/services/images/getImageFromApi.ts @@ -39,7 +39,7 @@ const isAudioUrl = (url: string) => const getCommonsCategoryApiUrl = (title: string) => encodeUrl`https://commons.wikimedia.org/w/api.php?action=query&generator=categorymembers&gcmtitle=${ title - }&gcmlimit=6&gcmtype=file&prop=imageinfo&iiprop=url&iiurlwidth=${ + }&gcmlimit=10&gcmtype=file&prop=imageinfo&iiprop=url&iiurlwidth=${ WIDTH }&format=json&origin=*`; From 583c83be061ed6473f6a79025d925cf21f7bf5de Mon Sep 17 00:00:00 2001 From: Dlurak <84224239+Dlurak@users.noreply.github.com> Date: Fri, 13 Sep 2024 21:50:32 +0200 Subject: [PATCH 04/11] Implement arrows for switching groups --- .../FeaturePanel/ImagePane/FeatureImages.tsx | 44 +++++++- .../FeaturePanel/ImagePane/Gallery.tsx | 69 ++---------- .../FeaturePanel/ImagePane/GalleryDialog.tsx | 62 +++++++++++ .../ImagePane/SwitchImageArrow.tsx | 102 ++++++++++++++++++ .../FeaturePanel/ImagePane/helpers.tsx | 11 ++ 5 files changed, 224 insertions(+), 64 deletions(-) create mode 100644 src/components/FeaturePanel/ImagePane/GalleryDialog.tsx create mode 100644 src/components/FeaturePanel/ImagePane/SwitchImageArrow.tsx diff --git a/src/components/FeaturePanel/ImagePane/FeatureImages.tsx b/src/components/FeaturePanel/ImagePane/FeatureImages.tsx index fd932c5a..720d5eda 100644 --- a/src/components/FeaturePanel/ImagePane/FeatureImages.tsx +++ b/src/components/FeaturePanel/ImagePane/FeatureImages.tsx @@ -2,13 +2,15 @@ import React from 'react'; import styled from '@emotion/styled'; import { 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'; export const Wrapper = styled.div` 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.div` @@ -27,12 +29,19 @@ const StyledScrollbars = styled.div` scroll-behavior: smooth; `; -export const Slider = ({ children }) => ( - {children} +export const Slider = ({ children, onScroll }) => ( + {children} ); export const FeatureImages = () => { const { loading, groups } = useLoadImages(); + const [rightBouncing, setRightBouncing] = React.useState(true); + const [visibleIndex, setVisibleIndex] = React.useState(0); + + const galleryRefs = React.useRef[]>([]); + galleryRefs.current = groups.map( + (_, i) => galleryRefs.current[i] ?? React.createRef(), + ); if (groups.length === 0) { return {loading ? : }; @@ -40,10 +49,37 @@ export const FeatureImages = () => { return ( - + { + setRightBouncing(false); + + const target = + pos === 'right' + ? galleryRefs.current[visibleIndex + 1] + : galleryRefs.current[visibleIndex - 1]; + + target?.current.scrollIntoView({ + behavior: 'smooth', + block: 'nearest', + inline: 'center', + }); + }} + /> + { + const currentIndex = galleryRefs.current.findIndex( + ({ current: el }) => isElementVisible(el), + ); + setVisibleIndex(currentIndex); + }} + > {groups.map((group, i) => ( = ({ image, more, onClick }) => ( ); -type GalleryDialogProps = { - images: ImageType[]; - def: ImageDef; - opened: boolean; - onClose: () => void; -}; - -const GalleryDialog: React.FC = ({ - images, - def, - opened, - onClose, -}) => { - const { feature } = useFeatureContext(); - const theme = useTheme(); - const fullScreen = useMediaQuery(theme.breakpoints.down('sm')); - const cols = useMediaQuery(theme.breakpoints.up('md')) ? 3 : 2; - - if (images.length <= 4) { - return null; - } - - return ( - - Images for {getLabel(feature)} - - - {images.map((img) => ( - - {getImageDefId(def)} - - ))} - - - - - - - ); -}; - const GalleryInner: React.FC = ({ def, images }) => { const [opened, setOpened] = React.useState(false); @@ -228,8 +173,12 @@ const GalleryInner: React.FC = ({ def, images }) => { ); }; -export const Gallery: React.FC = ({ def, images, isFirst }) => ( - - - +export const Gallery = React.forwardRef( + ({ def, images, isFirst }, ref) => ( + + + + ), ); + +Gallery.displayName = 'Gallery'; diff --git a/src/components/FeaturePanel/ImagePane/GalleryDialog.tsx b/src/components/FeaturePanel/ImagePane/GalleryDialog.tsx new file mode 100644 index 00000000..28ef45d0 --- /dev/null +++ b/src/components/FeaturePanel/ImagePane/GalleryDialog.tsx @@ -0,0 +1,62 @@ +import React from 'react'; +import { ImageDef } from '../../../services/types'; +import { + getImageDefId, + ImageType, +} from '../../../services/images/getImageDefs'; +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + ImageList, + ImageListItem, + useTheme, + useMediaQuery, +} from '@mui/material'; +import { useFeatureContext } from '../../utils/FeatureContext'; +import { getLabel } from '../../../helpers/featureLabel'; + +type GalleryDialogProps = { + images: ImageType[]; + def: ImageDef; + opened: boolean; + onClose: () => void; +}; + +export const GalleryDialog: React.FC = ({ + images, + def, + opened, + onClose, +}) => { + const { feature } = useFeatureContext(); + const theme = useTheme(); + const fullScreen = useMediaQuery(theme.breakpoints.down('sm')); + const cols = useMediaQuery(theme.breakpoints.up('md')) ? 3 : 2; + + if (images.length <= 4) { + return null; + } + + return ( + + Images for {getLabel(feature)} + + + {images.map((img) => ( + + {getImageDefId(def)} + + ))} + + + + + + + ); +}; diff --git a/src/components/FeaturePanel/ImagePane/SwitchImageArrow.tsx b/src/components/FeaturePanel/ImagePane/SwitchImageArrow.tsx new file mode 100644 index 00000000..01d7087d --- /dev/null +++ b/src/components/FeaturePanel/ImagePane/SwitchImageArrow.tsx @@ -0,0 +1,102 @@ +import { keyframes } from '@emotion/react'; +import styled from '@emotion/styled'; +import { ArrowLeft, ArrowRight } from '@mui/icons-material'; + +const bounce = keyframes` + 0%, 100% { + transform: translateX(15%); + animation-timing-function: cubic-bezier(0.8, 0, 1, 1); + } + 50% { + transform: none; + animation-timing-function: cubic-bezier(0, 0, 0.2, 1); + } +`; + +const BouncingWrapper = styled.div` + display: inline-block; /* Ensures it only wraps the size of the child */ + animation: ${bounce} 1s infinite; +`; + +const FloatingButton = styled.button<{ position: 'right' | 'left' }>` + width: 2rem; + height: 2rem; + position: absolute; + background-color: rgba(212, 212, 216, 0.5); + border-radius: 100vmax; + top: 50%; + transform: translateY(-50%); + z-index: 3; + border: none; + + ${({ position }) => + position === 'right' ? 'right: 0.5rem;' : 'left: 0.5rem;'} +`; + +type ArrowProps = { + position: 'left' | 'right'; + rightBouncing: boolean; + onClick: () => void; +}; + +const ArrowButton: React.FC = ({ + position, + rightBouncing, + onClick, +}) => { + const arrow = position === 'left' ? : ; + + return ( + + {rightBouncing && position === 'right' ? ( + {arrow} + ) : ( + arrow + )} + + ); +}; + +type SwitchImageArrowsProps = { + onLeft?: () => void; + onRight?: () => void; + onClick?: (pos: 'left' | 'right') => void; + + rightBouncing: boolean; + showLeft?: boolean; + showRight?: boolean; +}; + +export const SwitchImageArrows: React.FC = ({ + onLeft, + onRight, + onClick, + rightBouncing, + showLeft = true, + showRight = true, +}) => { + return ( + <> + {showLeft && ( + { + if (onLeft) onLeft(); + if (onClick) onClick('left'); + }} + /> + )} + {showRight && ( + { + if (onRight) onRight(); + if (onClick) onClick('right'); + }} + /> + )} + + ); +}; diff --git a/src/components/FeaturePanel/ImagePane/helpers.tsx b/src/components/FeaturePanel/ImagePane/helpers.tsx index 0bcab232..0ebd6676 100644 --- a/src/components/FeaturePanel/ImagePane/helpers.tsx +++ b/src/components/FeaturePanel/ImagePane/helpers.tsx @@ -19,3 +19,14 @@ export const ImageSkeleton = styled.div` } } `; + +export const isElementVisible = (element: HTMLElement) => { + const rect = element.getBoundingClientRect(); + return ( + rect.top >= 0 && + rect.left >= 0 && + rect.bottom <= + (window.innerHeight || document.documentElement.clientHeight) && + rect.right <= (window.innerWidth || document.documentElement.clientWidth) + ); +}; From 976ebb9e51c093874f21218e5dcc1b5766912747 Mon Sep 17 00:00:00 2001 From: Dlurak <84224239+Dlurak@users.noreply.github.com> Date: Sat, 14 Sep 2024 10:59:20 +0200 Subject: [PATCH 05/11] Remove unneeded code and support climbing crags --- pages/api/og-image.tsx | 4 +- src/components/FeaturePanel/CragsInArea.tsx | 15 +- .../FeaturePanel/ImagePane/FeatureImages.tsx | 44 +++- .../FeaturePanel/ImagePane/Gallery.tsx | 194 +++++++++++++----- .../FeaturePanel/ImagePane/Image/Image.tsx | 93 --------- .../ImagePane/Image/InfoButton.tsx | 68 +++--- .../ImagePane/Image/UncertainCover.tsx | 13 ++ .../FeaturePanel/ImagePane/Image/helpers.tsx | 53 ----- .../FeaturePanel/ImagePane/PathsSvg.tsx | 40 ++-- src/services/images/getImageFromApi.ts | 2 +- 10 files changed, 263 insertions(+), 263 deletions(-) delete mode 100644 src/components/FeaturePanel/ImagePane/Image/Image.tsx create mode 100644 src/components/FeaturePanel/ImagePane/Image/UncertainCover.tsx delete mode 100644 src/components/FeaturePanel/ImagePane/Image/helpers.tsx diff --git a/pages/api/og-image.tsx b/pages/api/og-image.tsx index 41b0e4dd..633ee053 100644 --- a/pages/api/og-image.tsx +++ b/pages/api/og-image.tsx @@ -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); diff --git a/src/components/FeaturePanel/CragsInArea.tsx b/src/components/FeaturePanel/CragsInArea.tsx index 4553bf65..83eeb2fd 100644 --- a/src/components/FeaturePanel/CragsInArea.tsx +++ b/src/components/FeaturePanel/CragsInArea.tsx @@ -9,9 +9,9 @@ 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, Slider, Wrapper } from './ImagePane/FeatureImages'; import { getInstantImage } from '../../services/images/getImageDefs'; +import { Gallery } from './ImagePane/Gallery'; const ArrowIcon = styled(ArrowForwardIosIcon)` opacity: 0.2; @@ -91,6 +91,7 @@ const Header = ({ ); +/* const Gallery = ({ images }) => { return ( @@ -102,6 +103,7 @@ const Gallery = ({ images }) => { ); }; +*/ const getOnClickWithHash = (apiId: OsmId) => (e) => { e.preventDefault(); @@ -132,7 +134,14 @@ const CragItem = ({ feature }: { feature: Feature }) => { routesCount={feature.members?.length} imagesCount={images.length} /> - {images.length ? : null} + {images.length ? ( + ({ + def, + images: [image], + }))} + /> + ) : null} ); diff --git a/src/components/FeaturePanel/ImagePane/FeatureImages.tsx b/src/components/FeaturePanel/ImagePane/FeatureImages.tsx index 720d5eda..0ad03bd6 100644 --- a/src/components/FeaturePanel/ImagePane/FeatureImages.tsx +++ b/src/components/FeaturePanel/ImagePane/FeatureImages.tsx @@ -5,6 +5,8 @@ import { NoImage } from './NoImage'; 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` height: calc(${HEIGHT}px + 10px); // 10px for scrollbar @@ -29,24 +31,37 @@ const StyledScrollbars = styled.div` scroll-behavior: smooth; `; -export const Slider = ({ children, onScroll }) => ( - {children} +export const Slider = ({ + children, + onScroll, +}: { + children: React.ReactNode; + onScroll?: () => void; +}) => ( + { + if (onScroll) onScroll(); + }} + > + {children} + ); -export const FeatureImages = () => { - const { loading, groups } = useLoadImages(); +type FeatureImagesUi = { + groups: { + def: ImageDef; + images: ImageType[]; + }[]; +}; + +export const FeatureImagesUi: React.FC = ({ groups }) => { const [rightBouncing, setRightBouncing] = React.useState(true); const [visibleIndex, setVisibleIndex] = React.useState(0); - const galleryRefs = React.useRef[]>([]); galleryRefs.current = groups.map( (_, i) => galleryRefs.current[i] ?? React.createRef(), ); - if (groups.length === 0) { - return {loading ? : }; - } - return ( { ref={galleryRefs.current[i]} def={group.def} images={group.images} - isFirst={i === 0} /> ))} ); }; + +export const FeatureImages = () => { + const { loading, groups } = useLoadImages(); + + if (groups.length === 0) { + return {loading ? : }; + } + + return ; +}; diff --git a/src/components/FeaturePanel/ImagePane/Gallery.tsx b/src/components/FeaturePanel/ImagePane/Gallery.tsx index ef7cddb4..605a7a27 100644 --- a/src/components/FeaturePanel/ImagePane/Gallery.tsx +++ b/src/components/FeaturePanel/ImagePane/Gallery.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { ImageDef } from '../../../services/types'; +import { ImageDef, isTag } from '../../../services/types'; import { getImageDefId, ImageType, @@ -7,11 +7,13 @@ import { 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'; type GalleryProps = { def: ImageDef; images: ImageType[]; - isFirst: boolean; }; const GalleryWrapper = styled.div` @@ -39,44 +41,67 @@ const BgImg = ({ url }: { url: string }) => ( /> ); -const MainImg = ({ url, alt }: { url: string; alt: string }) => ( - {alt}(({ url, alt, children }, ref) => ( +
-); + > + {alt} + {children} +
+)); + +MainImg.displayName = 'MainImg'; type ImageProps = { image: ImageType; def: ImageDef; high: boolean; wide: boolean; + children?: React.ReactNode; }; -const Image: React.FC = ({ image, def, high, wide }) => ( -
- - -
+const Image = React.forwardRef( + ({ image, def, high, wide, children }, ref) => ( +
+ + + {children} + +
+ ), ); +Image.displayName = 'Image'; + type SeeMoreProps = { image: ImageType; more: number; @@ -113,6 +138,62 @@ const SeeMoreButton: React.FC = ({ image, more, onClick }) => ( ); +type GallerySlotProps = { + images: ImageType[]; + def: ImageDef; + onSeeMore: () => void; + index: number; +}; + +const GallerySlot: React.FC = ({ + 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(); + + if (isBlurredButton) { + return ( + { + onSeeMore(); + }} + /> + ); + } + + return ( + + {hasPaths && imgRef.current && ( + + )} + + ); +}; + const GalleryInner: React.FC = ({ def, images }) => { const [opened, setOpened] = React.useState(false); @@ -141,44 +222,45 @@ const GalleryInner: React.FC = ({ def, images }) => { width: '100%', }} > - {images.slice(0, 4).map((image, i) => { - const isLastImg = images.length - 1 === i; - const isBlurredButton = !isLastImg && images.length > 4 && i === 3; - - if (isBlurredButton) { - return ( - { - setOpened(true); - }} - /> - ); - } - - return ( - - ); - })} + {images.slice(0, 4).map((_, i) => ( + { + setOpened(true); + }} + /> + ))}
); }; export const Gallery = React.forwardRef( - ({ def, images, isFirst }, ref) => ( - - - - ), + ({ def, images }, ref) => { + const showUncertainCover = + images.some(({ uncertainImage }) => uncertainImage) && + !images.some(({ panoramaUrl }) => panoramaUrl); + + const internalRef = React.useRef(); + + return ( + +
+ + + {showUncertainCover && } +
+
+ ); + }, ); Gallery.displayName = 'Gallery'; diff --git a/src/components/FeaturePanel/ImagePane/Image/Image.tsx b/src/components/FeaturePanel/ImagePane/Image/Image.tsx deleted file mode 100644 index 33c7b254..00000000 --- a/src/components/FeaturePanel/ImagePane/Image/Image.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import React from 'react'; -import styled from '@emotion/styled'; -import { - getImageDefId, - ImageType, -} from '../../../../services/images/getImageDefs'; -import { PathsSvg } from '../PathsSvg'; - -import { HEIGHT } from '../helpers'; -import { - initialSize, - UncertainCover, - useGetOnClick, - useImgSizeOnload, -} from './helpers'; -import { PanoramaImg } from './PanoramaImg'; -import { InfoButton } from './InfoButton'; -import { ImageDef, isTag } from '../../../../services/types'; -import { isMobileMode } from '../../../helpers'; -import { css } from '@emotion/react'; - -const Img = styled.img<{ $hasPaths: boolean }>` - margin-left: 50%; - transform: translateX(-50%); - - ${({ $hasPaths }) => $hasPaths && `opacity: 0.9;`} -`; - -const CROP_IMAGE_CSS = css` - overflow: hidden; - max-width: calc(410px - 2 * 8px); - @media ${isMobileMode} { - max-width: calc(100% - 2 * 8px); - } - - &:has(+ div) { - // leave some space if there is another image on the right - max-width: calc(410px - 2 * 8px - 15px); - @media ${isMobileMode} { - max-width: calc(100% - 2 * 8px - 15px); - } - } -`; - -const ImageWrapper = styled.div<{ $hasPaths: boolean }>` - display: inline-block; - position: relative; - height: 238px; - vertical-align: top; - overflow: hidden; - - margin-right: 8px; - &:first-of-type { - margin-left: 8px; - } - - ${({ onClick }) => onClick && `cursor: pointer;`} - ${({ $hasPaths }) => !$hasPaths && CROP_IMAGE_CSS} -`; - -type Props = { - def: ImageDef; - image: ImageType; -}; - -export const Image = ({ def, image }: Props) => { - const { imgRef, size, onPhotoLoad } = useImgSizeOnload(); - const onClick = useGetOnClick(def); - const hasPaths = - isTag(def) && !!(def.path?.length || def.memberPaths?.length); - - const isImageLoaded = size !== initialSize; - const showInfo = image.panoramaUrl || isImageLoaded; - return ( - - {image.panoramaUrl ? ( - - ) : ( - {getImageDefId(def)} - )} - {hasPaths && } - {showInfo && } - {image.uncertainImage && !image.panoramaUrl && } - - ); -}; diff --git a/src/components/FeaturePanel/ImagePane/Image/InfoButton.tsx b/src/components/FeaturePanel/ImagePane/Image/InfoButton.tsx index 513ce584..ab30b0e6 100644 --- a/src/components/FeaturePanel/ImagePane/Image/InfoButton.tsx +++ b/src/components/FeaturePanel/ImagePane/Image/InfoButton.tsx @@ -1,26 +1,45 @@ import React from 'react'; +import uniq from 'lodash/uniq'; +import uniqBy from 'lodash/uniqBy'; import styled from '@emotion/styled'; -import { Box } from '@mui/material'; import { ImageType } from '../../../../services/images/getImageDefs'; import { t } from '../../../../services/intl'; import { TooltipButton } from '../../../utils/TooltipButton'; -const TooltipContent = ({ image }: { image: ImageType }) => ( - <> - {image.description} -
- - {image.link} - - {image.uncertainImage && ( - <> -
-
- {t('featurepanel.uncertain_image')} - - )} - -); +type TooltipProps = { + images: ImageType[]; +}; + +const TooltipContent = ({ images }: TooltipProps) => { + const descs: string[] = uniq(images.map(({ description }) => description)); + const links: [string, string][] = uniqBy( + images.map(({ linkUrl, link }) => [linkUrl, link]), + ([url, link]) => `${url}-${link}`, + ); + const isUncertain = images.some(({ uncertainImage }) => uncertainImage); + return ( + <> + {descs.map((desc) => ( + <> + {desc} +
+ + ))} + {links.map(([linkUrl, link]) => ( + + {link} + + ))} + {isUncertain && ( + <> +
+
+ {t('featurepanel.uncertain_image')} + + )} + + ); +}; const InfoButtonWrapper = styled.div` position: absolute; @@ -34,19 +53,8 @@ const InfoButtonWrapper = styled.div` } `; -export const InfoButton = ({ image }: { image: ImageType }) => ( +export const InfoButton = ({ images }: TooltipProps) => ( - - - {image.sameUrlResolvedAlsoFrom?.map((item) => ( - - - - ))} - - } - /> + } /> ); diff --git a/src/components/FeaturePanel/ImagePane/Image/UncertainCover.tsx b/src/components/FeaturePanel/ImagePane/Image/UncertainCover.tsx new file mode 100644 index 00000000..05932657 --- /dev/null +++ b/src/components/FeaturePanel/ImagePane/Image/UncertainCover.tsx @@ -0,0 +1,13 @@ +import styled from '@emotion/styled'; + +export const UncertainCover = styled.div` + pointer-events: none; + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 100%; + backdrop-filter: contrast(0.6) brightness(1.2); + -webkit-backdrop-filter: contrast(0.6) brightness(1.2); + box-shadow: inset 0 0 100px rgba(255, 255, 255, 0.3); +`; diff --git a/src/components/FeaturePanel/ImagePane/Image/helpers.tsx b/src/components/FeaturePanel/ImagePane/Image/helpers.tsx deleted file mode 100644 index 2b6106df..00000000 --- a/src/components/FeaturePanel/ImagePane/Image/helpers.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import styled from '@emotion/styled'; -import React, { useEffect, useRef } from 'react'; -import Router from 'next/router'; -import { Size } from '../types'; -import { HEIGHT } from '../helpers'; -import { useFeatureContext } from '../../../utils/FeatureContext'; -import { getOsmappLink } from '../../../../services/helpers'; -import { removeFilePrefix } from '../../Climbing/utils/photo'; -import { ImageDef, isTag } from '../../../../services/types'; - -export const initialSize: Size = { width: 100, height: HEIGHT }; // until image size is known, the paths are rendered using this (eg. ssr) - -export const useImgSizeOnload = () => { - const imgRef = useRef(null); - const [size, setSize] = React.useState(initialSize); - useEffect(() => { - if (imgRef.current?.complete) { - setSize({ width: imgRef.current.width, height: imgRef.current.height }); // SSR case - } - }, []); - - const onPhotoLoad = (e) => { - setSize({ width: e.target.width, height: e.target.height }); // browser case - }; - - return { imgRef, size, onPhotoLoad }; -}; - -export const UncertainCover = styled.div` - pointer-events: none; - position: absolute; - top: 0; - left: 0; - height: 100%; - width: 100%; - backdrop-filter: contrast(0.6) brightness(1.2); - -webkit-backdrop-filter: contrast(0.6) brightness(1.2); - box-shadow: inset 0 0 100px rgba(255, 255, 255, 0.3); -`; - -export const useGetOnClick = (def: ImageDef) => { - const { feature } = useFeatureContext(); - - if (isTag(def) && feature.tags.climbing === 'crag') { - return () => { - const featureLink = getOsmappLink(feature); - const photoLink = removeFilePrefix(def.v); - Router.push(`${featureLink}/climbing/photo/${photoLink}`); - }; - } - - return undefined; -}; diff --git a/src/components/FeaturePanel/ImagePane/PathsSvg.tsx b/src/components/FeaturePanel/ImagePane/PathsSvg.tsx index 85965c55..44713958 100644 --- a/src/components/FeaturePanel/ImagePane/PathsSvg.tsx +++ b/src/components/FeaturePanel/ImagePane/PathsSvg.tsx @@ -16,7 +16,7 @@ import { import { Size } from './types'; import { useFeatureContext } from '../../utils/FeatureContext'; -import { getKey, getShortId } from '../../../services/helpers'; +import { getKey } from '../../../services/helpers'; const StyledSvg = styled.svg` position: absolute; @@ -31,6 +31,15 @@ const Svg = ({ children, size }) => ( {children} @@ -88,21 +97,22 @@ type PathsProps = { export const Paths = ({ def, feature, size }: PathsProps) => { const { preview } = useFeatureContext() ?? {}; + if (!isTag(def)) { + return null; + } return ( - isTag(def) && ( - <> - {def.path && } - {def.memberPaths?.map(({ path, member }) => ( - - ))} - - ) + <> + {def.path && } + {def.memberPaths?.map(({ path, member }) => ( + + ))} + ); }; // Careful: used also in image generation, eg. /api/image?id=r6 diff --git a/src/services/images/getImageFromApi.ts b/src/services/images/getImageFromApi.ts index 2286f44b..a6013754 100644 --- a/src/services/images/getImageFromApi.ts +++ b/src/services/images/getImageFromApi.ts @@ -34,7 +34,7 @@ const fetchCommonsFile = async (k: string, v: string): ImagePromise => { }; const isAudioUrl = (url: string) => - url.endsWith('.ogg') || url.endsWith('.mp3'); + url.endsWith('.ogg') || url.endsWith('.mp3') || url.endsWith('.wav'); const getCommonsCategoryApiUrl = (title: string) => encodeUrl`https://commons.wikimedia.org/w/api.php?action=query&generator=categorymembers&gcmtitle=${ From e6566855021953f82d400157d3c40786b336a9c1 Mon Sep 17 00:00:00 2001 From: Dlurak <84224239+Dlurak@users.noreply.github.com> Date: Sat, 14 Sep 2024 11:56:11 +0200 Subject: [PATCH 06/11] Implement multiple images for wikipedia --- .../FeaturePanel/ImagePane/Gallery.tsx | 120 +----------------- .../FeaturePanel/ImagePane/Image.tsx | 83 ++++++++++++ .../FeaturePanel/ImagePane/SeeMore.tsx | 42 ++++++ .../FeaturePanel/ImagePane/helpers.tsx | 2 +- src/services/images/getImageFromApi.ts | 45 ++++--- 5 files changed, 159 insertions(+), 133 deletions(-) create mode 100644 src/components/FeaturePanel/ImagePane/Image.tsx create mode 100644 src/components/FeaturePanel/ImagePane/SeeMore.tsx diff --git a/src/components/FeaturePanel/ImagePane/Gallery.tsx b/src/components/FeaturePanel/ImagePane/Gallery.tsx index 605a7a27..ba805c44 100644 --- a/src/components/FeaturePanel/ImagePane/Gallery.tsx +++ b/src/components/FeaturePanel/ImagePane/Gallery.tsx @@ -1,15 +1,14 @@ import React from 'react'; import { ImageDef, isTag } from '../../../services/types'; -import { - getImageDefId, - ImageType, -} from '../../../services/images/getImageDefs'; +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'; type GalleryProps = { def: ImageDef; @@ -25,119 +24,6 @@ const GalleryWrapper = styled.div` overflow: hidden; `; -const BgImg = ({ url }: { url: string }) => ( - -); - -const MainImg = React.forwardRef< - HTMLImageElement, - { url: string; alt: string; children?: React.ReactNode } ->(({ url, alt, children }, ref) => ( -
- {alt} - {children} -
-)); - -MainImg.displayName = 'MainImg'; - -type ImageProps = { - image: ImageType; - def: ImageDef; - high: boolean; - wide: boolean; - children?: React.ReactNode; -}; - -const Image = React.forwardRef( - ({ image, def, high, wide, children }, ref) => ( -
- - - {children} - -
- ), -); - -Image.displayName = 'Image'; - -type SeeMoreProps = { - image: ImageType; - more: number; - onClick: () => void; -}; - -const SeeMoreButton: React.FC = ({ image, more, onClick }) => ( - -); - type GallerySlotProps = { images: ImageType[]; def: ImageDef; diff --git a/src/components/FeaturePanel/ImagePane/Image.tsx b/src/components/FeaturePanel/ImagePane/Image.tsx new file mode 100644 index 00000000..65e05d6e --- /dev/null +++ b/src/components/FeaturePanel/ImagePane/Image.tsx @@ -0,0 +1,83 @@ +import React from 'react'; +import { + getImageDefId, + ImageType, +} from '../../../services/images/getImageDefs'; +import { ImageDef } from '../../../services/types'; + +export const BgImg = ({ url }: { url: string }) => ( + +); + +const MainImg = React.forwardRef< + HTMLImageElement, + { url: string; alt: string; children?: React.ReactNode } +>(({ url, alt, children }, ref) => ( +
+ {alt} + {children} +
+)); + +MainImg.displayName = 'MainImg'; + +type ImageProps = { + image: ImageType; + def: ImageDef; + high: boolean; + wide: boolean; + children?: React.ReactNode; +}; + +export const Image = React.forwardRef( + ({ image, def, high, wide, children }, ref) => ( +
+ + + {children} + +
+ ), +); + +Image.displayName = 'Image'; diff --git a/src/components/FeaturePanel/ImagePane/SeeMore.tsx b/src/components/FeaturePanel/ImagePane/SeeMore.tsx new file mode 100644 index 00000000..9e1ba8f3 --- /dev/null +++ b/src/components/FeaturePanel/ImagePane/SeeMore.tsx @@ -0,0 +1,42 @@ +import { ImageType } from '../../../services/images/getImageDefs'; +import { BgImg } from './Image'; + +type SeeMoreProps = { + image: ImageType; + more: number; + onClick: () => void; +}; + +export const SeeMoreButton: React.FC = ({ + image, + more, + onClick, +}) => ( + +); diff --git a/src/components/FeaturePanel/ImagePane/helpers.tsx b/src/components/FeaturePanel/ImagePane/helpers.tsx index 0ebd6676..0f052bfe 100644 --- a/src/components/FeaturePanel/ImagePane/helpers.tsx +++ b/src/components/FeaturePanel/ImagePane/helpers.tsx @@ -3,7 +3,7 @@ import styled from '@emotion/styled'; export const HEIGHT = 238; export const ImageSkeleton = styled.div` - width: 200px; + width: 100%; height: ${HEIGHT}px; margin: 0 auto; animation: skeleton-loading 1s linear infinite alternate; diff --git a/src/services/images/getImageFromApi.ts b/src/services/images/getImageFromApi.ts index a6013754..bd524c37 100644 --- a/src/services/images/getImageFromApi.ts +++ b/src/services/images/getImageFromApi.ts @@ -107,28 +107,43 @@ const parseWikipedia = (k: string, v: string) => { const getWikipediaApiUrl = (country: string, title: string) => encodeUrl`https://${ country - }.wikipedia.org/w/api.php?action=query&prop=pageimages&pithumbsize=${ - WIDTH - }&format=json&titles=${title}&origin=*`; + }.wikipedia.org/w/api.php?action=query&prop=images&titles=${title}&format=json&origin=*`; const fetchWikipedia = async (k: string, v: string): ImagePromise => { const { country, title } = parseWikipedia(k, v); const url = getWikipediaApiUrl(country, title); const data = await fetchJson(url); const page = Object.values(data.query.pages)[0] as any; - if (!page.pageimage) { - return []; - } - return [ - { - imageUrl: decodeURI(page.thumbnail.source), // it has to be decoded, because wikipedia - // encodes brackets (), but encodeURI doesnt + const imagesTitles = (page.images as { title: string }[]).map(({ title }) => { + const [_, name] = title.split(':', 2); + return `File:${name}`; + }); + + const promiseImages = imagesTitles.slice(0, 6).map(async (title) => { + const url = getCommonsFileApiUrl(title); + const data = await fetchJson(url); + const page = Object.values(data.query.pages)[0] as any; + if (!page.imageinfo?.length) { + return null; + } + const image = page.imageinfo[0]; + return { + imageUrl: decodeURI(image.thumburl), + link: page.title, + linkUrl: image.descriptionshorturl, + }; + }); + + const images = await Promise.all(promiseImages); + + return images + .filter((img) => img) + .map(({ imageUrl, link, linkUrl }) => ({ + imageUrl, + link, + linkUrl, description: `Wikipedia (${k}=*)`, - link: `File:${page.pageimage}`, - linkUrl: `https://commons.wikimedia.org/wiki/File:${page.pageimage}`, - // portrait: page.thumbnail.width < page.thumbnail.height, - }, - ]; + })); }; export const getImageFromApiRaw = async (def: ImageDef): ImagePromise => { From ee69dc6b1bd3aa1241d2bd60a1abb9ef7f429eae Mon Sep 17 00:00:00 2001 From: Dlurak <84224239+Dlurak@users.noreply.github.com> Date: Sat, 14 Sep 2024 13:20:11 +0200 Subject: [PATCH 07/11] Update the tests --- .../ImagePane/Image/InfoButton.tsx | 9 +- .../ImagePane/__tests__/useLoadImages.test.ts | 26 +- .../images/__tests__/apiMocks.fixture.ts | 249 +++++++++++++++--- .../images/__tests__/getImageFromApi.test.ts | 232 ++++++++-------- 4 files changed, 368 insertions(+), 148 deletions(-) diff --git a/src/components/FeaturePanel/ImagePane/Image/InfoButton.tsx b/src/components/FeaturePanel/ImagePane/Image/InfoButton.tsx index ab30b0e6..737ebe5a 100644 --- a/src/components/FeaturePanel/ImagePane/Image/InfoButton.tsx +++ b/src/components/FeaturePanel/ImagePane/Image/InfoButton.tsx @@ -26,9 +26,12 @@ const TooltipContent = ({ images }: TooltipProps) => { ))} {links.map(([linkUrl, link]) => ( - - {link} - + <> + + {link} + +
+ ))} {isUncertain && ( <> diff --git a/src/components/FeaturePanel/ImagePane/__tests__/useLoadImages.test.ts b/src/components/FeaturePanel/ImagePane/__tests__/useLoadImages.test.ts index 40601f84..ace61e29 100644 --- a/src/components/FeaturePanel/ImagePane/__tests__/useLoadImages.test.ts +++ b/src/components/FeaturePanel/ImagePane/__tests__/useLoadImages.test.ts @@ -14,30 +14,32 @@ const image2b = { imageUrl: '2.jpg', link: 'different source' } as ImageType; describe('mergeResultFn', () => { it('should merge to sameUrlResolvedAlsoFrom', () => { const prevImages = [ - { def: def1, image: image1 }, - { def: def2, image: image2 }, + { def: def1, images: [image1] }, + { def: def2, images: [image2] }, ]; - const result = mergeResultFn(def3, image2b, [])(prevImages); + const result = mergeResultFn(def3, [image2b], [])(prevImages); expect(result).toEqual([ - { def: def1, image: image1 }, + { def: def1, images: [image1] }, { def: def2, - image: { - ...image2, - sameUrlResolvedAlsoFrom: [image2b], - }, + images: [ + { + ...image2, + sameUrlResolvedAlsoFrom: [image2b], + }, + ], }, ]); }); it('should sort images', () => { const defs = [def1, def2, def3]; - const prevImages = [{ def: def2, image: image2 }]; + const prevImages = [{ def: def2, images: [image2] }]; - const result = mergeResultFn(def1, image1, defs)(prevImages); + const result = mergeResultFn(def1, [image1], defs)(prevImages); expect(result).toEqual([ - { def: def1, image: image1 }, - { def: def2, image: image2 }, + { def: def1, images: [image1] }, + { def: def2, images: [image2] }, ]); }); }); diff --git a/src/services/images/__tests__/apiMocks.fixture.ts b/src/services/images/__tests__/apiMocks.fixture.ts index aedca55c..c4001b01 100644 --- a/src/services/images/__tests__/apiMocks.fixture.ts +++ b/src/services/images/__tests__/apiMocks.fixture.ts @@ -29,12 +29,12 @@ export const WIKIDATA: ApiMock = { }; export const COMMONS_CATEGORY: ApiMock = { - url: 'https://commons.wikimedia.org/w/api.php?action=query&generator=categorymembers&gcmtitle=Category%3AYosemite%20National%20Park&gcmlimit=1&gcmtype=file&prop=imageinfo&&iiprop=url&iiurlwidth=410&format=json&origin=*', + url: 'https://commons.wikimedia.org/w/api.php?action=query&generator=categorymembers&gcmtitle=Category%3AYosemite%20National%20Park&gcmlimit=10&gcmtype=file&prop=imageinfo&iiprop=url&iiurlwidth=410&format=json&origin=*', response: { batchcomplete: '', continue: { gcmcontinue: - 'file|3139313620594f53454d4954452042592047454f52474520535445524c494e472046524f4e5420434f5645522e504e47|149319876', + 'file|35303050582050484f544f2028313039393139373931292e4a504547|78764811', continue: 'gcmcontinue||', }, query: { @@ -64,6 +64,223 @@ export const COMMONS_CATEGORY: ApiMock = { }, ], }, + '149319876': { + pageid: 149319876, + ns: 6, + title: 'File:1916 Yosemite by George Sterling front cover.png', + imagerepository: 'local', + imageinfo: [ + { + thumburl: + 'https://upload.wikimedia.org/wikipedia/commons/thumb/c/c2/1916_Yosemite_by_George_Sterling_front_cover.png/410px-1916_Yosemite_by_George_Sterling_front_cover.png', + thumbwidth: 410, + thumbheight: 598, + responsiveUrls: { + '1.5': + 'https://upload.wikimedia.org/wikipedia/commons/thumb/c/c2/1916_Yosemite_by_George_Sterling_front_cover.png/614px-1916_Yosemite_by_George_Sterling_front_cover.png', + '2': 'https://upload.wikimedia.org/wikipedia/commons/thumb/c/c2/1916_Yosemite_by_George_Sterling_front_cover.png/819px-1916_Yosemite_by_George_Sterling_front_cover.png', + }, + url: 'https://upload.wikimedia.org/wikipedia/commons/c/c2/1916_Yosemite_by_George_Sterling_front_cover.png', + descriptionurl: + 'https://commons.wikimedia.org/wiki/File:1916_Yosemite_by_George_Sterling_front_cover.png', + descriptionshorturl: + 'https://commons.wikimedia.org/w/index.php?curid=149319876', + }, + ], + }, + '133422920': { + pageid: 133422920, + ns: 6, + title: + 'File:339 b (?) 6 weeks old (18b4908c3f5342f0b50989504d9fd18f).jpg', + imagerepository: 'local', + imageinfo: [ + { + thumburl: + 'https://upload.wikimedia.org/wikipedia/commons/thumb/6/66/339_b_%28%3F%29_6_weeks_old_%2818b4908c3f5342f0b50989504d9fd18f%29.jpg/410px-339_b_%28%3F%29_6_weeks_old_%2818b4908c3f5342f0b50989504d9fd18f%29.jpg', + thumbwidth: 410, + thumbheight: 254, + responsiveUrls: { + '1.5': + 'https://upload.wikimedia.org/wikipedia/commons/thumb/6/66/339_b_%28%3F%29_6_weeks_old_%2818b4908c3f5342f0b50989504d9fd18f%29.jpg/615px-339_b_%28%3F%29_6_weeks_old_%2818b4908c3f5342f0b50989504d9fd18f%29.jpg', + '2': 'https://upload.wikimedia.org/wikipedia/commons/thumb/6/66/339_b_%28%3F%29_6_weeks_old_%2818b4908c3f5342f0b50989504d9fd18f%29.jpg/820px-339_b_%28%3F%29_6_weeks_old_%2818b4908c3f5342f0b50989504d9fd18f%29.jpg', + }, + url: 'https://upload.wikimedia.org/wikipedia/commons/6/66/339_b_%28%3F%29_6_weeks_old_%2818b4908c3f5342f0b50989504d9fd18f%29.jpg', + descriptionurl: + 'https://commons.wikimedia.org/wiki/File:339_b_(%3F)_6_weeks_old_(18b4908c3f5342f0b50989504d9fd18f).jpg', + descriptionshorturl: + 'https://commons.wikimedia.org/w/index.php?curid=133422920', + }, + ], + }, + '78764872': { + pageid: 78764872, + ns: 6, + title: 'File:500px photo (109919449).jpeg', + imagerepository: 'local', + imageinfo: [ + { + thumburl: + 'https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/500px_photo_%28109919449%29.jpeg/410px-500px_photo_%28109919449%29.jpeg', + thumbwidth: 410, + thumbheight: 308, + responsiveUrls: { + '1.5': + 'https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/500px_photo_%28109919449%29.jpeg/615px-500px_photo_%28109919449%29.jpeg', + '2': 'https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/500px_photo_%28109919449%29.jpeg/820px-500px_photo_%28109919449%29.jpeg', + }, + url: 'https://upload.wikimedia.org/wikipedia/commons/a/a7/500px_photo_%28109919449%29.jpeg', + descriptionurl: + 'https://commons.wikimedia.org/wiki/File:500px_photo_(109919449).jpeg', + descriptionshorturl: + 'https://commons.wikimedia.org/w/index.php?curid=78764872', + }, + ], + }, + '78764874': { + pageid: 78764874, + ns: 6, + title: 'File:500px photo (109919463).jpeg', + imagerepository: 'local', + imageinfo: [ + { + thumburl: + 'https://upload.wikimedia.org/wikipedia/commons/thumb/8/8e/500px_photo_%28109919463%29.jpeg/410px-500px_photo_%28109919463%29.jpeg', + thumbwidth: 410, + thumbheight: 308, + responsiveUrls: { + '1.5': + 'https://upload.wikimedia.org/wikipedia/commons/thumb/8/8e/500px_photo_%28109919463%29.jpeg/615px-500px_photo_%28109919463%29.jpeg', + '2': 'https://upload.wikimedia.org/wikipedia/commons/thumb/8/8e/500px_photo_%28109919463%29.jpeg/820px-500px_photo_%28109919463%29.jpeg', + }, + url: 'https://upload.wikimedia.org/wikipedia/commons/8/8e/500px_photo_%28109919463%29.jpeg', + descriptionurl: + 'https://commons.wikimedia.org/wiki/File:500px_photo_(109919463).jpeg', + descriptionshorturl: + 'https://commons.wikimedia.org/w/index.php?curid=78764874', + }, + ], + }, + '78764885': { + pageid: 78764885, + ns: 6, + title: 'File:500px photo (109919597).jpeg', + imagerepository: 'local', + imageinfo: [ + { + thumburl: + 'https://upload.wikimedia.org/wikipedia/commons/thumb/e/eb/500px_photo_%28109919597%29.jpeg/410px-500px_photo_%28109919597%29.jpeg', + thumbwidth: 410, + thumbheight: 308, + responsiveUrls: { + '1.5': + 'https://upload.wikimedia.org/wikipedia/commons/thumb/e/eb/500px_photo_%28109919597%29.jpeg/615px-500px_photo_%28109919597%29.jpeg', + '2': 'https://upload.wikimedia.org/wikipedia/commons/thumb/e/eb/500px_photo_%28109919597%29.jpeg/820px-500px_photo_%28109919597%29.jpeg', + }, + url: 'https://upload.wikimedia.org/wikipedia/commons/e/eb/500px_photo_%28109919597%29.jpeg', + descriptionurl: + 'https://commons.wikimedia.org/wiki/File:500px_photo_(109919597).jpeg', + descriptionshorturl: + 'https://commons.wikimedia.org/w/index.php?curid=78764885', + }, + ], + }, + '78764884': { + pageid: 78764884, + ns: 6, + title: 'File:500px photo (109919623).jpeg', + imagerepository: 'local', + imageinfo: [ + { + thumburl: + 'https://upload.wikimedia.org/wikipedia/commons/thumb/3/36/500px_photo_%28109919623%29.jpeg/410px-500px_photo_%28109919623%29.jpeg', + thumbwidth: 410, + thumbheight: 308, + responsiveUrls: { + '1.5': + 'https://upload.wikimedia.org/wikipedia/commons/thumb/3/36/500px_photo_%28109919623%29.jpeg/615px-500px_photo_%28109919623%29.jpeg', + '2': 'https://upload.wikimedia.org/wikipedia/commons/thumb/3/36/500px_photo_%28109919623%29.jpeg/820px-500px_photo_%28109919623%29.jpeg', + }, + url: 'https://upload.wikimedia.org/wikipedia/commons/3/36/500px_photo_%28109919623%29.jpeg', + descriptionurl: + 'https://commons.wikimedia.org/wiki/File:500px_photo_(109919623).jpeg', + descriptionshorturl: + 'https://commons.wikimedia.org/w/index.php?curid=78764884', + }, + ], + }, + '78764888': { + pageid: 78764888, + ns: 6, + title: 'File:500px photo (109919633).jpeg', + imagerepository: 'local', + imageinfo: [ + { + thumburl: + 'https://upload.wikimedia.org/wikipedia/commons/thumb/f/f3/500px_photo_%28109919633%29.jpeg/410px-500px_photo_%28109919633%29.jpeg', + thumbwidth: 410, + thumbheight: 308, + responsiveUrls: { + '1.5': + 'https://upload.wikimedia.org/wikipedia/commons/thumb/f/f3/500px_photo_%28109919633%29.jpeg/615px-500px_photo_%28109919633%29.jpeg', + '2': 'https://upload.wikimedia.org/wikipedia/commons/thumb/f/f3/500px_photo_%28109919633%29.jpeg/820px-500px_photo_%28109919633%29.jpeg', + }, + url: 'https://upload.wikimedia.org/wikipedia/commons/f/f3/500px_photo_%28109919633%29.jpeg', + descriptionurl: + 'https://commons.wikimedia.org/wiki/File:500px_photo_(109919633).jpeg', + descriptionshorturl: + 'https://commons.wikimedia.org/w/index.php?curid=78764888', + }, + ], + }, + '78764887': { + pageid: 78764887, + ns: 6, + title: 'File:500px photo (109919637).jpeg', + imagerepository: 'local', + imageinfo: [ + { + thumburl: + 'https://upload.wikimedia.org/wikipedia/commons/thumb/9/98/500px_photo_%28109919637%29.jpeg/410px-500px_photo_%28109919637%29.jpeg', + thumbwidth: 410, + thumbheight: 547, + responsiveUrls: { + '1.5': + 'https://upload.wikimedia.org/wikipedia/commons/thumb/9/98/500px_photo_%28109919637%29.jpeg/615px-500px_photo_%28109919637%29.jpeg', + '2': 'https://upload.wikimedia.org/wikipedia/commons/thumb/9/98/500px_photo_%28109919637%29.jpeg/820px-500px_photo_%28109919637%29.jpeg', + }, + url: 'https://upload.wikimedia.org/wikipedia/commons/9/98/500px_photo_%28109919637%29.jpeg', + descriptionurl: + 'https://commons.wikimedia.org/wiki/File:500px_photo_(109919637).jpeg', + descriptionshorturl: + 'https://commons.wikimedia.org/w/index.php?curid=78764887', + }, + ], + }, + '78764889': { + pageid: 78764889, + ns: 6, + title: 'File:500px photo (109919659).jpeg', + imagerepository: 'local', + imageinfo: [ + { + thumburl: + 'https://upload.wikimedia.org/wikipedia/commons/thumb/b/b1/500px_photo_%28109919659%29.jpeg/410px-500px_photo_%28109919659%29.jpeg', + thumbwidth: 410, + thumbheight: 547, + responsiveUrls: { + '1.5': + 'https://upload.wikimedia.org/wikipedia/commons/thumb/b/b1/500px_photo_%28109919659%29.jpeg/615px-500px_photo_%28109919659%29.jpeg', + '2': 'https://upload.wikimedia.org/wikipedia/commons/thumb/b/b1/500px_photo_%28109919659%29.jpeg/820px-500px_photo_%28109919659%29.jpeg', + }, + url: 'https://upload.wikimedia.org/wikipedia/commons/b/b1/500px_photo_%28109919659%29.jpeg', + descriptionurl: + 'https://commons.wikimedia.org/wiki/File:500px_photo_(109919659).jpeg', + descriptionshorturl: + 'https://commons.wikimedia.org/w/index.php?curid=78764889', + }, + ], + }, }, }, }, @@ -104,34 +321,6 @@ export const COMMONS_FILE: ApiMock = { }, }; -export const WIKIPEDIA: ApiMock = { - url: 'https://en.wikipedia.org/w/api.php?action=query&prop=pageimages&pithumbsize=410&format=json&titles=Yosemite%20National%20Park&origin=*', - response: { - batchcomplete: '', - query: { - pages: { - '48664': { - pageid: 48664, - ns: 0, - title: 'Yosemite National Park', - thumbnail: { - source: - 'https://upload.wikimedia.org/wikipedia/commons/thumb/1/13/Tunnel_View%2C_Yosemite_Valley%2C_Yosemite_NP_-_Diliff.jpg/410px-Tunnel_View%2C_Yosemite_Valley%2C_Yosemite_NP_-_Diliff.jpg', - width: 410, - height: 418, - }, - pageimage: 'Tunnel_View,_Yosemite_Valley,_Yosemite_NP_-_Diliff.jpg', - }, - }, - }, - }, -}; - -export const WIKIPEDIA_CS: ApiMock = { - url: 'https://cs.wikipedia.org/w/api.php?action=query&prop=pageimages&pithumbsize=410&format=json&titles=Yosemite%20National%20Park&origin=*', - response: WIKIPEDIA.response, -}; - export const FODY: ApiMock = { url: 'https://osm.fit.vutbr.cz/fody/api/close?lat=50.0995841&lon=14.304877&limit=1&distance=50', response: { diff --git a/src/services/images/__tests__/getImageFromApi.test.ts b/src/services/images/__tests__/getImageFromApi.test.ts index f6768d12..38b168fd 100644 --- a/src/services/images/__tests__/getImageFromApi.test.ts +++ b/src/services/images/__tests__/getImageFromApi.test.ts @@ -7,8 +7,6 @@ import { FODY, MAPILLARY, WIKIDATA, - WIKIPEDIA, - WIKIPEDIA_CS, } from './apiMocks.fixture'; jest.mock('../../fetch', () => ({ @@ -36,14 +34,17 @@ test('ImageFromCenter - mapillary', async () => { service: 'mapillary', center: [14.4212535, 50.0874654], }), - ).toEqual({ - description: 'Mapillary image from 3/13/2020, 11:46:50 AM', - imageUrl: 'mapillary_url_1024', - link: '321151246189360', - linkUrl: 'https://www.mapillary.com/app/?focus=photo&pKey=321151246189360', - uncertainImage: true, - panoramaUrl: 'mapillary_url_original', - }); + ).toEqual([ + { + description: 'Mapillary image from 3/13/2020, 11:46:50 AM', + imageUrl: 'mapillary_url_1024', + link: '321151246189360', + linkUrl: + 'https://www.mapillary.com/app/?focus=photo&pKey=321151246189360', + uncertainImage: true, + panoramaUrl: 'mapillary_url_original', + }, + ]); }); test('ImageFromCenter - fody', async () => { @@ -54,12 +55,14 @@ test('ImageFromCenter - fody', async () => { service: 'fody', center: [14.304877, 50.0995841], }), - ).toEqual({ - description: 'Fody photodb from Milancer (2019-11-10 15:19:18)', - imageUrl: 'https://osm.fit.vutbr.cz/fody/files/250px/25530.jpg', - link: 25530, - linkUrl: 'https://osm.fit.vutbr.cz/fody/?id=25530', - }); + ).toEqual([ + { + description: 'Fody photodb from Milancer (2019-11-10 15:19:18)', + imageUrl: 'https://osm.fit.vutbr.cz/fody/files/250px/25530.jpg', + link: 25530, + linkUrl: 'https://osm.fit.vutbr.cz/fody/?id=25530', + }, + ]); }); test('wikidata=*', async () => { @@ -71,13 +74,15 @@ test('wikidata=*', async () => { v: 'Q180402', instant: false, }), - ).toEqual({ - description: 'Wikidata image (wikidata=*)', - imageUrl: - 'https://upload.wikimedia.org/wikipedia/commons/thumb/e/ea/Half_Dome_with_Eastern_Yosemite_Valley_(50MP).jpg/410px-Half_Dome_with_Eastern_Yosemite_Valley_(50MP).jpg', - link: 'Q180402', - linkUrl: 'https://www.wikidata.org/wiki/Q180402', - }); + ).toEqual([ + { + description: 'Wikidata image (wikidata=*)', + imageUrl: + 'https://upload.wikimedia.org/wikipedia/commons/thumb/e/ea/Half_Dome_with_Eastern_Yosemite_Valley_(50MP).jpg/410px-Half_Dome_with_Eastern_Yosemite_Valley_(50MP).jpg', + link: 'Q180402', + linkUrl: 'https://www.wikidata.org/wiki/Q180402', + }, + ]); }); test('image=File:', async () => { @@ -89,13 +94,15 @@ test('image=File:', async () => { v: 'File:Hlubočepské plotny - Pravá plotna.jpg', instant: false, }), - ).toEqual({ - description: 'Wikimedia Commons (image=*)', - imageUrl: - 'https://upload.wikimedia.org/wikipedia/commons/thumb/2/21/Hlubočepské_plotny_-_Pravá_plotna.jpg/410px-Hlubočepské_plotny_-_Pravá_plotna.jpg', - link: 'File:Hlubočepské plotny - Pravá plotna.jpg', - linkUrl: 'https://commons.wikimedia.org/w/index.php?curid=145779916', - }); + ).toEqual([ + { + description: 'Wikimedia Commons (image=*)', + imageUrl: + 'https://upload.wikimedia.org/wikipedia/commons/thumb/2/21/Hlubočepské_plotny_-_Pravá_plotna.jpg/410px-Hlubočepské_plotny_-_Pravá_plotna.jpg', + link: 'File:Hlubočepské plotny - Pravá plotna.jpg', + linkUrl: 'https://commons.wikimedia.org/w/index.php?curid=145779916', + }, + ]); }); test('wikimedia_commons=File:', async () => { @@ -107,13 +114,15 @@ test('wikimedia_commons=File:', async () => { v: 'File:Hlubočepské plotny - Pravá plotna.jpg', instant: false, }), - ).toEqual({ - description: 'Wikimedia Commons (wikimedia_commons=*)', - imageUrl: - 'https://upload.wikimedia.org/wikipedia/commons/thumb/2/21/Hlubočepské_plotny_-_Pravá_plotna.jpg/410px-Hlubočepské_plotny_-_Pravá_plotna.jpg', - link: 'File:Hlubočepské plotny - Pravá plotna.jpg', - linkUrl: 'https://commons.wikimedia.org/w/index.php?curid=145779916', - }); + ).toEqual([ + { + description: 'Wikimedia Commons (wikimedia_commons=*)', + imageUrl: + 'https://upload.wikimedia.org/wikipedia/commons/thumb/2/21/Hlubočepské_plotny_-_Pravá_plotna.jpg/410px-Hlubočepské_plotny_-_Pravá_plotna.jpg', + link: 'File:Hlubočepské plotny - Pravá plotna.jpg', + linkUrl: 'https://commons.wikimedia.org/w/index.php?curid=145779916', + }, + ]); }); test('wikimedia_commons=Category:', async () => { @@ -121,73 +130,90 @@ test('wikimedia_commons=Category:', async () => { expect( await getImageFromApiRaw({ type: 'tag', - k: 'wikimedia_commons:2', + k: 'wikimedia_commons', v: 'Category:Yosemite National Park', instant: false, }), - ).toEqual({ - description: 'Wikimedia Commons category (wikimedia_commons:2=*)', - imageUrl: - 'https://upload.wikimedia.org/wikipedia/commons/thumb/0/0c/1912_Indian_Motorcycle._This_two-cylinder_motorcycle_is_thought_to_have_been_the_first_motorcycle_in_Yosemite._The_driver_was_(ca6a33cc-1dd8-b71b-0b83-9551ada5207f).jpg/410px-thumbnail.jpg', - link: 'Category:Yosemite National Park', - linkUrl: - 'https://commons.wikimedia.org/wiki/Category:Yosemite National Park', - }); -}); - -test('wikipedia=*', async () => { - mockApi(WIKIPEDIA); - expect( - await getImageFromApiRaw({ - type: 'tag', - k: 'wikipedia', - v: 'Yosemite National Park', - instant: false, - }), - ).toEqual({ - description: 'Wikipedia (wikipedia=*)', - imageUrl: - 'https://upload.wikimedia.org/wikipedia/commons/thumb/1/13/Tunnel_View%2C_Yosemite_Valley%2C_Yosemite_NP_-_Diliff.jpg/410px-Tunnel_View%2C_Yosemite_Valley%2C_Yosemite_NP_-_Diliff.jpg', - link: 'File:Tunnel_View,_Yosemite_Valley,_Yosemite_NP_-_Diliff.jpg', - linkUrl: - 'https://commons.wikimedia.org/wiki/File:Tunnel_View,_Yosemite_Valley,_Yosemite_NP_-_Diliff.jpg', - }); -}); - -test('wikipedia=* with lang prefix in value', async () => { - mockApi(WIKIPEDIA_CS); - expect( - await getImageFromApiRaw({ - type: 'tag', - k: 'wikipedia:2', - v: 'cs:Yosemite National Park', - instant: false, - }), - ).toEqual({ - description: 'Wikipedia (wikipedia:2=*)', - imageUrl: - 'https://upload.wikimedia.org/wikipedia/commons/thumb/1/13/Tunnel_View%2C_Yosemite_Valley%2C_Yosemite_NP_-_Diliff.jpg/410px-Tunnel_View%2C_Yosemite_Valley%2C_Yosemite_NP_-_Diliff.jpg', - link: 'File:Tunnel_View,_Yosemite_Valley,_Yosemite_NP_-_Diliff.jpg', - linkUrl: - 'https://commons.wikimedia.org/wiki/File:Tunnel_View,_Yosemite_Valley,_Yosemite_NP_-_Diliff.jpg', - }); -}); - -test('wikipedia:cs=* with lang prefix in key', async () => { - mockApi(WIKIPEDIA_CS); - expect( - await getImageFromApiRaw({ - type: 'tag', - k: 'wikipedia:cs', - v: 'Yosemite National Park', - instant: false, - }), - ).toEqual({ - description: 'Wikipedia (wikipedia:cs=*)', - imageUrl: - 'https://upload.wikimedia.org/wikipedia/commons/thumb/1/13/Tunnel_View%2C_Yosemite_Valley%2C_Yosemite_NP_-_Diliff.jpg/410px-Tunnel_View%2C_Yosemite_Valley%2C_Yosemite_NP_-_Diliff.jpg', - link: 'File:Tunnel_View,_Yosemite_Valley,_Yosemite_NP_-_Diliff.jpg', - linkUrl: - 'https://commons.wikimedia.org/wiki/File:Tunnel_View,_Yosemite_Valley,_Yosemite_NP_-_Diliff.jpg', - }); + ).toEqual([ + { + imageUrl: + 'https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/500px_photo_(109919449).jpeg/410px-500px_photo_(109919449).jpeg', + description: 'Wikimedia Commons category (wikimedia_commons=*)', + link: 'Category:Yosemite National Park', + linkUrl: + 'https://commons.wikimedia.org/wiki/Category:Yosemite National Park', + }, + { + imageUrl: + 'https://upload.wikimedia.org/wikipedia/commons/thumb/8/8e/500px_photo_(109919463).jpeg/410px-500px_photo_(109919463).jpeg', + description: 'Wikimedia Commons category (wikimedia_commons=*)', + link: 'Category:Yosemite National Park', + linkUrl: + 'https://commons.wikimedia.org/wiki/Category:Yosemite National Park', + }, + { + imageUrl: + 'https://upload.wikimedia.org/wikipedia/commons/thumb/3/36/500px_photo_(109919623).jpeg/410px-500px_photo_(109919623).jpeg', + description: 'Wikimedia Commons category (wikimedia_commons=*)', + link: 'Category:Yosemite National Park', + linkUrl: + 'https://commons.wikimedia.org/wiki/Category:Yosemite National Park', + }, + { + imageUrl: + 'https://upload.wikimedia.org/wikipedia/commons/thumb/e/eb/500px_photo_(109919597).jpeg/410px-500px_photo_(109919597).jpeg', + description: 'Wikimedia Commons category (wikimedia_commons=*)', + link: 'Category:Yosemite National Park', + linkUrl: + 'https://commons.wikimedia.org/wiki/Category:Yosemite National Park', + }, + { + imageUrl: + 'https://upload.wikimedia.org/wikipedia/commons/thumb/9/98/500px_photo_(109919637).jpeg/410px-500px_photo_(109919637).jpeg', + description: 'Wikimedia Commons category (wikimedia_commons=*)', + link: 'Category:Yosemite National Park', + linkUrl: + 'https://commons.wikimedia.org/wiki/Category:Yosemite National Park', + }, + { + imageUrl: + 'https://upload.wikimedia.org/wikipedia/commons/thumb/f/f3/500px_photo_(109919633).jpeg/410px-500px_photo_(109919633).jpeg', + description: 'Wikimedia Commons category (wikimedia_commons=*)', + link: 'Category:Yosemite National Park', + linkUrl: + 'https://commons.wikimedia.org/wiki/Category:Yosemite National Park', + }, + { + imageUrl: + 'https://upload.wikimedia.org/wikipedia/commons/thumb/b/b1/500px_photo_(109919659).jpeg/410px-500px_photo_(109919659).jpeg', + description: 'Wikimedia Commons category (wikimedia_commons=*)', + link: 'Category:Yosemite National Park', + linkUrl: + 'https://commons.wikimedia.org/wiki/Category:Yosemite National Park', + }, + { + imageUrl: + 'https://upload.wikimedia.org/wikipedia/commons/thumb/6/66/339_b_(%3F)_6_weeks_old_(18b4908c3f5342f0b50989504d9fd18f).jpg/410px-339_b_(%3F)_6_weeks_old_(18b4908c3f5342f0b50989504d9fd18f).jpg', + description: 'Wikimedia Commons category (wikimedia_commons=*)', + link: 'Category:Yosemite National Park', + linkUrl: + 'https://commons.wikimedia.org/wiki/Category:Yosemite National Park', + }, + { + imageUrl: + 'https://upload.wikimedia.org/wikipedia/commons/thumb/0/0c/1912_Indian_Motorcycle._This_two-cylinder_motorcycle_is_thought_to_have_been_the_first_motorcycle_in_Yosemite._The_driver_was_(ca6a33cc-1dd8-b71b-0b83-9551ada5207f).jpg/410px-thumbnail.jpg', + description: 'Wikimedia Commons category (wikimedia_commons=*)', + link: 'Category:Yosemite National Park', + linkUrl: + 'https://commons.wikimedia.org/wiki/Category:Yosemite National Park', + }, + { + imageUrl: + 'https://upload.wikimedia.org/wikipedia/commons/thumb/c/c2/1916_Yosemite_by_George_Sterling_front_cover.png/410px-1916_Yosemite_by_George_Sterling_front_cover.png', + description: 'Wikimedia Commons category (wikimedia_commons=*)', + link: 'Category:Yosemite National Park', + linkUrl: + 'https://commons.wikimedia.org/wiki/Category:Yosemite National Park', + }, + ]); }); From 2f62061d2cba80834f7e4ab965dffc361e214cf0 Mon Sep 17 00:00:00 2001 From: Dlurak <84224239+Dlurak@users.noreply.github.com> Date: Sat, 14 Sep 2024 13:25:26 +0200 Subject: [PATCH 08/11] Style fixes for ios --- src/components/FeaturePanel/ImagePane/SwitchImageArrow.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/FeaturePanel/ImagePane/SwitchImageArrow.tsx b/src/components/FeaturePanel/ImagePane/SwitchImageArrow.tsx index 01d7087d..2e07ec0c 100644 --- a/src/components/FeaturePanel/ImagePane/SwitchImageArrow.tsx +++ b/src/components/FeaturePanel/ImagePane/SwitchImageArrow.tsx @@ -29,6 +29,11 @@ const FloatingButton = styled.button<{ position: 'right' | 'left' }>` z-index: 3; border: none; + display: flex; + justify-content: center; + align-items: center; + color: #000; + ${({ position }) => position === 'right' ? 'right: 0.5rem;' : 'left: 0.5rem;'} `; From b85502f5d5464ff2f0e549290fd4103488e6bbf1 Mon Sep 17 00:00:00 2001 From: Dlurak <84224239+Dlurak@users.noreply.github.com> Date: Sun, 15 Sep 2024 18:07:54 +0200 Subject: [PATCH 09/11] Fix the overflow issue --- src/components/FeaturePanel/CragsInArea.tsx | 17 +------------ .../FeaturePanel/ImagePane/Gallery.tsx | 9 ++----- .../FeaturePanel/ImagePane/Image.tsx | 2 +- .../FeaturePanel/ImagePane/PathsSvg.tsx | 25 ++++++------------- .../FeaturePanel/ImagePane/helpers.tsx | 23 +++++++++++++++++ 5 files changed, 34 insertions(+), 42 deletions(-) diff --git a/src/components/FeaturePanel/CragsInArea.tsx b/src/components/FeaturePanel/CragsInArea.tsx index 83eeb2fd..50666113 100644 --- a/src/components/FeaturePanel/CragsInArea.tsx +++ b/src/components/FeaturePanel/CragsInArea.tsx @@ -9,9 +9,8 @@ import { Feature, isInstant, OsmId } from '../../services/types'; import { useMobileMode } from '../helpers'; import { getLabel } from '../../helpers/featureLabel'; -import { FeatureImagesUi, Slider, Wrapper } from './ImagePane/FeatureImages'; +import { FeatureImagesUi } from './ImagePane/FeatureImages'; import { getInstantImage } from '../../services/images/getImageDefs'; -import { Gallery } from './ImagePane/Gallery'; const ArrowIcon = styled(ArrowForwardIosIcon)` opacity: 0.2; @@ -91,20 +90,6 @@ const Header = ({ ); -/* -const Gallery = ({ images }) => { - return ( - - - {images.map((item) => ( - - ))} - - - ); -}; -*/ - const getOnClickWithHash = (apiId: OsmId) => (e) => { e.preventDefault(); Router.push(`/${getUrlOsmId(apiId)}${window.location.hash}`); diff --git a/src/components/FeaturePanel/ImagePane/Gallery.tsx b/src/components/FeaturePanel/ImagePane/Gallery.tsx index ba805c44..3decd10d 100644 --- a/src/components/FeaturePanel/ImagePane/Gallery.tsx +++ b/src/components/FeaturePanel/ImagePane/Gallery.tsx @@ -9,6 +9,7 @@ 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; @@ -68,13 +69,7 @@ const GallerySlot: React.FC = ({ ref={imgRef} > {hasPaths && imgRef.current && ( - + )} ); diff --git a/src/components/FeaturePanel/ImagePane/Image.tsx b/src/components/FeaturePanel/ImagePane/Image.tsx index 65e05d6e..ecafe131 100644 --- a/src/components/FeaturePanel/ImagePane/Image.tsx +++ b/src/components/FeaturePanel/ImagePane/Image.tsx @@ -42,7 +42,7 @@ const MainImg = React.forwardRef< style={{ objectFit: 'contain', height: '100%', - width: 'auto', + width: '100%', display: 'inline-block', }} loading="lazy" diff --git a/src/components/FeaturePanel/ImagePane/PathsSvg.tsx b/src/components/FeaturePanel/ImagePane/PathsSvg.tsx index 44713958..e29f6e52 100644 --- a/src/components/FeaturePanel/ImagePane/PathsSvg.tsx +++ b/src/components/FeaturePanel/ImagePane/PathsSvg.tsx @@ -18,29 +18,18 @@ import { Size } from './types'; import { useFeatureContext } from '../../utils/FeatureContext'; import { getKey } from '../../../services/helpers'; -const StyledSvg = styled.svg` +const StyledSvg = styled.svg<{ size: Size }>` position: absolute; - left: 0; - top: 0; - height: 100%; - width: 100%; pointer-events: none; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: ${({ size }) => `${size.width}px`}; + height: ${({ size }) => `${size.height}px`}; `; const Svg = ({ children, size }) => ( - + {children} ); diff --git a/src/components/FeaturePanel/ImagePane/helpers.tsx b/src/components/FeaturePanel/ImagePane/helpers.tsx index 0f052bfe..c7b58c56 100644 --- a/src/components/FeaturePanel/ImagePane/helpers.tsx +++ b/src/components/FeaturePanel/ImagePane/helpers.tsx @@ -30,3 +30,26 @@ export const isElementVisible = (element: HTMLElement) => { rect.right <= (window.innerWidth || document.documentElement.clientWidth) ); }; + +/** + * Calculates the displayed size of an image while maintaining its aspect ratio, + * based on the container's dimensions. This is useful when using `object-fit: cover`. + */ +export function calculateImageSize(img: HTMLImageElement) { + const { naturalWidth, naturalHeight, clientWidth, clientHeight } = img; + + const imageAspectRatio = naturalWidth / naturalHeight; + const containerAspectRatio = clientWidth / clientHeight; + + if (containerAspectRatio > imageAspectRatio) { + return { + width: clientHeight * imageAspectRatio, + height: clientHeight, + }; + } + + return { + width: clientWidth, + height: clientWidth / imageAspectRatio, + }; +} From 15f226212515a1113679de6a64d93bf8c51b76e2 Mon Sep 17 00:00:00 2001 From: Dlurak <84224239+Dlurak@users.noreply.github.com> Date: Sun, 15 Sep 2024 18:18:21 +0200 Subject: [PATCH 10/11] Move the cragitem images out of the link --- src/components/FeaturePanel/CragsInArea.tsx | 36 ++++++++++----------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/components/FeaturePanel/CragsInArea.tsx b/src/components/FeaturePanel/CragsInArea.tsx index 50666113..0ea759c6 100644 --- a/src/components/FeaturePanel/CragsInArea.tsx +++ b/src/components/FeaturePanel/CragsInArea.tsx @@ -90,7 +90,7 @@ const Header = ({ ); -const getOnClickWithHash = (apiId: OsmId) => (e) => { +const getOnClickWithHash = (apiId: OsmId) => (e: React.MouseEvent) => { e.preventDefault(); Router.push(`/${getUrlOsmId(apiId)}${window.location.hash}`); }; @@ -107,28 +107,28 @@ const CragItem = ({ feature }: { feature: Feature }) => { })) ?? []; return ( - setPreview(null)} - > - + + setPreview(null)} + >
- {images.length ? ( - ({ - def, - images: [image], - }))} - /> - ) : null} - - + + {!!images.length && ( + ({ + def, + images: [image], + }))} + /> + )} + ); }; From e79c00f6c68f4086118c3bf7323d79632197324c Mon Sep 17 00:00:00 2001 From: Dlurak <84224239+Dlurak@users.noreply.github.com> Date: Sun, 15 Sep 2024 22:17:53 +0200 Subject: [PATCH 11/11] Filter out some wikipedia images and remove the mergeResults function --- .../FeaturePanel/ImagePane/FeatureImages.tsx | 28 ++++----- .../ImagePane/SwitchImageArrow.tsx | 50 ++++++++-------- .../ImagePane/__tests__/useLoadImages.test.ts | 45 -------------- .../FeaturePanel/ImagePane/useLoadImages.tsx | 60 ++++--------------- src/services/images/getImageFromApi.ts | 55 ++++++++++++----- 5 files changed, 86 insertions(+), 152 deletions(-) delete mode 100644 src/components/FeaturePanel/ImagePane/__tests__/useLoadImages.test.ts diff --git a/src/components/FeaturePanel/ImagePane/FeatureImages.tsx b/src/components/FeaturePanel/ImagePane/FeatureImages.tsx index 0ad03bd6..f9384297 100644 --- a/src/components/FeaturePanel/ImagePane/FeatureImages.tsx +++ b/src/components/FeaturePanel/ImagePane/FeatureImages.tsx @@ -1,6 +1,6 @@ import React from 'react'; import styled from '@emotion/styled'; -import { useLoadImages } from './useLoadImages'; +import { ImageGroup, useLoadImages } from './useLoadImages'; import { NoImage } from './NoImage'; import { HEIGHT, ImageSkeleton, isElementVisible } from './helpers'; import { Gallery } from './Gallery'; @@ -47,14 +47,9 @@ export const Slider = ({ ); -type FeatureImagesUi = { - groups: { - def: ImageDef; - images: ImageType[]; - }[]; -}; - -export const FeatureImagesUi: React.FC = ({ groups }) => { +export const FeatureImagesUi: React.FC<{ groups: ImageGroup[] }> = ({ + groups, +}) => { const [rightBouncing, setRightBouncing] = React.useState(true); const [visibleIndex, setVisibleIndex] = React.useState(0); const galleryRefs = React.useRef[]>([]); @@ -89,15 +84,18 @@ export const FeatureImagesUi: React.FC = ({ groups }) => { ({ current: el }) => isElementVisible(el), ); setVisibleIndex(currentIndex); + setRightBouncing(false); }} > {groups.map((group, i) => ( - + <> + + ))} diff --git a/src/components/FeaturePanel/ImagePane/SwitchImageArrow.tsx b/src/components/FeaturePanel/ImagePane/SwitchImageArrow.tsx index 2e07ec0c..5dd97618 100644 --- a/src/components/FeaturePanel/ImagePane/SwitchImageArrow.tsx +++ b/src/components/FeaturePanel/ImagePane/SwitchImageArrow.tsx @@ -79,29 +79,27 @@ export const SwitchImageArrows: React.FC = ({ rightBouncing, showLeft = true, showRight = true, -}) => { - return ( - <> - {showLeft && ( - { - if (onLeft) onLeft(); - if (onClick) onClick('left'); - }} - /> - )} - {showRight && ( - { - if (onRight) onRight(); - if (onClick) onClick('right'); - }} - /> - )} - - ); -}; +}) => ( + <> + {showLeft && ( + { + if (onLeft) onLeft(); + if (onClick) onClick('left'); + }} + /> + )} + {showRight && ( + { + if (onRight) onRight(); + if (onClick) onClick('right'); + }} + /> + )} + +); diff --git a/src/components/FeaturePanel/ImagePane/__tests__/useLoadImages.test.ts b/src/components/FeaturePanel/ImagePane/__tests__/useLoadImages.test.ts deleted file mode 100644 index ace61e29..00000000 --- a/src/components/FeaturePanel/ImagePane/__tests__/useLoadImages.test.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { mergeResultFn } from '../useLoadImages'; -import { ImageDef } from '../../../../services/types'; -import { ImageType } from '../../../../services/images/getImageDefs'; - -jest.mock('maplibre-gl', () => ({})); - -const def1: ImageDef = { type: 'tag', k: 'key', v: '1', instant: false }; -const def2: ImageDef = { type: 'tag', k: 'key2', v: '2', instant: false }; -const def3: ImageDef = { type: 'tag', k: 'key3', v: '3', instant: false }; -const image1 = { imageUrl: '1.jpg' } as ImageType; -const image2 = { imageUrl: '2.jpg' } as ImageType; -const image2b = { imageUrl: '2.jpg', link: 'different source' } as ImageType; - -describe('mergeResultFn', () => { - it('should merge to sameUrlResolvedAlsoFrom', () => { - const prevImages = [ - { def: def1, images: [image1] }, - { def: def2, images: [image2] }, - ]; - const result = mergeResultFn(def3, [image2b], [])(prevImages); - expect(result).toEqual([ - { def: def1, images: [image1] }, - { - def: def2, - images: [ - { - ...image2, - sameUrlResolvedAlsoFrom: [image2b], - }, - ], - }, - ]); - }); - - it('should sort images', () => { - const defs = [def1, def2, def3]; - const prevImages = [{ def: def2, images: [image2] }]; - - const result = mergeResultFn(def1, [image1], defs)(prevImages); - expect(result).toEqual([ - { def: def1, images: [image1] }, - { def: def2, images: [image2] }, - ]); - }); -}); diff --git a/src/components/FeaturePanel/ImagePane/useLoadImages.tsx b/src/components/FeaturePanel/ImagePane/useLoadImages.tsx index f1aaae2c..598fd76f 100644 --- a/src/components/FeaturePanel/ImagePane/useLoadImages.tsx +++ b/src/components/FeaturePanel/ImagePane/useLoadImages.tsx @@ -8,55 +8,9 @@ import { getImageFromApi } from '../../../services/images/getImageFromApi'; import { useFeatureContext } from '../../utils/FeatureContext'; import { ImageDef, isInstant } from '../../../services/types'; -export type ImageGroup = { def: ImageDef; images: ImageType[] }[]; +export type ImageGroup = { def: ImageDef; images: ImageType[] }; -export const mergeResultFn = - (def: ImageDef, imgs: (ImageType | null)[], defs: ImageDef[]) => - (prevImages: ImageGroup): ImageGroup => { - const images = imgs.filter((x) => x); - if (images.length === 0) { - return prevImages; - } - - const imageUrls = images.map(({ imageUrl }) => imageUrl); - - const found = prevImages.find((group) => - group.images.some((img) => imageUrls.includes(img.imageUrl)), - ); - - if (found) { - // Update existing group - const updatedGroup = { - ...found, - images: found.images.map((img) => { - if (!imageUrls.includes(img.imageUrl)) return img; - - return { - ...img, - sameUrlResolvedAlsoFrom: [ - ...(img.sameUrlResolvedAlsoFrom ?? []), - ...images, - ], - }; - }), - }; - - return prevImages.map((group) => - group.def === found.def ? updatedGroup : group, - ); - } - - // Add new group - const sorted = [...prevImages, { def, images }].sort((a, b) => { - const aIndex = defs.findIndex((item) => item === a.def); - const bIndex = defs.findIndex((item) => item === b.def); - return aIndex - bIndex; - }); - - return sorted; - }; - -const getInitialState = (defs: ImageDef[]): ImageGroup => +const getInitialState = (defs: ImageDef[]): ImageGroup[] => defs?.filter(isInstant)?.map((def) => ({ def, images: getInstantImage(def) ? [getInstantImage(def)] : [], @@ -69,13 +23,19 @@ export const useLoadImages = () => { const initialState = useMemo(() => getInitialState(defs), [defs]); const [loading, setLoading] = useState(apiDefs.length > 0); - const [groups, setGroups] = useState(initialState); + const [groups, setGroups] = useState(initialState); useEffect(() => { setGroups(initialState); const promises = apiDefs.map(async (def) => { const images = await getImageFromApi(def); - setGroups(mergeResultFn(def, images, defs)); + setGroups((prev) => + [...prev, { def, images }].sort((a, b) => { + const aIndex = defs.findIndex((item) => item === a.def); + const bIndex = defs.findIndex((item) => item === b.def); + return aIndex - bIndex; + }), + ); }); Promise.all(promises).then(() => { diff --git a/src/services/images/getImageFromApi.ts b/src/services/images/getImageFromApi.ts index bd524c37..d85854d3 100644 --- a/src/services/images/getImageFromApi.ts +++ b/src/services/images/getImageFromApi.ts @@ -104,6 +104,26 @@ const parseWikipedia = (k: string, v: string) => { return { country: 'en', title: v }; }; +const isAllowedWikipedia = (title: string) => { + const forbiddenPatterns = [ + /File:Commons-logo\.svg/, + /File:Compass rose pale\.svg/, + /File:Arrleft\.svg/, + /File:Arrright\.svg/, + /File:Info Simple\.svg/, + /File:Blue pencil\.svg/, + /File:Fairytale warning\.png/, + /File:([A-Z][a-z]*)( transit icons -)? (S|U)\d{1,2}(-20\d{2})?\.svg/, + /File:BSicon (.*)?\.svg/, + /File:Deutsche Bahn AG-Logo\.svg/, + /File:S-Bahn-Logo\.svg/, + /File:Airplane silhouette\.svg/, + /File:(.*) Audiologo\.ogg/, + /File:Bundes(straße|autobahn) \d+ number\.svg/, + ]; + return !forbiddenPatterns.some((pattern) => pattern.test(title)); +}; + const getWikipediaApiUrl = (country: string, title: string) => encodeUrl`https://${ country @@ -119,21 +139,23 @@ const fetchWikipedia = async (k: string, v: string): ImagePromise => { return `File:${name}`; }); - const promiseImages = imagesTitles.slice(0, 6).map(async (title) => { - const url = getCommonsFileApiUrl(title); - const data = await fetchJson(url); - const page = Object.values(data.query.pages)[0] as any; - if (!page.imageinfo?.length) { - return null; - } - const image = page.imageinfo[0]; - return { - imageUrl: decodeURI(image.thumburl), - link: page.title, - linkUrl: image.descriptionshorturl, - }; - }); - + const promiseImages = imagesTitles + .filter(isAllowedWikipedia) + .slice(0, 10) + .map(async (title) => { + const url = getCommonsFileApiUrl(title); + const data = await fetchJson(url); + const page = Object.values(data.query.pages)[0] as any; + if (!page.imageinfo?.length) { + return null; + } + const image = page.imageinfo[0]; + return { + imageUrl: decodeURI(image.thumburl), + link: page.title, + linkUrl: image.descriptionshorturl, + }; + }); const images = await Promise.all(promiseImages); return images @@ -187,7 +209,8 @@ export const getImageFromApi = async (def: ImageDef): ImagePromise => { return img ? [img] : []; } - return await getImageFromApiRaw(def); + const images = await getImageFromApiRaw(def); + return images.filter((img) => img); } catch (e) { console.warn(e); // eslint-disable-line no-console return [];