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;