update board buttons UI, add catalog search UI

This commit is contained in:
Tom (plebeius.eth)
2025-03-06 15:44:26 +01:00
parent 23f70c96b8
commit cf183393a4
12 changed files with 213 additions and 38 deletions
+2 -1
View File
@@ -55,8 +55,9 @@ const BoardLayout = () => {
{isMobile {isMobile
? (subplebbitAddress || isInAllView || isInSubscriptionsView || pendingPost?.subplebbitAddress) && ( ? (subplebbitAddress || isInAllView || isInSubscriptionsView || pendingPost?.subplebbitAddress) && (
<> <>
<MobileBoardButtons />
<PostForm key={key} /> <PostForm key={key} />
<hr />
<MobileBoardButtons />
</> </>
) )
: (subplebbitAddress || isInAllView || isInSubscriptionsView || pendingPost?.subplebbitAddress) && ( : (subplebbitAddress || isInAllView || isInSubscriptionsView || pendingPost?.subplebbitAddress) && (
@@ -51,7 +51,6 @@
} }
.desktopBoardButtons { .desktopBoardButtons {
padding-right: 5px;
text-transform: capitalize; text-transform: capitalize;
} }
+13 -13
View File
@@ -4,17 +4,18 @@ import { useAccountComment, useSubscribe } from '@plebbit/plebbit-react-hooks';
import useSubplebbitsStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits'; import useSubplebbitsStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits';
import useSubplebbitsPagesStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits-pages'; import useSubplebbitsPagesStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits-pages';
import { isAllView, isCatalogView, isDescriptionView, isPendingPostView, isPostPageView, isSubscriptionsView } from '../../lib/utils/view-utils'; import { isAllView, isCatalogView, isDescriptionView, isPendingPostView, isPostPageView, isSubscriptionsView } from '../../lib/utils/view-utils';
import useCatalogFiltersStore from '../../stores/use-catalog-filters-store';
import useCatalogStyleStore from '../../stores/use-catalog-style-store'; import useCatalogStyleStore from '../../stores/use-catalog-style-store';
import useFeedResetStore from '../../stores/use-feed-reset-store'; import useFeedResetStore from '../../stores/use-feed-reset-store';
import useSortingStore from '../../stores/use-sorting-store'; import useSortingStore from '../../stores/use-sorting-store';
import useTimeFilter from '../../hooks/use-time-filter';
import CatalogFilters from '../../views/catalog/catalog-filters/';
import styles from './board-buttons.module.css';
import Tooltip from '../tooltip';
import useCountLinksInReplies from '../../hooks/use-count-links-in-replies'; import useCountLinksInReplies from '../../hooks/use-count-links-in-replies';
import _ from 'lodash';
import useIsMobile from '../../hooks/use-is-mobile'; import useIsMobile from '../../hooks/use-is-mobile';
import useCatalogFiltersStore from '../../stores/use-catalog-filters-store'; import useTimeFilter from '../../hooks/use-time-filter';
import CatalogFilters from '../catalog-filters';
import CatalogSearch from '../catalog-search';
import Tooltip from '../tooltip';
import styles from './board-buttons.module.css';
import _ from 'lodash';
interface BoardButtonsProps { interface BoardButtonsProps {
address?: string | undefined; address?: string | undefined;
@@ -282,7 +283,7 @@ export const MobileBoardButtons = () => {
<SortOptions /> <ImageSizeOptions /> <SortOptions /> <ImageSizeOptions />
</div> </div>
<div className={styles.mobileCatalogOptionsPadding}> <div className={styles.mobileCatalogOptionsPadding}>
<ShowOPCommentOption /> <CatalogFilters /> <ShowOPCommentOption /> <CatalogFilters /> <CatalogSearch />
</div> </div>
</div> </div>
</> </>
@@ -380,17 +381,16 @@ export const DesktopBoardButtons = () => {
{(isInAllView || isInSubscriptionsView) && ( {(isInAllView || isInSubscriptionsView) && (
<TimeFilter isInAllView={isInAllView} isInCatalogView={isInCatalogView} isInSubscriptionsView={isInSubscriptionsView} /> <TimeFilter isInAllView={isInAllView} isInCatalogView={isInCatalogView} isInSubscriptionsView={isInSubscriptionsView} />
)} )}
{isInCatalogView && (
<>
[
<CatalogFilters />]
</>
)}{' '}
{!(isInAllView || isInSubscriptionsView) && ( {!(isInAllView || isInSubscriptionsView) && (
<> <>
[ [
<SubscribeButton address={subplebbitAddress} />] <SubscribeButton address={subplebbitAddress} />]
</> </>
)}{' '}
{isInCatalogView && (
<>
[<CatalogFilters />] <CatalogSearch />
</>
)} )}
</span> </span>
</> </>
@@ -1,6 +1,6 @@
import { useState, useCallback, useRef, useEffect } from 'react'; import { useState, useCallback, useRef, useEffect } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import useCatalogFiltersStore from '../../../stores/use-catalog-filters-store'; import useCatalogFiltersStore from '../../stores/use-catalog-filters-store';
import styles from './catalog-filters.module.css'; import styles from './catalog-filters.module.css';
const FiltersTable = ({ onSave }: { onSave: () => void }) => { const FiltersTable = ({ onSave }: { onSave: () => void }) => {
@@ -41,6 +41,8 @@ const FiltersTable = ({ onSave }: { onSave: () => void }) => {
enabled: true, enabled: true,
count: 0, count: 0,
filteredCids: new Set<string>(), filteredCids: new Set<string>(),
subplebbitCounts: new Map<string, number>(),
subplebbitFilteredCids: new Map<string, Set<string>>(),
hide: true, hide: true,
top: false, top: false,
}, },
@@ -0,0 +1,49 @@
.button {
text-transform: capitalize;
color: var(--button-desktop-text-color);
text-decoration: var(--button-text-decoration);
}
.button:hover {
color: var(--button-desktop-text-color-hover);
cursor: pointer;
}
.filtersButton {
text-transform: capitalize;
}
.searchContainer {
display: inline;
}
.searchContainer input {
width: 75px;
margin: 0px 5px;
padding: 1px;
outline: none;
border: 1px solid #AAA;
font-size: 11px;
}
.searchContainer input:focus {
border: var(--post-form-field-input-focus-border);
}
.closeSearch {
cursor: pointer;
border: none;
white-space: nowrap;
color: var(--button-desktop-text-color);
}
.closeSearch:hover {
color: var(--button-desktop-text-color-hover);
}
@media (max-width: 640px) {
.searchContainer input {
width: 75px;
font-size: 16px;
}
}
@@ -0,0 +1,45 @@
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import styles from './catalog-search.module.css';
import useIsMobile from '../../hooks/use-is-mobile';
const CatalogSearch = () => {
const { t } = useTranslation();
const [openSearch, setOpenSearch] = useState(false);
useEffect(() => {
if (openSearch) {
const input = document.querySelector('input');
input?.focus();
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
setOpenSearch(false);
}
};
window.addEventListener('keydown', handleKeyDown);
}
}, [openSearch]);
const isMobile = useIsMobile();
return (
<>
{!isMobile && '['}
<span className={`${styles.filtersButton} button`} onClick={() => setOpenSearch(!openSearch)}>
{t('search')}
</span>
{!isMobile && ']'}
{openSearch && (
<div className={styles.searchContainer}>
<input type='text' />
<span className={styles.closeSearch} onClick={() => setOpenSearch(false)}>
</span>
</div>
)}
</>
);
};
export default CatalogSearch;
+1
View File
@@ -0,0 +1 @@
export { default } from './catalog-search';
@@ -15,8 +15,7 @@
.postFormMobile { .postFormMobile {
text-align: center; text-align: center;
margin-top: -2px; margin-top: -3px;
padding-bottom: 7px;
} }
.closed { .closed {
+66 -8
View File
@@ -7,6 +7,8 @@ interface FilterItem {
enabled: boolean; enabled: boolean;
count: number; count: number;
filteredCids: Set<string>; filteredCids: Set<string>;
subplebbitCounts: Map<string, number>;
subplebbitFilteredCids: Map<string, Set<string>>;
hide: boolean; hide: boolean;
top: boolean; top: boolean;
} }
@@ -24,8 +26,11 @@ interface CatalogFiltersStore {
initializeFilter: () => void; initializeFilter: () => void;
filteredCount: number; filteredCount: number;
filteredCids: Set<string>; filteredCids: Set<string>;
incrementFilterCount: (filterIndex: number, cid: string) => void; incrementFilterCount: (filterIndex: number, cid: string, subplebbitAddress: string) => void;
recalcFilteredCount: () => void; recalcFilteredCount: () => void;
currentSubplebbitAddress: string | null;
setCurrentSubplebbitAddress: (address: string | null) => void;
getFilteredCountForCurrentSubplebbit: () => number;
} }
const useCatalogFiltersStore = create( const useCatalogFiltersStore = create(
@@ -41,6 +46,8 @@ const useCatalogFiltersStore = create(
filterItems: [], filterItems: [],
filteredCount: 0, filteredCount: 0,
filteredCids: new Set<string>(), filteredCids: new Set<string>(),
currentSubplebbitAddress: null,
setCurrentSubplebbitAddress: (address: string | null) => set({ currentSubplebbitAddress: address }),
setFilterItems: (items: FilterItem[]) => { setFilterItems: (items: FilterItem[]) => {
const nonEmptyItems = items const nonEmptyItems = items
.filter((item) => item.text.trim() !== '') .filter((item) => item.text.trim() !== '')
@@ -48,6 +55,8 @@ const useCatalogFiltersStore = create(
...item, ...item,
count: item.count || 0, count: item.count || 0,
filteredCids: item.filteredCids || new Set(), filteredCids: item.filteredCids || new Set(),
subplebbitCounts: item.subplebbitCounts || new Map(),
subplebbitFilteredCids: item.subplebbitFilteredCids || new Map(),
hide: item.hide ?? true, hide: item.hide ?? true,
top: item.top ?? false, top: item.top ?? false,
})); }));
@@ -61,6 +70,8 @@ const useCatalogFiltersStore = create(
...item, ...item,
count: item.count || 0, count: item.count || 0,
filteredCids: item.filteredCids || new Set(), filteredCids: item.filteredCids || new Set(),
subplebbitCounts: item.subplebbitCounts || new Map(),
subplebbitFilteredCids: item.subplebbitFilteredCids || new Map(),
hide: item.hide ?? true, hide: item.hide ?? true,
top: item.top ?? false, top: item.top ?? false,
})); }));
@@ -98,30 +109,75 @@ const useCatalogFiltersStore = create(
initializeFilter: () => { initializeFilter: () => {
get().updateFilter(); get().updateFilter();
}, },
incrementFilterCount: (filterIndex: number, cid: string) => { incrementFilterCount: (filterIndex: number, cid: string, subplebbitAddress: string) => {
set((state) => { set((state) => {
const newFilterItems = [...state.filterItems]; const newFilterItems = [...state.filterItems];
if (newFilterItems[filterIndex]) { if (newFilterItems[filterIndex]) {
const item = newFilterItems[filterIndex]; const item = newFilterItems[filterIndex];
if (!item.filteredCids.has(cid)) {
const subplebbitFilteredCids = new Map(item.subplebbitFilteredCids);
if (!subplebbitFilteredCids.has(subplebbitAddress)) {
subplebbitFilteredCids.set(subplebbitAddress, new Set());
}
const cidSet = subplebbitFilteredCids.get(subplebbitAddress)!;
if (!cidSet.has(cid)) {
const newItemFilteredCids = new Set(item.filteredCids); const newItemFilteredCids = new Set(item.filteredCids);
newItemFilteredCids.add(cid); newItemFilteredCids.add(cid);
cidSet.add(cid);
subplebbitFilteredCids.set(subplebbitAddress, cidSet);
const subplebbitCounts = new Map(item.subplebbitCounts);
const currentCount = subplebbitCounts.get(subplebbitAddress) || 0;
subplebbitCounts.set(subplebbitAddress, currentCount + 1);
newFilterItems[filterIndex] = { newFilterItems[filterIndex] = {
...item, ...item,
count: item.count + 1, count: item.count + 1,
filteredCids: newItemFilteredCids, filteredCids: newItemFilteredCids,
subplebbitCounts,
subplebbitFilteredCids,
}; };
const newFilteredCount = newFilterItems.reduce((sum, filt) => (filt.hide ? sum + filt.count : sum), 0);
return { filterItems: newFilterItems, filteredCount: newFilteredCount }; return { filterItems: newFilterItems };
} }
} }
return state; return state;
}); });
get().recalcFilteredCount();
}, },
recalcFilteredCount: () => { recalcFilteredCount: () => {
set((state) => ({ set((state) => {
filteredCount: state.filterItems.reduce((sum, item) => (item.hide ? sum + item.count : sum), 0), const currentSubplebbit = state.currentSubplebbitAddress;
})); if (!currentSubplebbit) return { filteredCount: 0 };
let filteredCount = 0;
for (const item of state.filterItems) {
if (item.enabled && item.hide) {
const subCount = item.subplebbitCounts?.get(currentSubplebbit) || 0;
filteredCount += subCount;
}
}
return { filteredCount };
});
},
getFilteredCountForCurrentSubplebbit: () => {
const state = get();
const currentSubplebbit = state.currentSubplebbitAddress;
if (!currentSubplebbit) return 0;
let filteredCount = 0;
for (const item of state.filterItems) {
if (item.enabled && item.hide) {
const subCount = item.subplebbitCounts?.get(currentSubplebbit) || 0;
filteredCount += subCount;
}
}
return filteredCount;
}, },
}), }),
{ {
@@ -134,6 +190,8 @@ const useCatalogFiltersStore = create(
enabled: item.enabled, enabled: item.enabled,
hide: item.hide, hide: item.hide,
top: item.top, top: item.top,
subplebbitCounts: Array.from(item.subplebbitCounts || new Map()),
subplebbitFilteredCids: Array.from((item.subplebbitFilteredCids || new Map()).entries()).map(([key, value]) => [key, Array.from(value)]),
})), })),
} as any; } as any;
}, },
+33 -12
View File
@@ -23,7 +23,8 @@ const lastVirtuosoStates: { [key: string]: StateSnapshot } = {};
const createContentFilter = ( const createContentFilter = (
filterItems: { text: string; enabled: boolean; count: number; filteredCids: Set<string>; hide: boolean; top: boolean }[], filterItems: { text: string; enabled: boolean; count: number; filteredCids: Set<string>; hide: boolean; top: boolean }[],
onFilterMatch?: (filterIndex: number, cid: string) => void, subplebbitAddress: string,
onFilterMatch?: (filterIndex: number, cid: string, subplebbitAddress: string) => void,
) => { ) => {
// Create a unique key based on the enabled filter items // Create a unique key based on the enabled filter items
const enabledFilters = filterItems.filter((item) => item.enabled && item.text.trim() !== ''); const enabledFilters = filterItems.filter((item) => item.enabled && item.text.trim() !== '');
@@ -51,10 +52,10 @@ const createContentFilter = (
const filterIndex = filterItems.findIndex((f) => f.text === item.text && f.enabled); const filterIndex = filterItems.findIndex((f) => f.text === item.text && f.enabled);
if (filterIndex !== -1) { if (filterIndex !== -1) {
if (onFilterMatch) { if (onFilterMatch) {
onFilterMatch(filterIndex, comment.cid); onFilterMatch(filterIndex, comment.cid, subplebbitAddress);
} else { } else {
// Fallback to the store method if no callback provided // Fallback to the store method if no callback provided
useCatalogFiltersStore.getState().incrementFilterCount(filterIndex, comment.cid); useCatalogFiltersStore.getState().incrementFilterCount(filterIndex, comment.cid, subplebbitAddress);
} }
} }
@@ -91,10 +92,11 @@ const createImageFilter = (showTextOnlyThreads: boolean) => {
const createCombinedFilter = ( const createCombinedFilter = (
showTextOnlyThreads: boolean, showTextOnlyThreads: boolean,
filterItems: { text: string; enabled: boolean; count: number; filteredCids: Set<string>; hide: boolean; top: boolean }[], filterItems: { text: string; enabled: boolean; count: number; filteredCids: Set<string>; hide: boolean; top: boolean }[],
onFilterMatch?: (filterIndex: number, cid: string) => void, subplebbitAddress: string,
onFilterMatch?: (filterIndex: number, cid: string, subplebbitAddress: string) => void,
) => { ) => {
const imageFilter = createImageFilter(showTextOnlyThreads); const imageFilter = createImageFilter(showTextOnlyThreads);
const contentFilter = createContentFilter(filterItems, onFilterMatch); const contentFilter = createContentFilter(filterItems, subplebbitAddress, onFilterMatch);
return { return {
filter: (comment: Comment) => { filter: (comment: Comment) => {
@@ -152,17 +154,25 @@ const Catalog = () => {
const { timeFilterSeconds, timeFilterName } = useTimeFilter(); const { timeFilterSeconds, timeFilterName } = useTimeFilter();
const { sortType } = useSortingStore(); const { sortType } = useSortingStore();
// stable callback for filter matching // Create a stable callback for filter matching
const handleFilterMatch = useCallback((filterIndex: number, cid: string) => { const handleFilterMatch = useCallback((filterIndex: number, cid: string, subplebbitAddress: string) => {
useCatalogFiltersStore.getState().incrementFilterCount(filterIndex, cid); useCatalogFiltersStore.getState().incrementFilterCount(filterIndex, cid, subplebbitAddress);
}, []); }, []);
// Set the current subplebbit address
useEffect(() => {
useCatalogFiltersStore.getState().setCurrentSubplebbitAddress(subplebbitAddress || null);
return () => {
useCatalogFiltersStore.getState().setCurrentSubplebbitAddress(null);
};
}, [subplebbitAddress]);
const feedOptions = useMemo(() => { const feedOptions = useMemo(() => {
const options: any = { const options: any = {
subplebbitAddresses, subplebbitAddresses,
sortType, sortType,
postsPerPage: isInAllView || isInSubscriptionsView ? 10 : postsPerPage, postsPerPage: isInAllView || isInSubscriptionsView ? 10 : postsPerPage,
filter: createCombinedFilter(showTextOnlyThreads, filterItems, handleFilterMatch), filter: createCombinedFilter(showTextOnlyThreads, filterItems, subplebbitAddress || 'all', handleFilterMatch),
}; };
if (isInAllView || isInSubscriptionsView) { if (isInAllView || isInSubscriptionsView) {
@@ -170,7 +180,18 @@ const Catalog = () => {
} }
return options; return options;
}, [subplebbitAddresses, sortType, isInAllView, isInSubscriptionsView, postsPerPage, timeFilterSeconds, showTextOnlyThreads, filterItems, handleFilterMatch]); }, [
subplebbitAddresses,
sortType,
isInAllView,
isInSubscriptionsView,
postsPerPage,
timeFilterSeconds,
showTextOnlyThreads,
filterItems,
subplebbitAddress,
handleFilterMatch,
]);
const { feed, hasMore, loadMore, reset, subplebbitAddressesWithNewerPosts } = useFeed(feedOptions); const { feed, hasMore, loadMore, reset, subplebbitAddressesWithNewerPosts } = useFeed(feedOptions);
const { accountComments } = useAccountComments(); const { accountComments } = useAccountComments();
@@ -226,14 +247,14 @@ const Catalog = () => {
subplebbitAddresses, subplebbitAddresses,
sortType, sortType,
newerThan: 60 * 60 * 24 * 7, newerThan: 60 * 60 * 24 * 7,
filter: createCombinedFilter(showTextOnlyThreads, filterItems, handleFilterMatch), filter: createCombinedFilter(showTextOnlyThreads, filterItems, subplebbitAddress || 'all', handleFilterMatch),
}); });
const { feed: monthlyFeed } = useFeed({ const { feed: monthlyFeed } = useFeed({
subplebbitAddresses, subplebbitAddresses,
sortType, sortType,
newerThan: 60 * 60 * 24 * 30, newerThan: 60 * 60 * 24 * 30,
filter: createCombinedFilter(showTextOnlyThreads, filterItems, handleFilterMatch), filter: createCombinedFilter(showTextOnlyThreads, filterItems, subplebbitAddress || 'all', handleFilterMatch),
}); });
const [showMorePostsSuggestion, setShowMorePostsSuggestion] = useState(false); const [showMorePostsSuggestion, setShowMorePostsSuggestion] = useState(false);