mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
update board buttons UI, add catalog search UI
This commit is contained in:
@@ -51,7 +51,6 @@
|
||||
}
|
||||
|
||||
.desktopBoardButtons {
|
||||
padding-right: 5px;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,17 +4,18 @@ import { useAccountComment, useSubscribe } from '@plebbit/plebbit-react-hooks';
|
||||
import useSubplebbitsStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits';
|
||||
import useSubplebbitsPagesStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits-pages';
|
||||
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 useFeedResetStore from '../../stores/use-feed-reset-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 _ from 'lodash';
|
||||
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 {
|
||||
address?: string | undefined;
|
||||
@@ -282,7 +283,7 @@ export const MobileBoardButtons = () => {
|
||||
<SortOptions /> <ImageSizeOptions />
|
||||
</div>
|
||||
<div className={styles.mobileCatalogOptionsPadding}>
|
||||
<ShowOPCommentOption /> <CatalogFilters />
|
||||
<ShowOPCommentOption /> <CatalogFilters /> <CatalogSearch />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
@@ -380,17 +381,16 @@ export const DesktopBoardButtons = () => {
|
||||
{(isInAllView || isInSubscriptionsView) && (
|
||||
<TimeFilter isInAllView={isInAllView} isInCatalogView={isInCatalogView} isInSubscriptionsView={isInSubscriptionsView} />
|
||||
)}
|
||||
{isInCatalogView && (
|
||||
<>
|
||||
[
|
||||
<CatalogFilters />]
|
||||
</>
|
||||
)}{' '}
|
||||
{!(isInAllView || isInSubscriptionsView) && (
|
||||
<>
|
||||
[
|
||||
<SubscribeButton address={subplebbitAddress} />]
|
||||
</>
|
||||
)}{' '}
|
||||
{isInCatalogView && (
|
||||
<>
|
||||
[<CatalogFilters />] <CatalogSearch />
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
</>
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
.overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: rgba(0, 0, 0, 0.25);
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.modal label, .modal button, .modal select {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.modal button {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.modal {
|
||||
top: 60px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 380px;
|
||||
height: auto;
|
||||
max-height: 80%;
|
||||
overflow-y: auto;
|
||||
position: fixed;
|
||||
z-index: 5;
|
||||
padding: 2px;
|
||||
font-size: var(--settings-modal-font-size);
|
||||
box-shadow: 0 0 5px rgba(0, 0, 0, 0.25);
|
||||
background-color: var(--settings-modal-background-color);
|
||||
border-top: var(--settings-modal-border-top);
|
||||
border-right: var(--settings-modal-border-right);
|
||||
border-bottom: var(--settings-modal-border-bottom);
|
||||
border-left: var(--settings-modal-border-left);
|
||||
}
|
||||
|
||||
@media (max-width: 420px) {
|
||||
.modal {
|
||||
width: 90vw;
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 5px;
|
||||
margin-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
text-align: center;
|
||||
line-height: 14px;
|
||||
border-bottom: var(--settings-modal-header-border-bottom);
|
||||
}
|
||||
|
||||
.title {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.closeButton {
|
||||
right: 5px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
display: block;
|
||||
background-size: 100%;
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
image-rendering: pixelated;
|
||||
background-image: var(--settings-modal-close-button-background-image);
|
||||
}
|
||||
|
||||
.filters {
|
||||
padding: 5px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.filters label {
|
||||
display: inline-block;
|
||||
margin-bottom: 7px;
|
||||
}
|
||||
|
||||
.filters label input[type='checkbox'] {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.categoryTitle {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.paddingBottom {
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.saveButton {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.filtersTable {
|
||||
width: 100%;
|
||||
border-spacing: 1px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding: 10px 2px 2px 2px;
|
||||
}
|
||||
|
||||
.filtersTable th {
|
||||
text-align: center;
|
||||
font-weight: 700;
|
||||
font-size: 11px;
|
||||
min-width: 20px;
|
||||
}
|
||||
|
||||
.filtersTable tbody td {
|
||||
padding: 8px 0 0;
|
||||
}
|
||||
|
||||
.filtersTable td {
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
font-size: 10pt;
|
||||
}
|
||||
|
||||
.filtersTable input[type='checkbox'] {
|
||||
display: inline-block;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.filtersTable input[type='text'] {
|
||||
margin: 0 2px;
|
||||
padding: 1px;
|
||||
outline: none;
|
||||
border: var(--reply-modal-field-input-border, revert);
|
||||
font-size: 11px;
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.filtersTable input[type='text']:focus {
|
||||
border: var(--reply-modal-field-input-border-focus, revert);
|
||||
}
|
||||
|
||||
.filtersTable span {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.filtersTable tfoot td {
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.addButton {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.saveButton {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.filterHits {
|
||||
font-size: 12px !important;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.clickbox {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
line-height: 16px;
|
||||
font-size: 10px;
|
||||
display: block;
|
||||
text-align: center;
|
||||
background-color: #fff;
|
||||
border: 1px solid #aaa;
|
||||
text-decoration: none;
|
||||
color: #000;
|
||||
margin: auto;
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
import { useState, useCallback, useRef, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import useCatalogFiltersStore from '../../stores/use-catalog-filters-store';
|
||||
import styles from './catalog-filters.module.css';
|
||||
|
||||
const FiltersTable = ({ onSave }: { onSave: () => void }) => {
|
||||
const { t } = useTranslation();
|
||||
const { filterItems, saveAndApplyFilters } = useCatalogFiltersStore();
|
||||
|
||||
const [localFilterItems, setLocalFilterItems] = useState(
|
||||
filterItems.map((item) => ({
|
||||
...item,
|
||||
hide: item.hide ?? true,
|
||||
top: item.top ?? false,
|
||||
})),
|
||||
);
|
||||
|
||||
const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
|
||||
|
||||
// Update local items when store changes
|
||||
useEffect(() => {
|
||||
setLocalFilterItems(
|
||||
filterItems.map((item) => ({
|
||||
...item,
|
||||
hide: item.hide ?? true,
|
||||
top: item.top ?? false,
|
||||
})),
|
||||
);
|
||||
}, [filterItems]);
|
||||
|
||||
const handleAddFilter = useCallback(() => {
|
||||
setLocalFilterItems((prev) => {
|
||||
const newIndex = prev.length;
|
||||
setTimeout(() => {
|
||||
inputRefs.current[newIndex]?.focus();
|
||||
}, 0);
|
||||
return [
|
||||
...prev,
|
||||
{
|
||||
text: '',
|
||||
enabled: true,
|
||||
count: 0,
|
||||
filteredCids: new Set<string>(),
|
||||
subplebbitCounts: new Map<string, number>(),
|
||||
subplebbitFilteredCids: new Map<string, Set<string>>(),
|
||||
hide: true,
|
||||
top: false,
|
||||
},
|
||||
];
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
const nonEmptyFilters = localFilterItems.filter((item) => item.text.trim() !== '');
|
||||
saveAndApplyFilters(nonEmptyFilters);
|
||||
onSave();
|
||||
}, [saveAndApplyFilters, localFilterItems, onSave]);
|
||||
|
||||
const updateLocalFilterItem = useCallback((index: number, item: any) => {
|
||||
setLocalFilterItems((prev) => prev.map((f, i) => (i === index ? item : f)));
|
||||
}, []);
|
||||
|
||||
const removeLocalFilterItem = useCallback((index: number) => {
|
||||
setLocalFilterItems((prev) => prev.filter((_, i) => i !== index));
|
||||
}, []);
|
||||
|
||||
const moveLocalFilterItemUp = useCallback((index: number) => {
|
||||
if (index === 0) return;
|
||||
setLocalFilterItems((prev) => {
|
||||
const newItems = [...prev];
|
||||
[newItems[index - 1], newItems[index]] = [newItems[index], newItems[index - 1]];
|
||||
return newItems;
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<table className={styles.filtersTable}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>order</th>
|
||||
<th>on</th>
|
||||
<th>pattern</th>
|
||||
<th>color</th>
|
||||
<th>hide</th>
|
||||
<th>top</th>
|
||||
<th>del</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{localFilterItems.map((item, index) => (
|
||||
<tr key={index}>
|
||||
<td>
|
||||
<span className={styles.orderButton} onClick={() => moveLocalFilterItemUp(index)}>
|
||||
↑
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type='checkbox'
|
||||
className={styles.onCheckbox}
|
||||
checked={item.enabled}
|
||||
onChange={(e) => updateLocalFilterItem(index, { ...item, enabled: e.target.checked })}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type='text'
|
||||
autoCorrect='off'
|
||||
autoComplete='off'
|
||||
spellCheck='false'
|
||||
value={item.text}
|
||||
onChange={(e) => updateLocalFilterItem(index, { ...item, text: e.target.value })}
|
||||
ref={(el) => (inputRefs.current[index] = el)}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<span className={styles.clickbox} />
|
||||
</td>
|
||||
<td>
|
||||
<input type='checkbox' checked={item.hide} onChange={(e) => updateLocalFilterItem(index, { ...item, hide: e.target.checked })} />
|
||||
</td>
|
||||
<td>
|
||||
<input type='checkbox' checked={item.top} onChange={(e) => updateLocalFilterItem(index, { ...item, top: e.target.checked })} />
|
||||
</td>
|
||||
<td>
|
||||
<span className={styles.deleteButton} onClick={() => removeLocalFilterItem(index)}>
|
||||
×
|
||||
</span>
|
||||
</td>
|
||||
<td className={styles.filterHits}>{item.count > 0 && `x${item.count}`}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colSpan={7}>
|
||||
<button className={styles.addButton} onClick={handleAddFilter}>
|
||||
{t('add')}
|
||||
</button>
|
||||
<button className={styles.saveButton} onClick={handleSave}>
|
||||
{t('save')}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
|
||||
const FiltersModal = ({ closeModal }: { closeModal: () => void }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.overlay} onClick={closeModal} />
|
||||
<div className={styles.modal}>
|
||||
<div className={styles.header}>
|
||||
<span className={styles.title}>{t('filters_and_highlights')}</span>
|
||||
<span className={styles.closeButton} title='close' onClick={closeModal} />
|
||||
</div>
|
||||
<FiltersTable onSave={closeModal} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const CatalogFilters = () => {
|
||||
const { t } = useTranslation();
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
|
||||
const closeModal = useCallback(() => {
|
||||
setShowModal(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const onEscapeKey = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
closeModal();
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', onEscapeKey);
|
||||
return () => document.removeEventListener('keydown', onEscapeKey);
|
||||
}, [closeModal]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<span className={`${styles.filtersButton} button`} onClick={() => setShowModal(true)}>
|
||||
{t('filters')}
|
||||
</span>
|
||||
{showModal && <FiltersModal closeModal={closeModal} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CatalogFilters;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './catalog-filters';
|
||||
@@ -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;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './catalog-search';
|
||||
@@ -15,8 +15,7 @@
|
||||
|
||||
.postFormMobile {
|
||||
text-align: center;
|
||||
margin-top: -2px;
|
||||
padding-bottom: 7px;
|
||||
margin-top: -3px;
|
||||
}
|
||||
|
||||
.closed {
|
||||
|
||||
Reference in New Issue
Block a user