Skip to content

Commit bb8b233

Browse files
authored
Merge pull request #6 from nemanjam/feature/add-gallery-page
Add gallery page
2 parents 1c88769 + 3e42b15 commit bb8b233

File tree

17 files changed

+363
-11
lines changed

17 files changed

+363
-11
lines changed
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
---
2+
import { Image } from 'astro:assets';
3+
4+
import ReactGallery from '@/components/react/Gallery';
5+
import { IMAGE_SIZES } from '@/constants/image';
6+
import { cn } from '@/utils/styles';
7+
8+
import type { AstroImageProps, ImageProps } from '@/types/common';
9+
import type { ImageMetadata } from 'astro';
10+
11+
export interface Props extends astroHTML.JSX.HTMLAttributes {}
12+
13+
// filenames
14+
const EXCLUDE_IMAGES = ['avatar1.jpg'];
15+
16+
const getAllImagesMetadata = (): ImageMetadata[] => {
17+
const imageModules = import.meta.glob<{ default: ImageMetadata }>(
18+
// cant be even variable
19+
'/src/assets/images/all-images/*.jpg',
20+
{ eager: true }
21+
);
22+
23+
// convert map to array
24+
const imagesMetadata = Object.keys(imageModules)
25+
// filter excluded filenames
26+
.filter((path) => !EXCLUDE_IMAGES.some((excludedFileName) => path.endsWith(excludedFileName)))
27+
// return metadata array
28+
.map((path) => imageModules[path].default);
29+
return imagesMetadata;
30+
};
31+
32+
const imagesMetadata = getAllImagesMetadata();
33+
34+
const imageMetadataToAstroImageProps = (imagesMetadata: ImageMetadata): AstroImageProps => ({
35+
src: imagesMetadata,
36+
...IMAGE_SIZES.FIXED.MDX_XXS_16_9,
37+
alt: 'Gallery image',
38+
});
39+
40+
const astroImagePropsToReactImageProps = (astroImageProps: AstroImageProps): ImageProps => {
41+
// console.log('astroImageProps', JSON.stringify(astroImageProps, null, 2));
42+
43+
const astroImageSrc = astroImageProps.src as ImageMetadata;
44+
45+
return {
46+
src: astroImageSrc.src,
47+
originalSrc: astroImageSrc.src,
48+
width: parseInt(String(astroImageProps.width)),
49+
height: parseInt(String(astroImageProps.height)),
50+
};
51+
};
52+
53+
const astroImages = imagesMetadata.map((metadata) => imageMetadataToAstroImageProps(metadata));
54+
const reactImages = astroImages.map((astroProps) => astroImagePropsToReactImageProps(astroProps));
55+
56+
console.log('images', JSON.stringify(reactImages, null, 2));
57+
58+
const { class: className } = Astro.props;
59+
---
60+
61+
<div class={cn('', className)}>
62+
<ReactGallery
63+
client:only="react"
64+
images={reactImages}
65+
thumbnailImageComponent={(<Image {...IMAGE_SIZES.FIXED.MDX_XXS_16_9} src="" alt="default" />)}
66+
/>
67+
</div>
68+
{
69+
/*
70+
71+
/@fs/home/username/Desktop/nemanjam.github.io/src/assets/images/all-images/cyco5.jpg?origWidth=4608&origHeight=2592&origFormat=jpg
72+
73+
<ul class={cn('grid gap-4 grid-cols-1 sm:grid-cols-2 md:grid-cols-3', className)}>
74+
{
75+
imagesMetadata.map((image) => (
76+
<li>
77+
<Image {...IMAGE_SIZES.FIXED.MDX_XXS_16_9} src={image} alt="my image" />
78+
</li>
79+
))
80+
}
81+
</ul>
82+
83+
interface Image {
84+
key?: Key;
85+
src: string;
86+
width: number;
87+
height: number;
88+
nano?: string;
89+
alt?: string;
90+
tags?: ImageTag[];
91+
isSelected?: boolean;
92+
caption?: ReactNode;
93+
customOverlay?: ReactNode;
94+
thumbnailCaption?: ReactNode;
95+
orientation?: number;
96+
}
97+
*/
98+
}

docs/working-notes/todo2.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,9 @@ https://lea.verou.me/blog/2023/going-lean/#public-or-private-repo%3F
200200
analytics, internationalisation, github issue template, small project
201201
https://github.com/wanoo21/tailwind-astro-starting-blog
202202

203+
// good blog design
204+
https://www.builder.io/blog/react-intersection-observer
205+
203206
-----------
204207
za local state mora react ili solid
205208
for state between pages nanostore with localStorage

docs/working-notes/todo3.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,4 +472,26 @@ astro env vars sa schema
472472
astro component container, render component to string for rss
473473
css directive transitions
474474
remote collection 2.0, custom folder
475+
476+
-------------
477+
// gallery
478+
479+
480+
For lazy loading checkout unpic - https://unpic.pics/lib/
481+
482+
For image resizing I would use a CDN - depending on usage you might be able to use a free tier or pay very little.
483+
484+
For Gallery - https://benhowell.github.io/react-grid-gallery - which is client side React. For onscroll I used react-intersection-observer - https://www.builder.io/blog/react-intersection-observer
485+
486+
I hope this helps, sorry I can't share my code.
487+
----
488+
glob za slike
489+
poenta, ne sme da ima {} za jednu opciju
490+
ovo puca
491+
'/src/assets/images/all-images/*.{jpg}',
492+
ovo radi
493+
'/src/assets/images/all-images/*.jpg',
494+
495+
496+
------------
475497
```

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@
4848
"object-treeify": "^4.0.1",
4949
"react": "^18.3.1",
5050
"react-dom": "^18.3.1",
51+
"react-grid-gallery": "^1.0.1",
52+
"react-image-lightbox": "^5.1.4",
5153
"reading-time": "^1.5.0",
5254
"sharp": "0.32.6",
5355
"tailwind-clip-path": "^1.0.0",

src/components/Gallery.astro

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
---
2+
import { getImage } from 'astro:assets';
3+
4+
import ReactGallery from '@/components/react/Gallery';
5+
import { IMAGE_SIZES } from '@/constants/image';
6+
import { cn } from '@/utils/styles';
7+
8+
import type { ImageProps } from '@/types/common';
9+
import type { ImageMetadata } from 'astro';
10+
11+
export interface Props extends astroHTML.JSX.HTMLAttributes {}
12+
13+
// filenames
14+
const EXCLUDE_IMAGES = ['avatar1.jpg'];
15+
16+
const getAllImagesMetadata = (): ImageMetadata[] => {
17+
const imageModules = import.meta.glob<{ default: ImageMetadata }>(
18+
// cant be even variable
19+
'/src/assets/images/all-images/*.jpg',
20+
{ eager: true }
21+
);
22+
23+
// convert map to array
24+
const imagesMetadata = Object.keys(imageModules)
25+
// filter excluded filenames
26+
.filter((path) => !EXCLUDE_IMAGES.some((excludedFileName) => path.endsWith(excludedFileName)))
27+
// return metadata array
28+
.map((path) => imageModules[path].default);
29+
return imagesMetadata;
30+
};
31+
32+
const imagesMetadata = getAllImagesMetadata();
33+
34+
const imageMetadataToReactImageProps = async (
35+
imagesMetadata: ImageMetadata
36+
): Promise<ImageProps> => {
37+
const astroImageProps = {
38+
src: imagesMetadata,
39+
format: 'webp',
40+
};
41+
42+
const thumbnailAstroImageProps = {
43+
...astroImageProps,
44+
...IMAGE_SIZES.FIXED.MDX_XXS_16_9,
45+
alt: 'Thumbnail image',
46+
};
47+
48+
const lightboxAstroImageProps = {
49+
...astroImageProps,
50+
...IMAGE_SIZES.FIXED.MDX_XL_16_9,
51+
alt: 'Lightbox image',
52+
};
53+
54+
const optimizedThumbnailAstroImageProps = await getImage(thumbnailAstroImageProps);
55+
const { src, attributes } = optimizedThumbnailAstroImageProps;
56+
57+
const optimizedLightboxAstroImageProps = await getImage(lightboxAstroImageProps);
58+
const { src: originalSrc } = optimizedLightboxAstroImageProps;
59+
60+
const reactImageProps = {
61+
src,
62+
originalSrc,
63+
// width and height only for thumbnails
64+
width: parseInt(String(attributes.width)),
65+
height: parseInt(String(attributes.height)),
66+
};
67+
68+
return reactImageProps;
69+
};
70+
71+
const reactImages = await Promise.all(
72+
imagesMetadata.map((metadata) => imageMetadataToReactImageProps(metadata))
73+
);
74+
75+
// console.log('reactImages', reactImages);
76+
77+
const { class: className } = Astro.props;
78+
---
79+
80+
<div class={cn('', className)}>
81+
<ReactGallery client:only="react" images={reactImages} />
82+
</div>

src/components/react/Gallery.tsx

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { useState } from 'react';
2+
3+
import { Gallery as ReactGridGallery } from 'react-grid-gallery';
4+
import Lightbox from 'react-image-lightbox';
5+
6+
import type { ImageProps } from '@/types/common';
7+
8+
import 'react-image-lightbox/style.css';
9+
10+
interface Props {
11+
images: ImageProps[];
12+
}
13+
14+
// global polyfill for react-image-lightbox
15+
window.global = window.global || window;
16+
17+
const Gallery: React.FC<Props> = ({ images }) => {
18+
const [index, setIndex] = useState(-1);
19+
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;
25+
26+
const handleClick = (index: number, _item: ImageProps) => setIndex(index);
27+
const handleClose = () => setIndex(-1);
28+
const handleMovePrev = () => setIndex(prevIndex);
29+
const handleMoveNext = () => setIndex(nextIndex);
30+
31+
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>
49+
);
50+
};
51+
52+
export default Gallery;

src/components/react/ScrollToTop.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ const ScrollToTop: React.FC<Props> = ({ children }) => {
5757
};
5858

5959
// bellow 100 or it will break again on fast scroll
60+
// todo: set threshold and remove debounce
6061
const debouncedCallback = debounce(callback, 20);
6162
const intersect = new IntersectionObserver(debouncedCallback);
6263

src/constants/navigation.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ export const NAVIGATION_ITEMS = [
2626
title: 'About',
2727
path: ROUTES.ABOUT,
2828
},
29+
{
30+
title: 'Gallery',
31+
path: ROUTES.GALLERY,
32+
},
2933
// {
3034
// title: 'Resume',
3135
// path: ROUTES.RESUME,

src/constants/routes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export const ROUTES = {
1515
EXPLORE_TAGS: '/blog/explore/tags/',
1616
EXPLORE_CATEGORIES: '/blog/explore/categories/',
1717
DESIGN: '/design/',
18+
GALLERY: '/gallery/',
1819
/** maybe in future */
1920
DRAFTS: '/drafts/',
2021
_404: '/404/',

src/layouts/Centered.astro

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,25 @@
11
---
22
import Base from '@/layouts/Base.astro';
3+
import { cn } from '@/utils/styles';
34
45
import type { BaseProps } from '@/layouts/Base.astro';
56
6-
export interface Props extends BaseProps {}
7+
export interface Props extends BaseProps, astroHTML.JSX.HTMLAttributes {}
78
89
const props = Astro.props;
10+
11+
// targets <main /> tag
12+
const { class: className } = Astro.props;
913
---
1014

1115
{/* flex here to take full height for pagination, flex-row align-stretch */}
1216
{/* h-full dosent work without all parents h-full */}
1317

1418
<Base {...props}>
15-
<main id="main" class="flex-grow flex flex-col centered-px max-w-4xl py-4 lg:py-8">
19+
<main
20+
id="main"
21+
class={cn('flex-grow flex flex-col centered-px max-w-4xl py-4 lg:py-8', className)}
22+
>
1623
<slot />
1724
</main>
1825
</Base>

0 commit comments

Comments
 (0)