feat(catalog): add media, styling

This commit is contained in:
plebeius.eth
2024-04-12 17:47:30 +02:00
parent f124d03366
commit 65c99ab188
7 changed files with 197 additions and 82 deletions
+12 -6
View File
@@ -2,10 +2,11 @@ import { useTranslation } from 'react-i18next';
import { Link, useLocation, useParams } from 'react-router-dom'; import { Link, useLocation, useParams } from 'react-router-dom';
import { useSubscribe } from '@plebbit/plebbit-react-hooks'; import { useSubscribe } from '@plebbit/plebbit-react-hooks';
import styles from './board-buttons.module.css'; import styles from './board-buttons.module.css';
import { isPostPageView } from '../../lib/utils/view-utils'; import { isCatalogView, isPostPageView } from '../../lib/utils/view-utils';
interface BoardButtonsProps { interface BoardButtonsProps {
address: string | undefined; address: string | undefined;
isInCatalogView?: boolean;
} }
const OptionsButton = () => { const OptionsButton = () => {
@@ -13,11 +14,11 @@ const OptionsButton = () => {
return <button className='button'>{t('options')}</button>; return <button className='button'>{t('options')}</button>;
}; };
const CatalogButton = ({ address }: BoardButtonsProps) => { const CatalogButton = ({ address, isInCatalogView }: BoardButtonsProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<button className='button'> <button className='button'>
<Link to={`/p/${address}/catalog`}>{t('catalog')}</Link> <Link to={isInCatalogView ? `/p/${address}` : `/p/${address}/catalog`}>{t(isInCatalogView ? 'return' : 'catalog')}</Link>
</button> </button>
); );
}; };
@@ -79,20 +80,25 @@ export const MobileBoardButtons = () => {
export const DesktopBoardButtons = () => { export const DesktopBoardButtons = () => {
const { subplebbitAddress: address } = useParams<{ subplebbitAddress: string }>(); const { subplebbitAddress: address } = useParams<{ subplebbitAddress: string }>();
const isInPostPage = isPostPageView(useLocation().pathname, useParams());
const location = useLocation();
const params = useParams();
const isInPostPage = isPostPageView(location.pathname, params);
const isInCatalogView = isCatalogView(location.pathname, params);
return ( return (
<div className={styles.desktopBoardButtons}> <div className={styles.desktopBoardButtons}>
<hr /> <hr />
{isInPostPage ? ( {isInPostPage ? (
<> <>
[<ReturnButton address={address} />] [<CatalogButton address={address} />] [<BottomButton />] [<OptionsButton />] [<ReturnButton address={address} />] [<CatalogButton address={address} isInCatalogView={isInCatalogView} />] [<BottomButton />] [<OptionsButton />]
<span className={styles.subscribeButton}> <span className={styles.subscribeButton}>
[<SubscribeButton address={address} />] [<SubscribeButton address={address} />]
</span> </span>
</> </>
) : ( ) : (
<> <>
[<OptionsButton />] [<CatalogButton address={address} />] [<OptionsButton />] [<CatalogButton address={address} isInCatalogView={isInCatalogView} />]
<span className={styles.subscribeButton}> <span className={styles.subscribeButton}>
[<SubscribeButton address={address} />] [<SubscribeButton address={address} />]
</span> </span>
@@ -1,39 +1,59 @@
.row { .row {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: space-around; justify-content: center;
align-items: center; text-align: center;
padding-bottom: 20px;
} }
.post { .post {
width: 180px; width: 180px;
font-size: 12px; position: relative;
max-height: 320px;
/* fix text overflowing */ padding: 10px 0 3px 0;
vertical-align: top;
display: inline-block;
overflow: hidden; overflow: hidden;
} }
.postHeader { .mediaPaddingWrapper {
/* make cursors like button */ padding-bottom: 5px;
cursor: pointer; display: flex;
justify-content: center;
align-items: center;
} }
.postStats {
font-weight: bold;
}
.media {
height: 180px;
}
/* media and no media must be the same height, or the infinite scroll bugs out */
.mediaWrapper { .mediaWrapper {
overflow: hidden; background-color: var(--media-thumbnail-background-color);
width: 180px; width: var(--width);
height: 180px; height: var(--height);
max-width: 150px;
max-height: 150px;
} }
/* add some padding to rows when there's no image */ .post img, .post video, .post audio {
.noMedia { box-shadow: 0 0 5px rgba(0, 0, 0, 0.25);
height: 2px; width: 100%;
height: 100%;
min-width: 50px;
min-height: 50px;
object-fit: contain;
} }
.meta {
font-size: 11px;
line-height: 8px;
padding: 2px 0 1px 0;
}
.teaser {
display: block;
padding: 0 15px;
overflow: hidden;
word-wrap: break-word;
}
.post a {
color: unset;
text-decoration: none;
}
+81 -5
View File
@@ -1,10 +1,88 @@
import styles from './catalog-row.module.css';
import { Comment } from '@plebbit/plebbit-react-hooks'; import { Comment } from '@plebbit/plebbit-react-hooks';
import { getCommentMediaInfo, getHasThumbnail } from '../../lib/utils/media-utils';
import useFetchGifFirstFrame from '../../hooks/use-fetch-gif-first-frame';
import useCountLinksInReplies from '../../hooks/use-count-links-in-replies';
import styles from './catalog-row.module.css';
import { Link } from 'react-router-dom';
const CatalogPostMedia = ({ commentMediaInfo, link }: { commentMediaInfo: any; link: string }) => {
const { patternThumbnailUrl, thumbnail, type, url } = commentMediaInfo || {};
const iframeThumbnail = patternThumbnailUrl || thumbnail;
const gifFrameUrl = useFetchGifFirstFrame(type === 'gif' ? url : undefined);
let thumbnailComponent: React.ReactNode = null;
if (type === 'image') {
thumbnailComponent = <img src={url} alt='' />;
} else if (type === 'video') {
thumbnailComponent = thumbnail ? <img src={thumbnail} alt='' /> : <video src={`${url}#t=0.001`} />;
} else if (type === 'webpage') {
thumbnailComponent = <img src={thumbnail} alt='' />;
} else if (type === 'iframe') {
thumbnailComponent = iframeThumbnail ? <img src={iframeThumbnail} alt='' /> : null;
} else if (type === 'gif' && gifFrameUrl) {
thumbnailComponent = <img src={gifFrameUrl} alt='' />;
} else if (type === 'audio') {
thumbnailComponent = <audio src={url} controls />;
}
return thumbnailComponent;
};
const CatalogPost = ({ post }: { post: Comment }) => { const CatalogPost = ({ post }: { post: Comment }) => {
const { title, cid } = post || {}; const { cid, content, isDescription, isRules, link, linkHeight, linkWidth, replyCount, subplebbitAddress, title } = post || {};
const commentMediaInfo = getCommentMediaInfo(post);
const { type } = commentMediaInfo || {};
const hasThumbnail = getHasThumbnail(commentMediaInfo, link);
return <div className={styles.post}>{title || 'this is a post without a title'}</div>; let displayWidth, displayHeight;
const maxThumbnailSize = 150;
if (linkWidth && linkHeight) {
let scale = Math.min(1, maxThumbnailSize / Math.max(linkWidth, linkHeight));
displayWidth = `${linkWidth * scale}px`;
displayHeight = `${linkHeight * scale}px`;
} else {
displayWidth = `${maxThumbnailSize}px`;
displayHeight = `${maxThumbnailSize}px`;
}
if (type === 'audio') {
displayWidth = '150px';
displayHeight = '75px';
}
const thumbnailDimensions = { '--width': displayWidth, '--height': displayHeight } as React.CSSProperties;
const postLink = `/p/${subplebbitAddress}/${isDescription ? 'description' : isRules ? 'rules' : `c/${cid}`}`;
const linkCount = useCountLinksInReplies(post);
return (
<div className={styles.post}>
{hasThumbnail && (
<Link to={postLink}>
<div className={styles.mediaPaddingWrapper}>
<div className={styles.mediaWrapper} style={thumbnailDimensions}>
<CatalogPostMedia commentMediaInfo={commentMediaInfo} link={link} />
</div>
</div>
</Link>
)}
<div className={styles.meta}>
R: <b>{replyCount || '0'}</b>
{linkCount > 0 && (
<span>
{' '}
/ L: <b>{linkCount}</b>
</span>
)}
</div>
<Link to={postLink}>
<div className={styles.teaser}>
<b>{title && `${title}${content ? ': ' : ''}`}</b>
{content}
</div>
</Link>
</div>
);
}; };
interface CatalogRowProps { interface CatalogRowProps {
@@ -16,9 +94,7 @@ interface CatalogRowProps {
const CatalogRow = ({ description, index, row, rules }: CatalogRowProps) => { const CatalogRow = ({ description, index, row, rules }: CatalogRowProps) => {
const rowPosts = index === 0 ? [...(rules?.content?.length > 0 ? [rules] : []), ...(description?.content?.length > 0 ? [description] : []), ...row] : row; const rowPosts = index === 0 ? [...(rules?.content?.length > 0 ? [rules] : []), ...(description?.content?.length > 0 ? [description] : []), ...row] : row;
const posts = rowPosts.map((post, index) => <CatalogPost key={index} post={post} />); const posts = rowPosts.map((post, index) => <CatalogPost key={index} post={post} />);
return <div className={styles.row}>{posts}</div>; return <div className={styles.row}>{posts}</div>;
}; };
+33 -32
View File
@@ -36,8 +36,11 @@ const ThumbnailSmall = ({ style, children, thumbnailSmallPadding }: ThumbnailPro
); );
const Thumbnail = ({ commentMediaInfo, isMobile, isOutOfFeed, isReply, linkHeight, linkWidth, setShowThumbnail }: MediaProps) => { const Thumbnail = ({ commentMediaInfo, isMobile, isOutOfFeed, isReply, linkHeight, linkWidth, setShowThumbnail }: MediaProps) => {
const { patternThumbnailUrl, thumbnail, type, url } = commentMediaInfo || {};
let displayWidth, displayHeight; let displayWidth, displayHeight;
const maxThumbnailSize = isMobile || isReply ? 125 : 250; const maxThumbnailSize = isMobile || isReply ? 125 : 250;
if (linkWidth && linkHeight) { if (linkWidth && linkHeight) {
let scale = Math.min(1, maxThumbnailSize / Math.max(linkWidth, linkHeight)); let scale = Math.min(1, maxThumbnailSize / Math.max(linkWidth, linkHeight));
displayWidth = `${linkWidth * scale}px`; displayWidth = `${linkWidth * scale}px`;
@@ -47,7 +50,7 @@ const Thumbnail = ({ commentMediaInfo, isMobile, isOutOfFeed, isReply, linkHeigh
displayHeight = `${maxThumbnailSize}px`; displayHeight = `${maxThumbnailSize}px`;
} }
if (commentMediaInfo?.type === 'audio') { if (type === 'audio') {
displayWidth = '250px'; displayWidth = '250px';
displayHeight = '75px'; displayHeight = '75px';
} }
@@ -58,24 +61,21 @@ const Thumbnail = ({ commentMediaInfo, isMobile, isOutOfFeed, isReply, linkHeigh
} }
let thumbnailComponent: React.ReactNode = null; let thumbnailComponent: React.ReactNode = null;
const iframeThumbnail = commentMediaInfo?.patternThumbnailUrl || commentMediaInfo?.thumbnail; const iframeThumbnail = patternThumbnailUrl || thumbnail;
const gifFrameUrl = useFetchGifFirstFrame(commentMediaInfo?.type === 'gif' ? commentMediaInfo.url : undefined); const gifFrameUrl = useFetchGifFirstFrame(type === 'gif' ? url : undefined);
if (commentMediaInfo?.type === 'image') {
thumbnailComponent = <img src={commentMediaInfo.url} alt='' onClick={() => setShowThumbnail(false)} />; if (type === 'image') {
} else if (commentMediaInfo?.type === 'video') { thumbnailComponent = <img src={url} alt='' onClick={() => setShowThumbnail(false)} />;
thumbnailComponent = commentMediaInfo.thumbnail ? ( } else if (type === 'video') {
<img src={commentMediaInfo.thumbnail} alt='' /> thumbnailComponent = thumbnail ? <img src={thumbnail} alt='' /> : <video src={`${url}#t=0.001`} onClick={() => setShowThumbnail(false)} />;
) : ( } else if (type === 'webpage') {
<video src={`${commentMediaInfo.url}#t=0.001`} onClick={() => setShowThumbnail(false)} /> thumbnailComponent = <img src={thumbnail} alt='' onClick={() => setShowThumbnail(false)} />;
); } else if (type === 'iframe') {
} else if (commentMediaInfo?.type === 'webpage') {
thumbnailComponent = <img src={commentMediaInfo.thumbnail} alt='' onClick={() => setShowThumbnail(false)} />;
} else if (commentMediaInfo?.type === 'iframe') {
thumbnailComponent = iframeThumbnail ? <img src={iframeThumbnail} alt='' onClick={() => setShowThumbnail(false)} /> : null; thumbnailComponent = iframeThumbnail ? <img src={iframeThumbnail} alt='' onClick={() => setShowThumbnail(false)} /> : null;
} else if (commentMediaInfo?.type === 'gif' && gifFrameUrl) { } else if (type === 'gif' && gifFrameUrl) {
thumbnailComponent = <img src={gifFrameUrl} alt='' onClick={() => setShowThumbnail(false)} />; thumbnailComponent = <img src={gifFrameUrl} alt='' onClick={() => setShowThumbnail(false)} />;
} else if (commentMediaInfo?.type === 'audio') { } else if (type === 'audio') {
thumbnailComponent = <audio src={commentMediaInfo.url} controls />; thumbnailComponent = <audio src={url} controls />;
} }
const thumbnailSmallPadding = isMobile ? styles.thumbnailMobile : styles.thumbnailReplyDesktop; const thumbnailSmallPadding = isMobile ? styles.thumbnailMobile : styles.thumbnailReplyDesktop;
@@ -92,30 +92,31 @@ const Thumbnail = ({ commentMediaInfo, isMobile, isOutOfFeed, isReply, linkHeigh
const Media = ({ commentMediaInfo, isMobile, isReply, setShowThumbnail }: MediaProps) => { const Media = ({ commentMediaInfo, isMobile, isReply, setShowThumbnail }: MediaProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { thumbnail, type, url } = commentMediaInfo || {};
const mediaClass = isMobile ? styles.mediaMobile : isReply ? styles.mediaDesktopReply : styles.mediaDesktopOp; const mediaClass = isMobile ? styles.mediaMobile : isReply ? styles.mediaDesktopReply : styles.mediaDesktopOp;
return ( return (
<span className={mediaClass}> <span className={mediaClass}>
{commentMediaInfo?.type === 'iframe' ? ( {type === 'iframe' && url ? (
<Embed url={commentMediaInfo.url} /> <Embed url={url} />
) : commentMediaInfo?.type === 'gif' ? ( ) : type === 'gif' ? (
<img src={commentMediaInfo.url} alt='' onClick={() => setShowThumbnail(true)} /> <img src={url} alt='' onClick={() => setShowThumbnail(true)} />
) : commentMediaInfo?.type === 'video' ? ( ) : type === 'video' ? (
<video src={commentMediaInfo.url} controls autoPlay loop muted /> <video src={url} controls autoPlay loop muted />
) : commentMediaInfo?.type === 'image' ? ( ) : type === 'image' ? (
<img src={commentMediaInfo.url} alt='' onClick={() => setShowThumbnail(true)} /> <img src={url} alt='' onClick={() => setShowThumbnail(true)} />
) : commentMediaInfo?.type === 'webpage' ? ( ) : type === 'webpage' ? (
<img src={commentMediaInfo.thumbnail} alt='' onClick={() => setShowThumbnail(true)} /> <img src={thumbnail} alt='' onClick={() => setShowThumbnail(true)} />
) : null} ) : null}
{isMobile && commentMediaInfo?.type && ( {isMobile && type && (
<div className={styles.fileInfo}> <div className={styles.fileInfo}>
<a href={commentMediaInfo.url} target='_blank' rel='noopener noreferrer'> <a href={url} target='_blank' rel='noopener noreferrer'>
{commentMediaInfo.url.length > 30 ? commentMediaInfo?.url.slice(0, 30) + '...' : commentMediaInfo?.url} {url && url.length > 30 ? url.slice(0, 30) + '...' : url}
</a>{' '} </a>{' '}
({commentMediaInfo?.type}) ({type})
</div> </div>
)} )}
{isMobile && (commentMediaInfo?.type === 'iframe' || commentMediaInfo?.type === 'video' || commentMediaInfo?.type === 'audio') && ( {isMobile && (type === 'iframe' || type === 'video' || type === 'audio') && (
<div className={styles.closeButton}> <div className={styles.closeButton}>
<span className='button' onClick={() => setShowThumbnail(true)}> <span className='button' onClick={() => setShowThumbnail(true)}>
{t('close')} {t('close')}
+5
View File
@@ -24,3 +24,8 @@ export const isPostPageView = (pathname: string, params: ParamsType): boolean =>
const decodedPathname = decodeURIComponent(pathname); const decodedPathname = decodeURIComponent(pathname);
return params.subplebbitAddress && params.commentCid ? decodedPathname.startsWith(`/p/${params.subplebbitAddress}/c/${params.commentCid}`) : false; return params.subplebbitAddress && params.commentCid ? decodedPathname.startsWith(`/p/${params.subplebbitAddress}/c/${params.commentCid}`) : false;
}; };
export const isCatalogView = (pathname: string, params: ParamsType): boolean => {
const decodedPathname = decodeURIComponent(pathname);
return params.subplebbitAddress ? decodedPathname.startsWith(`/p/${params.subplebbitAddress}/catalog`) : false;
};
+4
View File
@@ -2,6 +2,10 @@
padding-top: 10px; padding-top: 10px;
} }
.catalog {
padding-top: 20px;
}
@media (max-width: 640px) { @media (max-width: 640px) {
.footer { .footer {
padding-left: 5px; padding-left: 5px;
+19 -16
View File
@@ -118,7 +118,7 @@ const Catalog = () => {
}); });
window.addEventListener('scroll', setLastVirtuosoState); window.addEventListener('scroll', setLastVirtuosoState);
return () => window.removeEventListener('scroll', setLastVirtuosoState); return () => window.removeEventListener('scroll', setLastVirtuosoState);
}, []); }, [subplebbitAddress]);
const lastVirtuosoState = lastVirtuosoStates?.[subplebbitAddress + 'catalog']; const lastVirtuosoState = lastVirtuosoStates?.[subplebbitAddress + 'catalog'];
@@ -128,21 +128,24 @@ const Catalog = () => {
}, [title, shortAddress]); }, [title, shortAddress]);
return ( return (
<div> <div className={styles.content}>
<Virtuoso <hr />
increaseViewportBy={{ bottom: 1200, top: 1200 }} <div className={styles.catalog}>
totalCount={rows?.length || 0} <Virtuoso
data={rows} increaseViewportBy={{ bottom: 1200, top: 1200 }}
itemContent={(index, row) => ( totalCount={rows?.length || 0}
<CatalogRow description={index === 0 && includeDescription && descriptionPost} index={index} row={row} rules={index === 0 && includeRules && rulesPost} /> data={rows}
)} itemContent={(index, row) => (
useWindowScroll={true} <CatalogRow description={index === 0 && includeDescription && descriptionPost} index={index} row={row} rules={index === 0 && includeRules && rulesPost} />
components={{ Footer }} )}
endReached={loadMore} useWindowScroll={true}
ref={virtuosoRef} components={{ Footer }}
restoreStateFrom={lastVirtuosoState} endReached={loadMore}
initialScrollTop={lastVirtuosoState?.scrollTop} ref={virtuosoRef}
/> restoreStateFrom={lastVirtuosoState}
initialScrollTop={lastVirtuosoState?.scrollTop}
/>
</div>
</div> </div>
); );
}; };