From f124d033660a675cb9e8e134ab7b0d6dd71f771c Mon Sep 17 00:00:00 2001 From: "plebeius.eth" Date: Fri, 12 Apr 2024 14:28:06 +0200 Subject: [PATCH] add catalog view --- src/app.tsx | 2 + .../catalog-row/catalog-row.module.css | 39 +++++ src/components/catalog-row/catalog-row.tsx | 25 +++ src/components/catalog-row/index.ts | 1 + .../subplebbit-description.tsx | 3 +- .../subplebbit-rules/subplebbit-rules.tsx | 2 +- src/views/catalog/catalog.module.css | 9 ++ src/views/catalog/catalog.tsx | 150 ++++++++++++++++++ src/views/catalog/index.ts | 1 + 9 files changed, 230 insertions(+), 2 deletions(-) create mode 100644 src/components/catalog-row/catalog-row.module.css create mode 100644 src/components/catalog-row/catalog-row.tsx create mode 100644 src/components/catalog-row/index.ts create mode 100644 src/views/catalog/catalog.module.css create mode 100644 src/views/catalog/catalog.tsx create mode 100644 src/views/catalog/index.ts diff --git a/src/app.tsx b/src/app.tsx index 3898dd5d..5765078c 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -5,6 +5,7 @@ import { useSubplebbits } from '@plebbit/plebbit-react-hooks'; import { useDefaultSubplebbitAddresses } from './hooks/use-default-subplebbits'; import useTheme from './hooks/use-theme'; import styles from './app.module.css'; +import Catalog from './views/catalog'; import Home from './views/home'; import PostPage from './views/post-page'; import Settings from './views/settings'; @@ -58,6 +59,7 @@ const App = () => { } /> } /> } /> + } /> } /> diff --git a/src/components/catalog-row/catalog-row.module.css b/src/components/catalog-row/catalog-row.module.css new file mode 100644 index 00000000..cd9173b7 --- /dev/null +++ b/src/components/catalog-row/catalog-row.module.css @@ -0,0 +1,39 @@ +.row { + display: flex; + flex-direction: row; + justify-content: space-around; + align-items: center; +} + +.post { + width: 180px; + font-size: 12px; + + /* fix text overflowing */ + overflow: hidden; +} + +.postHeader { + /* make cursors like button */ + cursor: pointer; +} + +.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; +} + +/* add some padding to rows when there's no image */ +.noMedia { + height: 2px; +} diff --git a/src/components/catalog-row/catalog-row.tsx b/src/components/catalog-row/catalog-row.tsx new file mode 100644 index 00000000..ddcbc6a2 --- /dev/null +++ b/src/components/catalog-row/catalog-row.tsx @@ -0,0 +1,25 @@ +import styles from './catalog-row.module.css'; +import { Comment } from '@plebbit/plebbit-react-hooks'; + +const CatalogPost = ({ post }: { post: Comment }) => { + const { title, cid } = post || {}; + + return
{title || 'this is a post without a title'}
; +}; + +interface CatalogRowProps { + description: any | undefined; + index: number; + rules: any | undefined; + row: Comment[]; +} + +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) => ); + + return
{posts}
; +}; + +export default CatalogRow; diff --git a/src/components/catalog-row/index.ts b/src/components/catalog-row/index.ts new file mode 100644 index 00000000..d481e3d2 --- /dev/null +++ b/src/components/catalog-row/index.ts @@ -0,0 +1 @@ +export { default } from './catalog-row'; diff --git a/src/components/subplebbit-description/subplebbit-description.tsx b/src/components/subplebbit-description/subplebbit-description.tsx index 7e5ee979..e4ed8644 100644 --- a/src/components/subplebbit-description/subplebbit-description.tsx +++ b/src/components/subplebbit-description/subplebbit-description.tsx @@ -1,4 +1,5 @@ import Post from '../post'; + interface DescriptionPostProps { subplebbitAddress: string | undefined; createdAt: number; @@ -7,7 +8,7 @@ interface DescriptionPostProps { title: string; } -export const SubplebbitDescription = ({ subplebbitAddress, createdAt, description, avatarUrl, title }: DescriptionPostProps) => { +const SubplebbitDescription = ({ subplebbitAddress, createdAt, description, avatarUrl, title }: DescriptionPostProps) => { const post = { isDescription: true, subplebbitAddress, diff --git a/src/components/subplebbit-rules/subplebbit-rules.tsx b/src/components/subplebbit-rules/subplebbit-rules.tsx index 78f9edb6..ccf3dffb 100644 --- a/src/components/subplebbit-rules/subplebbit-rules.tsx +++ b/src/components/subplebbit-rules/subplebbit-rules.tsx @@ -6,7 +6,7 @@ interface RulesPostProps { rules: string[]; } -export const SubplebbitRules = ({ subplebbitAddress, createdAt, rules }: RulesPostProps) => { +const SubplebbitRules = ({ subplebbitAddress, createdAt, rules }: RulesPostProps) => { const content = rules.map((rule, index) => `${index + 1}. ${rule}`).join('\n'); const post = { isRules: true, diff --git a/src/views/catalog/catalog.module.css b/src/views/catalog/catalog.module.css new file mode 100644 index 00000000..7ddc39cc --- /dev/null +++ b/src/views/catalog/catalog.module.css @@ -0,0 +1,9 @@ +.footer { + padding-top: 10px; +} + +@media (max-width: 640px) { + .footer { + padding-left: 5px; + } +} \ No newline at end of file diff --git a/src/views/catalog/catalog.tsx b/src/views/catalog/catalog.tsx new file mode 100644 index 00000000..f527afa6 --- /dev/null +++ b/src/views/catalog/catalog.tsx @@ -0,0 +1,150 @@ +import { useEffect, useMemo, useRef } from 'react'; +import { useParams } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import { useFeed, useSubplebbit } from '@plebbit/plebbit-react-hooks'; +import { Virtuoso, VirtuosoHandle, StateSnapshot } from 'react-virtuoso'; +import useFeedStateString from '../../hooks/use-feed-state-string'; +import useWindowWidth from '../../hooks/use-window-width'; +import CatalogRow from '../../components/catalog-row'; +import LoadingEllipsis from '../../components/loading-ellipsis'; +import styles from './catalog.module.css'; + +const lastVirtuosoStates: { [key: string]: StateSnapshot } = {}; + +const useFeedRows = (feed: any[], columnCount: number, includeDescription: boolean, includeRules: boolean) => { + const rowsRef = useRef([]); + return useMemo(() => { + const rows: any[] = []; + let startIndex = 0; + let adjustment = 0; // Adjustment for the first row if description/rules are included + + // Calculate adjustment for the first row + if (includeDescription) adjustment += 1; + if (includeRules) adjustment += 1; + + while (startIndex < feed.length) { + let currentColumnCount = columnCount; + + // Adjust the number of items in the first row to compensate for description/rules + if (rows.length === 0 && adjustment > 0) { + currentColumnCount = columnCount - adjustment; + } + + // Ensure we don't try to slice more items than are available + const endIndex = Math.min(startIndex + currentColumnCount, feed.length); + + if (rowsRef.current[rows.length] && rowsRef.current[rows.length].length === currentColumnCount) { + rows.push(rowsRef.current[rows.length]); + } else { + rows.push(feed.slice(startIndex, endIndex)); + } + + startIndex = endIndex; // Update startIndex to the end of the current row + } + + rowsRef.current = rows; + return rows; + }, [feed, columnCount, includeDescription, includeRules]); +}; + +const columnWidth = 180; + +const Catalog = () => { + const { t } = useTranslation(); + const { subplebbitAddress } = useParams<{ subplebbitAddress: string }>(); + const subplebbitAddresses = useMemo(() => [subplebbitAddress], [subplebbitAddress]) as string[]; + + const columnCount = Math.floor(useWindowWidth() / columnWidth); + // postPerPage based on columnCount for optimized feed, dont change value after first render + // eslint-disable-next-line + const postsPerPage = useMemo(() => (columnCount <= 2 ? 10 : columnCount === 3 ? 15 : columnCount === 4 ? 20 : 25), []); + + const { feed, hasMore, loadMore } = useFeed({ subplebbitAddresses, sortType: 'active', postsPerPage }); + + const subplebbit = useSubplebbit({ subplebbitAddress }); + const { createdAt, description, rules, shortAddress, state, suggested, title } = subplebbit || {}; + const { avatarUrl } = suggested || {}; + const loadingStateString = useFeedStateString(subplebbitAddresses) || t('loading'); + const loadingString =
{state === 'failed' ? state : }
; + + const Footer = () => { + let footerContent; + if (feed.length === 0) { + footerContent = t('no_posts'); + } + if (hasMore || subplebbitAddresses.length === 0) { + footerContent = loadingString; + } + return
{footerContent}
; + }; + + // split feed into rows + const includeDescription = !!description && description.length > 0; + const includeRules = !!rules && rules.length > 0; + const rows = useFeedRows(feed, columnCount, includeDescription, includeRules); + + const descriptionPost = { + isDescription: true, + subplebbitAddress, + timestamp: createdAt, + author: { displayName: '## Board Mods' }, + content: description, + link: avatarUrl, + title: 'Welcome to ' + (title || `p/${shortAddress}`), + pinned: true, + locked: true, + }; + + const content = rules?.map((rule: string, index: number) => `${index + 1}. ${rule}`).join('\n'); + const rulesPost = { + isRules: true, + subplebbitAddress, + timestamp: createdAt, + author: { displayName: '## Board Mods' }, + content, + title: 'Rules', + pinned: true, + locked: true, + }; + + // save the last Virtuoso state to restore it when navigating back + const virtuosoRef = useRef(null); + useEffect(() => { + const setLastVirtuosoState = () => + virtuosoRef.current?.getState((snapshot: StateSnapshot) => { + if (snapshot?.ranges?.length) { + lastVirtuosoStates[subplebbitAddress + 'catalog'] = snapshot; + } + }); + window.addEventListener('scroll', setLastVirtuosoState); + return () => window.removeEventListener('scroll', setLastVirtuosoState); + }, []); + + const lastVirtuosoState = lastVirtuosoStates?.[subplebbitAddress + 'catalog']; + + useEffect(() => { + let documentTitle = title ? title : shortAddress; + document.title = documentTitle + ' - Catalog'; + }, [title, shortAddress]); + + return ( +
+ ( + + )} + useWindowScroll={true} + components={{ Footer }} + endReached={loadMore} + ref={virtuosoRef} + restoreStateFrom={lastVirtuosoState} + initialScrollTop={lastVirtuosoState?.scrollTop} + /> +
+ ); +}; + +export default Catalog; diff --git a/src/views/catalog/index.ts b/src/views/catalog/index.ts new file mode 100644 index 00000000..35a07304 --- /dev/null +++ b/src/views/catalog/index.ts @@ -0,0 +1 @@ +export { default } from './catalog';