feat(catalog filter): add filtering with complex patterns, including regex, help modal

This commit is contained in:
Tom (plebeius.eth)
2025-03-06 23:48:17 +01:00
parent ad60404792
commit d7a9c1640b
45 changed files with 500 additions and 85 deletions
@@ -33,7 +33,7 @@
.modal {
top: 60px;
left: 50%;
left: calc(50% + 5px);
transform: translateX(-50%);
width: 380px;
height: auto;
@@ -41,14 +41,23 @@
overflow-y: auto;
position: fixed;
z-index: 5;
padding: 2px;
padding: 2px 5px 5px 5px;
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);
}
.filtersProtipModal {
top: 50px;
width: 570px;
left: 50%;
padding: 2px 10px 5px 10px;
}
@media (max-width: 614px) {
.filtersProtipModal {
width: 90vw;
}
}
@media (max-width: 420px) {
@@ -82,6 +91,54 @@
top: 5px;
image-rendering: pixelated;
background-image: var(--settings-modal-close-button-background-image);
cursor: pointer;
}
.openHelpButton {
right: 23px;
width: 18px;
height: 18px;
display: block;
background-size: 100%;
position: absolute;
top: 5px;
image-rendering: pixelated;
background-image: var(--open-help-button-background-image);
cursor: pointer;
}
.filtersProtip {
text-transform: none;
}
.filtersProtip code {
padding: 1px 5px 1px 5px;
background-color: #EEE;
color: #000;
}
.filtersProtip li {
list-style: none;
margin: 5px 0px;
}
.filtersProtip ul {
margin-bottom: 10px;
}
.filtersProtip h4 {
font-size: 15px;
margin: 10px 0px 5px 0px;
padding-top: 10px;
}
.filtersProtip h4:first-child {
padding-top: 0px;
}
.filtersProtip h4::before {
content: "»";
margin-right: 3px;
}
.filters {
@@ -108,16 +165,12 @@
padding-bottom: 5px;
}
.saveButton {
margin-left: 5px;
}
.filtersTable {
width: 100%;
border-spacing: 1px;
margin-left: auto;
margin-right: auto;
padding: 10px 2px 2px 2px;
padding: 0px 2px 2px 2px;
}
.filtersTable th {
@@ -168,7 +221,8 @@
}
.saveButton {
float: right;
position: absolute;
right: 5px;
}
.filterHits {
@@ -1,11 +1,13 @@
import { useState, useCallback, useRef, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import useCatalogFiltersStore from '../../stores/use-catalog-filters-store';
import useFeedResetStore from '../../stores/use-feed-reset-store';
import styles from './catalog-filters.module.css';
const FiltersTable = ({ onSave }: { onSave: () => void }) => {
const { t } = useTranslation();
const { filterItems, saveAndApplyFilters, currentSubplebbitAddress } = useCatalogFiltersStore();
const resetFeed = useFeedResetStore((state) => state.reset);
const [localFilterItems, setLocalFilterItems] = useState(
filterItems.map((item) => ({
@@ -52,9 +54,17 @@ const FiltersTable = ({ onSave }: { onSave: () => void }) => {
const handleSave = useCallback(() => {
const nonEmptyFilters = localFilterItems.filter((item) => item.text.trim() !== '');
saveAndApplyFilters(nonEmptyFilters);
useCatalogFiltersStore.getState().resetCountsForCurrentSubplebbit();
if (resetFeed) {
resetFeed();
}
onSave();
}, [saveAndApplyFilters, localFilterItems, onSave]);
}, [saveAndApplyFilters, localFilterItems, onSave, resetFeed]);
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
@@ -142,7 +152,7 @@ const FiltersTable = ({ onSave }: { onSave: () => void }) => {
</span>
</td>
<td className={styles.filterHits}>
{currentSubplebbitAddress && (item.subplebbitCounts?.get(currentSubplebbitAddress) ?? 0) > 0 && `x${item.subplebbitCounts?.get(currentSubplebbitAddress)}`}
{currentSubplebbitAddress && item.subplebbitFilteredCids?.has(currentSubplebbitAddress) && `x${item.subplebbitCounts?.get(currentSubplebbitAddress) ?? 0}`}
</td>
</tr>
))}
@@ -163,18 +173,107 @@ const FiltersTable = ({ onSave }: { onSave: () => void }) => {
);
};
const FiltersProtip = () => {
return (
<div className={styles.filtersProtip}>
<h4>Patterns</h4>
<ul>
<li>
<strong>Matching whole words:</strong>
</li>
<li>
<code>feel</code> will match <em>"feel"</em> but not <em>"feeling"</em>. This search is case-insensitive.
</li>
</ul>
<ul>
<li>
<strong>AND operator:</strong>
</li>
<li>
<code>feel girlfriend</code> will match <em>"feel"</em> AND <em>"girlfriend"</em> in any order.
</li>
</ul>
<ul>
<li>
<strong>OR operator:</strong>
</li>
<li>
<code>feel|girlfriend</code> will match <em>"feel"</em> OR <em>"girlfriend"</em>.
</li>
</ul>
<ul>
<li>
<strong>Mixing both operators:</strong>
</li>
<li>
<code>girlfriend|boyfriend feel</code> matches <em>"feel"</em> AND <em>"girlfriend"</em>, or <em>"feel"</em> AND <em>"boyfriend"</em>.
</li>
</ul>
<ul>
<li>
<strong>Exact match search:</strong>
</li>
<li>
<code>"that feel when"</code> place double quotes around the pattern to search for an exact string.
</li>
</ul>
<ul>
<li>
<strong>Wildcards:</strong>
</li>
<li>
<code>feel*</code> matches expressions such as <em>"feel"</em>, <em>"feels"</em>, <em>"feeling"</em>, <em>"feeler"</em>, etc
</li>
<li>
<code>idolm*ster</code> this can match <em>"idolmaster"</em> or <em>"idolm@ster"</em>, etc
</li>
</ul>
<ul>
<strong>It is also possible to filter by regular expression:</strong>
<li>
<code>/^(?=.*detachable)(?=.*hats).*$/i</code> AND operator.
</li>
<li>
<code>/^(?!.*touhou).*$/i</code> NOT operator.
</li>
<li>
<code>{'/^&gt;/'}</code> threads starting with a quote (<em>{'">"'}</em> character as an html entity).
</li>
<li>
<code>/^$/</code> threads with no text.
</li>
</ul>
<h4>Controls</h4>
<ul>
<li>
<strong>On</strong> enables or disables the filter.
</li>
<li>
<strong>Hide</strong> hides matched threads.
</li>
<li>
<strong>Top</strong> moves the filter to the top of the feed.
</li>
</ul>
</div>
);
};
const FiltersModal = ({ closeModal }: { closeModal: () => void }) => {
const { t } = useTranslation();
const [showHelp, setShowHelp] = useState(false);
const openHelp = () => setShowHelp(true);
const closeHelp = () => setShowHelp(false);
return (
<>
<div className={styles.overlay} onClick={closeModal} />
<div className={styles.modal}>
<div className={styles.overlay} onClick={showHelp ? closeHelp : closeModal} />
<div className={`${styles.modal} ${showHelp ? styles.filtersProtipModal : ''}`}>
<div className={styles.header}>
<span className={styles.title}>{t('filters_and_highlights')}</span>
<span className={styles.closeButton} title='close' onClick={closeModal} />
<span className={styles.title}>{showHelp ? t('filter_and_highlights_help') : t('filter_and_highlights')}</span>
{!showHelp && <span className={styles.openHelpButton} title={t('help')} onClick={openHelp} />}
<span className={styles.closeButton} title={t('close')} onClick={closeModal} />
</div>
<FiltersTable onSave={closeModal} />
{showHelp ? <FiltersProtip /> : <FiltersTable onSave={closeModal} />}
</div>
</>
);