diff --git a/public/images/animals.jpg b/public/images/animals.jpg
new file mode 100644
index 0000000..e02d994
Binary files /dev/null and b/public/images/animals.jpg differ
diff --git a/public/images/art.jpg b/public/images/art.jpg
new file mode 100644
index 0000000..21352ec
Binary files /dev/null and b/public/images/art.jpg differ
diff --git a/public/images/bitcoin.jpg b/public/images/bitcoin.jpg
new file mode 100644
index 0000000..ca1a79f
Binary files /dev/null and b/public/images/bitcoin.jpg differ
diff --git a/public/images/gardening.jpg b/public/images/gardening.jpg
new file mode 100644
index 0000000..5a9bde4
Binary files /dev/null and b/public/images/gardening.jpg differ
diff --git a/public/images/lifestyle.jpg b/public/images/lifestyle.jpg
new file mode 100644
index 0000000..abe9708
Binary files /dev/null and b/public/images/lifestyle.jpg differ
diff --git a/public/images/nostr.jpg b/public/images/nostr.jpg
new file mode 100644
index 0000000..b2a421a
Binary files /dev/null and b/public/images/nostr.jpg differ
diff --git a/public/images/photography.jpg b/public/images/photography.jpg
new file mode 100644
index 0000000..06aab3c
Binary files /dev/null and b/public/images/photography.jpg differ
diff --git a/src/components/Home/Home.css b/src/components/Home/Home.css
new file mode 100644
index 0000000..cab11a2
--- /dev/null
+++ b/src/components/Home/Home.css
@@ -0,0 +1,41 @@
+.home {
+ box-sizing: border-box;
+ width: 100vw;
+ max-width: 50em;
+ padding: 2em;
+ margin: auto;
+}
+
+.home .topics {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.home .topic {
+ background-color: #222;
+ border-radius: 16px;
+ padding: 1em;
+ cursor: pointer;
+ height: 6em;
+ background-size: cover;
+}
+
+.home .topic-title {
+ font-size: 30px;
+ padding-bottom: 0.5em;
+ text-transform: capitalize;
+}
+
+.home .tag {
+ display: inline;
+ padding: 0.2em 0.6em;
+ margin-right: 0.2em;
+ border-radius: 24px;
+ background-color: rgba(0, 0, 0, 0.4);
+ color: white;
+ line-height: 2.2em;
+ font-size: 1rem;
+ cursor: pointer;
+ overflow: visible;
+}
diff --git a/src/components/Home/index.tsx b/src/components/Home/index.tsx
new file mode 100644
index 0000000..37d0f10
--- /dev/null
+++ b/src/components/Home/index.tsx
@@ -0,0 +1,38 @@
+import { topics } from '../env';
+import useNav from '../../utils/useNav';
+import './Home.css';
+
+const Home = () => {
+ const { nav, currentSettings } = useNav();
+
+ const topicKeys = Object.keys(topics);
+
+ return (
+
+
Topics
+
+ {topicKeys.map(tk => (
+
nav({ ...currentSettings, tags: topics[tk].tags })}
+ >
+
{tk}
+{/*
+
+ {topics[tk].tags.slice(0, 5).map(t => (
+ <>
+ {t}{' '}
+ >
+ ))}
+ {topics[tk].tags.length > 5 ? '...' : ''}
+
+ */}
+
+ ))}
+
+
+ );
+};
+
+export default Home;
diff --git a/src/components/MasonryView/MasonryImage.tsx b/src/components/MasonryView/MasonryImage.tsx
new file mode 100644
index 0000000..b25115d
--- /dev/null
+++ b/src/components/MasonryView/MasonryImage.tsx
@@ -0,0 +1,51 @@
+import { MouseEventHandler, SyntheticEvent, useState } from 'react';
+import { NostrImage, createImgProxyUrl, isVideo } from '../nostrImageDownload';
+import LazyLoad from 'react-lazy-load';
+
+interface GridImageProps {
+ image: NostrImage;
+ onClick?: MouseEventHandler | undefined;
+ index: number;
+}
+
+const GridImage = ({ image, onClick, index }: GridImageProps) => {
+ const [loaded, setLoaded] = useState(false);
+
+ const mediaIsVideo = isVideo(image.url);
+
+ return (
+
+
+ {mediaIsVideo ? (
+
+ ) : (
+ ) => {
+ console.log('not found: ', e.currentTarget.src);
+ e.currentTarget.src = '/notfound.png';
+ }}
+ className={`image ${loaded ? 'show' : ''}`}
+ onLoad={() => setLoaded(true)}
+ loading="lazy"
+ key={image.url}
+ onClick={onClick}
+ src={createImgProxyUrl(image.url, 200, -1)}
+ >
+ )}
+
+
+ );
+};
+
+export default GridImage;
diff --git a/src/components/MasonryView/MasonryView.css b/src/components/MasonryView/MasonryView.css
new file mode 100644
index 0000000..2502ee5
--- /dev/null
+++ b/src/components/MasonryView/MasonryView.css
@@ -0,0 +1,79 @@
+@keyframes showGridImage {
+ from {
+ opacity: 0;
+ visibility: visible;
+ }
+
+ to {
+ opacity: 1;
+ }
+}
+
+.gridview {
+ display: flex;
+ flex-direction: row;
+ align-items: start;
+ height: 100dvh;
+ overflow: scroll;
+}
+
+.imagegrid {
+ display: block;
+ padding: 1rem;
+ width: 100vw;
+ box-sizing: border-box;
+}
+
+.imagegrid img.image {
+ border-radius: 0.5rem;
+ width: 100%;
+ cursor: pointer;
+ visibility: hidden;
+}
+
+.imagegrid video.image {
+ border-radius: 0.5rem;
+ width: 100%;
+ cursor: pointer;
+}
+
+.imagegrid .image.show {
+ animation-duration: 0.5s;
+ animation-timing-function: ease-in;
+ animation-name: showGridImage;
+ visibility: visible;
+}
+
+@media screen and (max-width: 600px) {
+ .imagegrid {
+ grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
+ grid-gap: 8px;
+ padding: 8px;
+ }
+}
+
+.imagegrid .image:hover {
+ filter: brightness(1.1);
+ outline: 1px solid #fff;
+}
+
+.profile-header {
+ padding: 2em;
+ padding-bottom: 1em;
+}
+
+.profile-header h2 {
+ margin: 0px;
+}
+
+.profile-header .author-info {
+ position: relative;
+ bottom: initial;
+ left: initial;
+}
+
+@media screen and (max-width: 768px) {
+ .profile-header .author-info .author-name {
+ display: block;
+ }
+}
diff --git a/src/components/MasonryView/MasonryView.tsx b/src/components/MasonryView/MasonryView.tsx
new file mode 100644
index 0000000..77b9f72
--- /dev/null
+++ b/src/components/MasonryView/MasonryView.tsx
@@ -0,0 +1,131 @@
+import { useEffect, useMemo } from 'react';
+import { NostrImage, urlFix } from '../nostrImageDownload';
+import './MasonryView.css';
+import { Settings } from '../../utils/useNav';
+import AuthorProfile from '../AuthorProfile/AuthorProfile';
+import { useSwipeable } from 'react-swipeable';
+import { Helmet } from 'react-helmet';
+import useProfile from '../../utils/useProfile';
+import { ViewMode } from '../SlideShow';
+import { useGlobalState } from '../../utils/globalState';
+import { Dictionary, groupBy } from 'lodash';
+
+type MasonryViewProps = {
+ settings: Settings;
+ images: NostrImage[];
+ currentImage: number | undefined;
+ setCurrentImage: React.Dispatch>;
+ setViewMode: React.Dispatch>;
+};
+
+const MasonryView = ({ settings, images, currentImage, setCurrentImage, setViewMode }: MasonryViewProps) => {
+ const { activeProfile, title } = useProfile(settings);
+ const [_, setState] = useGlobalState();
+
+ const numColumns = 4;
+
+ const sortedImages = useMemo(
+ () => {
+ const sorted = images.sort((a, b) => (b.timestamp && a.timestamp ? b.timestamp - a.timestamp : 0)); // sort by timestamp descending
+ const grouped = groupBy(sorted, (i: number) => Math.floor(i / numColumns)) as Dictionary;
+ console.log(grouped);
+ return grouped;
+ },
+ [images] // settings is not used here, but we need to include it to trigger a re-render when it changes
+ );
+ console.log(sortedImages);
+
+ const showNextImage = () => {
+ setCurrentImage(idx => (idx !== undefined ? idx + 1 : 0));
+ };
+
+ const showPreviousImage = () => {
+ setCurrentImage(idx => (idx !== undefined && idx > 0 ? idx - 1 : idx));
+ };
+
+ const onKeyDown = (event: KeyboardEvent) => {
+ console.log(event);
+
+ if (event.key === 'ArrowRight') {
+ showNextImage();
+ }
+ if (event.key === 'ArrowLeft') {
+ showPreviousImage();
+ }
+ /*
+ if (event.key === 'Escape') {
+ setCurrentImage(undefined);
+ }
+ */
+ };
+
+ const swipeHandlers = useSwipeable({
+ onSwipedLeft: () => {
+ showNextImage();
+ },
+ onSwipedRight: () => {
+ showPreviousImage();
+ },
+ });
+
+ useEffect(() => {
+ document.body.addEventListener('keydown', onKeyDown);
+ setState({ activeImage: undefined });
+
+ if (currentImage) {
+ console.log('setting hash to #g' + currentImage);
+ window.location.hash = '#g' + currentImage;
+ }
+
+ return () => {
+ document.body.removeEventListener('keydown', onKeyDown);
+ };
+ }, []);
+
+ return (
+
+
+ {title}
+
+ {/*
+ {currentImage !== undefined ? (
+
+ ) : null}
+ */}
+ {(activeProfile || settings.tags.length == 1) && (
+
+ {activeProfile ? (
+
+ ) : (
+ settings.tags.map(t =>
#{t}
)
+ )}
+
+ )}
+ {/*
+
+ {sortedImages[0] && sortedImages[0].map((image, idx) => (
+ {
+ e.stopPropagation();
+ setCurrentImage(idx);
+ setViewMode('scroll');
+ }}
+ >
+ ))}
+
+ */}
+
+ );
+};
+
+export default MasonryView;