mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
feat(catalog): add media, styling
This commit is contained in:
@@ -2,10 +2,11 @@ import { useTranslation } from 'react-i18next';
|
||||
import { Link, useLocation, useParams } from 'react-router-dom';
|
||||
import { useSubscribe } from '@plebbit/plebbit-react-hooks';
|
||||
import styles from './board-buttons.module.css';
|
||||
import { isPostPageView } from '../../lib/utils/view-utils';
|
||||
import { isCatalogView, isPostPageView } from '../../lib/utils/view-utils';
|
||||
|
||||
interface BoardButtonsProps {
|
||||
address: string | undefined;
|
||||
isInCatalogView?: boolean;
|
||||
}
|
||||
|
||||
const OptionsButton = () => {
|
||||
@@ -13,11 +14,11 @@ const OptionsButton = () => {
|
||||
return <button className='button'>{t('options')}</button>;
|
||||
};
|
||||
|
||||
const CatalogButton = ({ address }: BoardButtonsProps) => {
|
||||
const CatalogButton = ({ address, isInCatalogView }: BoardButtonsProps) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
};
|
||||
@@ -79,20 +80,25 @@ export const MobileBoardButtons = () => {
|
||||
|
||||
export const DesktopBoardButtons = () => {
|
||||
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 (
|
||||
<div className={styles.desktopBoardButtons}>
|
||||
<hr />
|
||||
{isInPostPage ? (
|
||||
<>
|
||||
[<ReturnButton address={address} />] [<CatalogButton address={address} />] [<BottomButton />] [<OptionsButton />]
|
||||
[<ReturnButton address={address} />] [<CatalogButton address={address} isInCatalogView={isInCatalogView} />] [<BottomButton />] [<OptionsButton />]
|
||||
<span className={styles.subscribeButton}>
|
||||
[<SubscribeButton address={address} />]
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
[<OptionsButton />] [<CatalogButton address={address} />]
|
||||
[<OptionsButton />] [<CatalogButton address={address} isInCatalogView={isInCatalogView} />]
|
||||
<span className={styles.subscribeButton}>
|
||||
[<SubscribeButton address={address} />]
|
||||
</span>
|
||||
|
||||
@@ -1,39 +1,59 @@
|
||||
.row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.post {
|
||||
width: 180px;
|
||||
font-size: 12px;
|
||||
|
||||
/* fix text overflowing */
|
||||
position: relative;
|
||||
max-height: 320px;
|
||||
padding: 10px 0 3px 0;
|
||||
vertical-align: top;
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.postHeader {
|
||||
/* make cursors like button */
|
||||
cursor: pointer;
|
||||
.mediaPaddingWrapper {
|
||||
padding-bottom: 5px;
|
||||
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 {
|
||||
overflow: hidden;
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
background-color: var(--media-thumbnail-background-color);
|
||||
width: var(--width);
|
||||
height: var(--height);
|
||||
max-width: 150px;
|
||||
max-height: 150px;
|
||||
}
|
||||
|
||||
/* add some padding to rows when there's no image */
|
||||
.noMedia {
|
||||
height: 2px;
|
||||
.post img, .post video, .post audio {
|
||||
box-shadow: 0 0 5px rgba(0, 0, 0, 0.25);
|
||||
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;
|
||||
}
|
||||
@@ -1,10 +1,88 @@
|
||||
import styles from './catalog-row.module.css';
|
||||
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 { 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 {
|
||||
@@ -16,9 +94,7 @@ interface 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 posts = rowPosts.map((post, index) => <CatalogPost key={index} post={post} />);
|
||||
|
||||
return <div className={styles.row}>{posts}</div>;
|
||||
};
|
||||
|
||||
|
||||
@@ -36,8 +36,11 @@ const ThumbnailSmall = ({ style, children, thumbnailSmallPadding }: ThumbnailPro
|
||||
);
|
||||
|
||||
const Thumbnail = ({ commentMediaInfo, isMobile, isOutOfFeed, isReply, linkHeight, linkWidth, setShowThumbnail }: MediaProps) => {
|
||||
const { patternThumbnailUrl, thumbnail, type, url } = commentMediaInfo || {};
|
||||
|
||||
let displayWidth, displayHeight;
|
||||
const maxThumbnailSize = isMobile || isReply ? 125 : 250;
|
||||
|
||||
if (linkWidth && linkHeight) {
|
||||
let scale = Math.min(1, maxThumbnailSize / Math.max(linkWidth, linkHeight));
|
||||
displayWidth = `${linkWidth * scale}px`;
|
||||
@@ -47,7 +50,7 @@ const Thumbnail = ({ commentMediaInfo, isMobile, isOutOfFeed, isReply, linkHeigh
|
||||
displayHeight = `${maxThumbnailSize}px`;
|
||||
}
|
||||
|
||||
if (commentMediaInfo?.type === 'audio') {
|
||||
if (type === 'audio') {
|
||||
displayWidth = '250px';
|
||||
displayHeight = '75px';
|
||||
}
|
||||
@@ -58,24 +61,21 @@ const Thumbnail = ({ commentMediaInfo, isMobile, isOutOfFeed, isReply, linkHeigh
|
||||
}
|
||||
|
||||
let thumbnailComponent: React.ReactNode = null;
|
||||
const iframeThumbnail = commentMediaInfo?.patternThumbnailUrl || commentMediaInfo?.thumbnail;
|
||||
const gifFrameUrl = useFetchGifFirstFrame(commentMediaInfo?.type === 'gif' ? commentMediaInfo.url : undefined);
|
||||
if (commentMediaInfo?.type === 'image') {
|
||||
thumbnailComponent = <img src={commentMediaInfo.url} alt='' onClick={() => setShowThumbnail(false)} />;
|
||||
} else if (commentMediaInfo?.type === 'video') {
|
||||
thumbnailComponent = commentMediaInfo.thumbnail ? (
|
||||
<img src={commentMediaInfo.thumbnail} alt='' />
|
||||
) : (
|
||||
<video src={`${commentMediaInfo.url}#t=0.001`} onClick={() => setShowThumbnail(false)} />
|
||||
);
|
||||
} else if (commentMediaInfo?.type === 'webpage') {
|
||||
thumbnailComponent = <img src={commentMediaInfo.thumbnail} alt='' onClick={() => setShowThumbnail(false)} />;
|
||||
} else if (commentMediaInfo?.type === 'iframe') {
|
||||
const iframeThumbnail = patternThumbnailUrl || thumbnail;
|
||||
const gifFrameUrl = useFetchGifFirstFrame(type === 'gif' ? url : undefined);
|
||||
|
||||
if (type === 'image') {
|
||||
thumbnailComponent = <img src={url} alt='' onClick={() => setShowThumbnail(false)} />;
|
||||
} else if (type === 'video') {
|
||||
thumbnailComponent = thumbnail ? <img src={thumbnail} alt='' /> : <video src={`${url}#t=0.001`} onClick={() => setShowThumbnail(false)} />;
|
||||
} else if (type === 'webpage') {
|
||||
thumbnailComponent = <img src={thumbnail} alt='' onClick={() => setShowThumbnail(false)} />;
|
||||
} else if (type === 'iframe') {
|
||||
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)} />;
|
||||
} else if (commentMediaInfo?.type === 'audio') {
|
||||
thumbnailComponent = <audio src={commentMediaInfo.url} controls />;
|
||||
} else if (type === 'audio') {
|
||||
thumbnailComponent = <audio src={url} controls />;
|
||||
}
|
||||
|
||||
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 { t } = useTranslation();
|
||||
const { thumbnail, type, url } = commentMediaInfo || {};
|
||||
const mediaClass = isMobile ? styles.mediaMobile : isReply ? styles.mediaDesktopReply : styles.mediaDesktopOp;
|
||||
|
||||
return (
|
||||
<span className={mediaClass}>
|
||||
{commentMediaInfo?.type === 'iframe' ? (
|
||||
<Embed url={commentMediaInfo.url} />
|
||||
) : commentMediaInfo?.type === 'gif' ? (
|
||||
<img src={commentMediaInfo.url} alt='' onClick={() => setShowThumbnail(true)} />
|
||||
) : commentMediaInfo?.type === 'video' ? (
|
||||
<video src={commentMediaInfo.url} controls autoPlay loop muted />
|
||||
) : commentMediaInfo?.type === 'image' ? (
|
||||
<img src={commentMediaInfo.url} alt='' onClick={() => setShowThumbnail(true)} />
|
||||
) : commentMediaInfo?.type === 'webpage' ? (
|
||||
<img src={commentMediaInfo.thumbnail} alt='' onClick={() => setShowThumbnail(true)} />
|
||||
{type === 'iframe' && url ? (
|
||||
<Embed url={url} />
|
||||
) : type === 'gif' ? (
|
||||
<img src={url} alt='' onClick={() => setShowThumbnail(true)} />
|
||||
) : type === 'video' ? (
|
||||
<video src={url} controls autoPlay loop muted />
|
||||
) : type === 'image' ? (
|
||||
<img src={url} alt='' onClick={() => setShowThumbnail(true)} />
|
||||
) : type === 'webpage' ? (
|
||||
<img src={thumbnail} alt='' onClick={() => setShowThumbnail(true)} />
|
||||
) : null}
|
||||
{isMobile && commentMediaInfo?.type && (
|
||||
{isMobile && type && (
|
||||
<div className={styles.fileInfo}>
|
||||
<a href={commentMediaInfo.url} target='_blank' rel='noopener noreferrer'>
|
||||
{commentMediaInfo.url.length > 30 ? commentMediaInfo?.url.slice(0, 30) + '...' : commentMediaInfo?.url}
|
||||
<a href={url} target='_blank' rel='noopener noreferrer'>
|
||||
{url && url.length > 30 ? url.slice(0, 30) + '...' : url}
|
||||
</a>{' '}
|
||||
({commentMediaInfo?.type})
|
||||
({type})
|
||||
</div>
|
||||
)}
|
||||
{isMobile && (commentMediaInfo?.type === 'iframe' || commentMediaInfo?.type === 'video' || commentMediaInfo?.type === 'audio') && (
|
||||
{isMobile && (type === 'iframe' || type === 'video' || type === 'audio') && (
|
||||
<div className={styles.closeButton}>
|
||||
<span className='button' onClick={() => setShowThumbnail(true)}>
|
||||
{t('close')}
|
||||
|
||||
@@ -24,3 +24,8 @@ export const isPostPageView = (pathname: string, params: ParamsType): boolean =>
|
||||
const decodedPathname = decodeURIComponent(pathname);
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.catalog {
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.footer {
|
||||
padding-left: 5px;
|
||||
|
||||
@@ -118,7 +118,7 @@ const Catalog = () => {
|
||||
});
|
||||
window.addEventListener('scroll', setLastVirtuosoState);
|
||||
return () => window.removeEventListener('scroll', setLastVirtuosoState);
|
||||
}, []);
|
||||
}, [subplebbitAddress]);
|
||||
|
||||
const lastVirtuosoState = lastVirtuosoStates?.[subplebbitAddress + 'catalog'];
|
||||
|
||||
@@ -128,21 +128,24 @@ const Catalog = () => {
|
||||
}, [title, shortAddress]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Virtuoso
|
||||
increaseViewportBy={{ bottom: 1200, top: 1200 }}
|
||||
totalCount={rows?.length || 0}
|
||||
data={rows}
|
||||
itemContent={(index, row) => (
|
||||
<CatalogRow description={index === 0 && includeDescription && descriptionPost} index={index} row={row} rules={index === 0 && includeRules && rulesPost} />
|
||||
)}
|
||||
useWindowScroll={true}
|
||||
components={{ Footer }}
|
||||
endReached={loadMore}
|
||||
ref={virtuosoRef}
|
||||
restoreStateFrom={lastVirtuosoState}
|
||||
initialScrollTop={lastVirtuosoState?.scrollTop}
|
||||
/>
|
||||
<div className={styles.content}>
|
||||
<hr />
|
||||
<div className={styles.catalog}>
|
||||
<Virtuoso
|
||||
increaseViewportBy={{ bottom: 1200, top: 1200 }}
|
||||
totalCount={rows?.length || 0}
|
||||
data={rows}
|
||||
itemContent={(index, row) => (
|
||||
<CatalogRow description={index === 0 && includeDescription && descriptionPost} index={index} row={row} rules={index === 0 && includeRules && rulesPost} />
|
||||
)}
|
||||
useWindowScroll={true}
|
||||
components={{ Footer }}
|
||||
endReached={loadMore}
|
||||
ref={virtuosoRef}
|
||||
restoreStateFrom={lastVirtuosoState}
|
||||
initialScrollTop={lastVirtuosoState?.scrollTop}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user