mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
feat(catalog filter): add filtering with complex patterns, including regex, help modal
This commit is contained in:
@@ -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>{'/^>/'}</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>
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user