Skip to content

Commit 6ef147e

Browse files
committed
remove old gallery, upgrade to react 19, fix lint
1 parent cb36b62 commit 6ef147e

File tree

19 files changed

+557
-877
lines changed

19 files changed

+557
-877
lines changed

.eslintrc.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ module.exports = {
2929
'@typescript-eslint/ban-ts-comment': 'off',
3030
'@typescript-eslint/no-non-null-assertion': 'off',
3131
'no-unused-vars': 'off',
32+
'@typescript-eslint/no-empty-object-type': 'off',
3233
'@typescript-eslint/no-unused-vars': [
3334
'error',
3435
{

docs/working-notes/todo4.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,13 @@ must use <img /> tag for srcset and sizes to be in dom instead of const img = ne
131131

132132
loading... div bottom indicator
133133
set min-height blank gallery
134+
zabluri prvo postojecu, pa onda promeni
135+
136+
check slug to id in feed and og-image
137+
PostCollection, Post, idToSlug
138+
replace getAllPosts with getAllPostsWithReadingTime,
139+
140+
spellcheck yarn script
134141
```
135142

136143

package.json

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,8 @@
5151
"mdast-util-to-string": "^4.0.0",
5252
"object-treeify": "^5.0.1",
5353
"photoswipe": "^5.4.4",
54-
"react": "^18.3.1",
55-
"react-dom": "^18.3.1",
56-
"react-grid-gallery": "^1.0.1",
57-
"react-image-lightbox": "^5.1.4",
54+
"react": "^19.0.0",
55+
"react-dom": "^19.0.0",
5856
"reading-time": "^1.5.0",
5957
"sharp": "0.33.5",
6058
"tailwind-clip-path": "^1.0.0",
@@ -69,8 +67,8 @@
6967
"@tailwindcss/typography": "^0.5.16",
7068
"@types/lodash.debounce": "^4.0.9",
7169
"@types/mdast": "^4.0.3",
72-
"@types/react": "^18.3.12",
73-
"@types/react-dom": "^18.3.1",
70+
"@types/react": "^19.0.12",
71+
"@types/react-dom": "^19.0.4",
7472
"@typescript-eslint/eslint-plugin": "^8.27.0",
7573
"@typescript-eslint/parser": "^8.27.0",
7674
"eslint": "8.57.0",

src/components/Gallery.astro

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
import NewReactGallery from '@/components/react/NewGallery';
2+
import ReactGallery from '@/components/react/Gallery';
33
import { getGalleryImages } from '@/libs/gallery/images';
44
import { randomizeArray } from '@/utils/objects';
55
@@ -12,5 +12,5 @@ const { class: className } = Astro.props;
1212
---
1313

1414
<div class={className}>
15-
<NewReactGallery client:only="react" images={randomizedGalleryImages} />
15+
<ReactGallery client:only="react" images={randomizedGalleryImages} />
1616
</div>

src/components/react/Gallery.tsx

Lines changed: 109 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,124 @@
1-
import { useState } from 'react';
1+
import { useEffect, useMemo, useRef, useState } from 'react';
22

3-
import { Gallery as ReactGridGallery } from 'react-grid-gallery';
4-
import Lightbox from 'react-image-lightbox';
3+
import debounce from 'lodash.debounce';
4+
import PhotoSwipeLightbox from 'photoswipe/lightbox';
55

6-
import type { ImageProps } from '@/types/common';
6+
import { GALLERY } from '@/constants/gallery';
7+
import { cn } from '@/utils/styles';
78

8-
import 'react-image-lightbox/style.css';
9+
import type { GalleryImage } from '@/types/image';
10+
import type { FC } from 'react';
11+
12+
import 'photoswipe/style.css';
913

1014
interface Props {
11-
images: ImageProps[];
15+
images: GalleryImage[];
1216
}
1317

14-
// global polyfill for react-image-lightbox
15-
window.global = window.global || window;
18+
type LoadedStates = Record<string, boolean>;
19+
20+
const { PAGE_SIZE, INITIAL_PAGE, OBSERVER_DEBOUNCE, GALLERY_ID } = GALLERY;
21+
22+
const fetchImagesUpToPage = (images: GalleryImage[], nextPage: number): GalleryImage[] => {
23+
const endIndex = nextPage * PAGE_SIZE;
24+
return images.slice(0, endIndex);
25+
};
26+
27+
const Gallery: FC<Props> = ({ images }) => {
28+
const [loadedImages, setLoadedImages] = useState<GalleryImage[]>([]);
29+
const [page, setPage] = useState<number>(INITIAL_PAGE);
30+
const observerTarget = useRef(null);
31+
32+
const [loadedStates, setLoadedStates] = useState<LoadedStates>({});
33+
// calculate if new page is loaded on scroll
34+
// not for blur transition
35+
const isAllImagesLoaded = useMemo(
36+
() => Object.values(loadedStates).every(Boolean),
37+
[loadedStates, loadedImages.length]
38+
);
39+
40+
const isEnd = loadedImages.length === images.length;
41+
42+
// converts page to loaded images
43+
useEffect(() => {
44+
const upToPageImages = fetchImagesUpToPage(images, page);
45+
setLoadedImages(upToPageImages);
46+
}, [page, images]);
47+
48+
// sets only page
49+
useEffect(() => {
50+
const callback: IntersectionObserverCallback = (entries) => {
51+
// must wait here for images to load
52+
if (!isEnd && isAllImagesLoaded && entries[0].isIntersecting) {
53+
setPage((prevPage) => prevPage + 1);
54+
}
55+
};
56+
const debouncedCallback = debounce(callback, OBSERVER_DEBOUNCE);
57+
const options: IntersectionObserverInit = { threshold: 1 };
58+
59+
const observer = new IntersectionObserver(debouncedCallback, options);
60+
61+
const observerRef = observerTarget.current;
62+
if (observerRef) observer.observe(observerRef);
63+
64+
return () => {
65+
if (observerRef) observer.unobserve(observerRef);
66+
};
67+
// page dependency is important for initial load to work for all resolutions
68+
}, [observerTarget, page, isEnd]);
1669

17-
const Gallery: React.FC<Props> = ({ images }) => {
18-
const [index, setIndex] = useState(-1);
70+
// lightbox
71+
useEffect(() => {
72+
let lightbox: PhotoSwipeLightbox | null = new PhotoSwipeLightbox({
73+
gallery: '#' + GALLERY_ID,
74+
children: 'a',
75+
pswpModule: () => import('photoswipe'),
76+
});
77+
lightbox.init();
1978

20-
const currentImage = images[index];
21-
const nextIndex = (index + 1) % images.length;
22-
const nextImage = images[nextIndex] || currentImage;
23-
const prevIndex = (index + images.length - 1) % images.length;
24-
const prevImage = images[prevIndex] || currentImage;
79+
return () => {
80+
lightbox?.destroy();
81+
lightbox = null;
82+
};
83+
}, []);
2584

26-
const handleClick = (index: number, _item: ImageProps) => setIndex(index);
27-
const handleClose = () => setIndex(-1);
28-
const handleMovePrev = () => setIndex(prevIndex);
29-
const handleMoveNext = () => setIndex(nextIndex);
85+
const handleLoad = (src: string) => {
86+
setLoadedStates((prev) => ({ ...prev, [src]: true }));
87+
};
3088

3189
return (
32-
<div>
33-
<ReactGridGallery images={images} onClick={handleClick} enableImageSelection={false} />
34-
{!!currentImage && (
35-
<Lightbox
36-
imageTitle={currentImage.caption}
37-
mainSrc={currentImage.originalSrc}
38-
mainSrcThumbnail={currentImage.src}
39-
nextSrc={nextImage.originalSrc}
40-
nextSrcThumbnail={nextImage.src}
41-
prevSrc={prevImage.originalSrc}
42-
prevSrcThumbnail={prevImage.src}
43-
onCloseRequest={handleClose}
44-
onMovePrevRequest={handleMovePrev}
45-
onMoveNextRequest={handleMoveNext}
46-
/>
47-
)}
48-
</div>
90+
<>
91+
<div
92+
id={GALLERY_ID}
93+
className="pswp-gallery grid grid-cols-1 gap-1 sm:grid-cols-2 lg:grid-cols-3"
94+
>
95+
{loadedImages.map((image) => (
96+
<a
97+
key={`${GALLERY_ID}--${image.lightbox.src}`}
98+
// lightbox doesn't support responsive image
99+
href={image.lightbox.src}
100+
data-pswp-width={image.lightbox.width}
101+
data-pswp-height={image.lightbox.height}
102+
target="_blank"
103+
rel="noreferrer"
104+
>
105+
<img
106+
{...image.thumbnail}
107+
onLoad={() => handleLoad(image.thumbnail.src)}
108+
alt="Gallery image"
109+
className={cn(
110+
'w-full transition-all duration-[2s] ease-in-out',
111+
loadedStates[image.thumbnail.src]
112+
? 'opacity-100 blur-0 grayscale-0'
113+
: 'opacity-75 blur-sm grayscale'
114+
)}
115+
/>
116+
</a>
117+
))}
118+
</div>
119+
{/* control threshold with margin-top */}
120+
<div ref={observerTarget} className="mt-0"></div>
121+
</>
49122
);
50123
};
51124

src/components/react/ImageBlurPreloader.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,18 @@ import { useEffect, useState } from 'react';
33
import usePrevious from '@/components/react/hooks/usePrevious';
44
import { cn } from '@/utils/styles';
55

6-
import type { ImgAttributes } from '@/libs/gallery/images';
6+
import type { ImgTagAttributes } from '@/types/image';
77
import type { FC } from 'react';
88

99
interface Props {
10-
blurAttributes: ImgAttributes;
11-
mainAttributes: ImgAttributes;
10+
blurAttributes: ImgTagAttributes;
11+
mainAttributes: ImgTagAttributes;
1212
className?: string;
1313
divClassName?: string;
1414
onMainLoaded?: () => void;
1515
}
1616

17-
const initialAttributes: ImgAttributes = { src: '' } as const;
17+
const initialAttributes: ImgTagAttributes = { src: '' } as const;
1818

1919
const ImageBlurPreloader: FC<Props> = ({
2020
blurAttributes = initialAttributes,
@@ -28,6 +28,7 @@ const ImageBlurPreloader: FC<Props> = ({
2828

2929
// reset hasLoaded on main image change
3030
useEffect(() => {
31+
// store var in useMemo
3132
if (prevMainAttributes !== mainAttributes) {
3233
setHasLoaded(false);
3334
}
@@ -36,7 +37,7 @@ const ImageBlurPreloader: FC<Props> = ({
3637
// important: main image must be in DOM for onLoad to work
3738
// unmount and display: none will fail
3839
const handleLoad = () => {
39-
setHasLoaded(true);
40+
setHasLoaded(true); // check if new image
4041
onMainLoaded?.();
4142
};
4243

src/components/react/ImageRandom.tsx

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import ImageBlurPreloader from '@/components/react/ImageBlurPreloader';
44
import { getRandomElementFromArray } from '@/utils/array';
55
import { cn } from '@/utils/styles';
66

7-
import type { HeroImage, ImgAttributes } from '@/libs/gallery/images';
7+
import type { HeroImage, ImgTagAttributes } from '@/types/image';
88
import type { FC, ImgHTMLAttributes } from 'react';
99

1010
interface Props extends ImgHTMLAttributes<HTMLImageElement> {
@@ -13,7 +13,7 @@ interface Props extends ImgHTMLAttributes<HTMLImageElement> {
1313
divClassName?: string;
1414
}
1515

16-
const imageAttributes: ImgAttributes = {
16+
const imageAttributes: ImgTagAttributes = {
1717
src: '',
1818
width: 0,
1919
height: 0,
@@ -39,21 +39,11 @@ const ImageRandomReact: FC<Props> = ({ galleryImages, className, divClassName, .
3939
setImage(randomImage);
4040
};
4141

42-
const moreBlurAttributes = {
43-
alt: 'Blur image',
44-
onClick: handleClick,
45-
};
46-
47-
const moreMainAttributes = {
48-
alt: 'Hero image',
49-
onClick: handleClick,
50-
};
51-
5242
return (
5343
<ImageBlurPreloader
5444
{...props}
55-
blurAttributes={{ ...image.blur, ...moreBlurAttributes }}
56-
mainAttributes={{ ...image.hero, ...moreMainAttributes }}
45+
blurAttributes={{ ...image.blur, alt: '' }}
46+
mainAttributes={{ ...image.hero, onClick: handleClick, alt: 'Hero image' }}
5747
className={cn('cursor-pointer my-0', className)}
5848
divClassName={divClassName}
5949
/>

0 commit comments

Comments
 (0)