feat: now home page

This commit is contained in:
florian 2024-02-19 20:07:23 +01:00
parent 38743f7625
commit bc8e118429
12 changed files with 340 additions and 0 deletions

BIN
public/images/animals.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 486 KiB

BIN
public/images/art.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 710 KiB

BIN
public/images/bitcoin.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 KiB

BIN
public/images/gardening.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 510 KiB

BIN
public/images/lifestyle.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

BIN
public/images/nostr.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 366 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 457 KiB

View File

@ -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;
}

View File

@ -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 (
<div className="home">
<h2>Topics</h2>
<div className="topics">
{topicKeys.map(tk => (
<div
className="topic"
style={{ backgroundImage: `linear-gradient(to bottom, rgba(0, 0, 0, .8) 0%, rgba(0, 0, 0, 0) 40%, rgba(0, 0, 0, 0) 100%), url('/images/${tk}.jpg')` }}
onClick={() => nav({ ...currentSettings, tags: topics[tk].tags })}
>
<div className="topic-title">{tk}</div>
{/*
<div>
{topics[tk].tags.slice(0, 5).map(t => (
<>
<span className="tag">{t}</span>{' '}
</>
))}
{topics[tk].tags.length > 5 ? '...' : ''}
</div>
*/}
</div>
))}
</div>
</div>
);
};
export default Home;

View File

@ -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 (
<a id={'g' + index}>
<LazyLoad height={200}>
{mediaIsVideo ? (
<video
className={`image ${loaded ? 'show' : ''}`}
data-node-id={image.noteId}
key={image.url}
controls={false}
autoPlay={false}
onClick={onClick}
src={image.url + '#t=0.1'}
playsInline
onLoad={() => setLoaded(true)}
></video>
) : (
<img
data-node-id={image.noteId}
onError={(e: SyntheticEvent<HTMLImageElement>) => {
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)}
></img>
)}
</LazyLoad>
</a>
);
};
export default GridImage;

View File

@ -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;
}
}

View File

@ -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<React.SetStateAction<number | undefined>>;
setViewMode: React.Dispatch<React.SetStateAction<ViewMode>>;
};
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<NostrImage[]>;
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 (
<div className="gridview" {...swipeHandlers}>
<Helmet>
<title>{title}</title>
</Helmet>
{/*
{currentImage !== undefined ? (
<DetailsView images={sortedImages} currentImage={currentImage} setCurrentImage={setCurrentImage} />
) : null}
*/}
{(activeProfile || settings.tags.length == 1) && (
<div className="profile-header">
{activeProfile ? (
<AuthorProfile
src={urlFix(activeProfile.image || '')}
author={activeProfile.displayName || activeProfile.name}
npub={activeProfile.npub}
setViewMode={setViewMode}
followButton
externalLink
></AuthorProfile>
) : (
settings.tags.map(t => <h2>#{t}</h2>)
)}
</div>
)}
{/*
<div className="imagegrid">
{sortedImages[0] && sortedImages[0].map((image, idx) => (
<GridImage
index={idx}
key={image.url}
image={image}
onClick={e => {
e.stopPropagation();
setCurrentImage(idx);
setViewMode('scroll');
}}
></GridImage>
))}
</div>
*/}
</div>
);
};
export default MasonryView;