better
This commit is contained in:
@@ -12,11 +12,19 @@ export interface PostSortingProps {
|
||||
privilegeLevel: "anonymous" | "admin" | "user";
|
||||
filters?: string;
|
||||
sort?: string;
|
||||
include?: string;
|
||||
}
|
||||
|
||||
export default function PostSorting(props: PostSortingProps) {
|
||||
// Build set of tags that are ALLOWED (not filtered out)
|
||||
// Build set of tags that are ALLOWED
|
||||
const allowedTags = createMemo(() => {
|
||||
// WHITELIST MODE: If 'include' param is present, only show posts with those tags
|
||||
if (props.include) {
|
||||
const includeList = props.include.split("|").filter(Boolean);
|
||||
return new Set(includeList);
|
||||
}
|
||||
|
||||
// BLACKLIST MODE: Filter out tags in 'filter' param
|
||||
const filterList = props.filters?.split("|").filter(Boolean) || [];
|
||||
|
||||
// If no filters set, all tags are allowed
|
||||
@@ -42,7 +50,33 @@ export default function PostSorting(props: PostSortingProps) {
|
||||
const filteredPosts = createMemo(() => {
|
||||
const allowed = allowedTags();
|
||||
|
||||
// If all tags are allowed, show all posts
|
||||
// In whitelist mode, only show posts with allowed tags
|
||||
if (props.include) {
|
||||
// Build map of post_id -> tags for that post
|
||||
const postTags = new Map<number, Set<string>>();
|
||||
props.tags.forEach((tag) => {
|
||||
if (!postTags.has(tag.post_id)) {
|
||||
postTags.set(tag.post_id, new Set());
|
||||
}
|
||||
postTags.get(tag.post_id)!.add(tag.value.slice(1));
|
||||
});
|
||||
|
||||
// Keep posts that have at least one allowed tag
|
||||
return props.posts.filter((post) => {
|
||||
const tags = postTags.get(post.id);
|
||||
if (!tags) return false; // Post has no tags
|
||||
|
||||
// Check if post has at least one allowed tag
|
||||
for (const tag of tags) {
|
||||
if (allowed.has(tag)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
// In blacklist mode, show all posts if all tags are allowed
|
||||
if (
|
||||
allowed.size ===
|
||||
props.tags
|
||||
|
||||
@@ -21,12 +21,16 @@ export default function PostSortingSelect(props: PostSortingSelectProps) {
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const currentFilters = () => searchParams.filter || null;
|
||||
const currentInclude = () => searchParams.include || null;
|
||||
|
||||
createEffect(() => {
|
||||
let newRoute = location.pathname + "?sort=" + selected().val;
|
||||
if (currentFilters()) {
|
||||
newRoute += "&filter=" + currentFilters();
|
||||
}
|
||||
if (currentInclude()) {
|
||||
newRoute += "&include=" + currentInclude();
|
||||
}
|
||||
navigate(newRoute);
|
||||
});
|
||||
|
||||
|
||||
@@ -15,6 +15,9 @@ export interface TagSelectorProps {
|
||||
export default function TagSelector(props: TagSelectorProps) {
|
||||
const [showingMenu, setShowingMenu] = createSignal(false);
|
||||
const [showingRareTags, setShowingRareTags] = createSignal(false);
|
||||
const [filterMode, setFilterMode] = createSignal<"whitelist" | "blacklist">(
|
||||
"blacklist"
|
||||
);
|
||||
let buttonRef: HTMLButtonElement | undefined;
|
||||
let menuRef: HTMLDivElement | undefined;
|
||||
const navigate = useNavigate();
|
||||
@@ -22,7 +25,19 @@ export default function TagSelector(props: TagSelectorProps) {
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const currentSort = () => searchParams.sort || "";
|
||||
const currentFilters = () => searchParams.filter?.split("|") || [];
|
||||
const currentFilters = () =>
|
||||
searchParams.filter?.split("|").filter(Boolean) || [];
|
||||
const currentInclude = () =>
|
||||
searchParams.include?.split("|").filter(Boolean) || [];
|
||||
|
||||
// Sync filter mode with URL params
|
||||
createEffect(() => {
|
||||
if (searchParams.include) {
|
||||
setFilterMode("whitelist");
|
||||
} else if (searchParams.filter) {
|
||||
setFilterMode("blacklist");
|
||||
}
|
||||
});
|
||||
|
||||
const frequentTags = createMemo(() =>
|
||||
Object.entries(props.tagMap).filter(([_, count]) => count > 1)
|
||||
@@ -36,9 +51,23 @@ export default function TagSelector(props: TagSelectorProps) {
|
||||
Object.keys(props.tagMap).map((key) => key.slice(1))
|
||||
);
|
||||
|
||||
const allChecked = createMemo(() =>
|
||||
allTagKeys().every((tag) => !currentFilters().includes(tag))
|
||||
);
|
||||
// In blacklist mode: checked = not filtered out
|
||||
// In whitelist mode: checked = included in whitelist
|
||||
const isTagChecked = (tag: string) => {
|
||||
if (filterMode() === "whitelist") {
|
||||
return currentInclude().includes(tag);
|
||||
} else {
|
||||
return !currentFilters().includes(tag);
|
||||
}
|
||||
};
|
||||
|
||||
const allChecked = createMemo(() => {
|
||||
if (filterMode() === "whitelist") {
|
||||
return currentInclude().length === allTagKeys().length;
|
||||
} else {
|
||||
return allTagKeys().every((tag) => !currentFilters().includes(tag));
|
||||
}
|
||||
});
|
||||
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
if (
|
||||
@@ -64,42 +93,87 @@ export default function TagSelector(props: TagSelectorProps) {
|
||||
setShowingMenu(!showingMenu());
|
||||
};
|
||||
|
||||
const handleCheck = (filter: string, isChecked: boolean) => {
|
||||
if (isChecked) {
|
||||
const newFilters = searchParams.filter?.replace(filter + "|", "");
|
||||
if (newFilters && newFilters.length >= 1) {
|
||||
const handleCheck = (tag: string, isChecked: boolean) => {
|
||||
if (filterMode() === "whitelist") {
|
||||
// Whitelist mode: manage include param
|
||||
let newInclude: string[];
|
||||
if (isChecked) {
|
||||
// Add to whitelist
|
||||
newInclude = [...currentInclude(), tag];
|
||||
} else {
|
||||
// Remove from whitelist
|
||||
newInclude = currentInclude().filter((t) => t !== tag);
|
||||
}
|
||||
|
||||
if (newInclude.length > 0) {
|
||||
const includeStr = newInclude.map((t) => `#${t}`).join("|");
|
||||
navigate(
|
||||
`${location.pathname}?sort=${currentSort()}&filter=${newFilters}`
|
||||
`${location.pathname}?sort=${currentSort()}&include=${includeStr}`
|
||||
);
|
||||
} else {
|
||||
// If no tags selected, clear whitelist
|
||||
navigate(`${location.pathname}?sort=${currentSort()}`);
|
||||
}
|
||||
} else {
|
||||
const currentFiltersStr = searchParams.filter;
|
||||
if (currentFiltersStr) {
|
||||
const newFilters = currentFiltersStr + filter + "|";
|
||||
navigate(
|
||||
`${location.pathname}?sort=${currentSort()}&filter=${newFilters}`
|
||||
);
|
||||
// Blacklist mode: manage filter param
|
||||
if (isChecked) {
|
||||
const newFilters = searchParams.filter?.replace(tag + "|", "");
|
||||
if (newFilters && newFilters.length >= 1) {
|
||||
navigate(
|
||||
`${location.pathname}?sort=${currentSort()}&filter=${newFilters}`
|
||||
);
|
||||
} else {
|
||||
navigate(`${location.pathname}?sort=${currentSort()}`);
|
||||
}
|
||||
} else {
|
||||
navigate(
|
||||
`${location.pathname}?sort=${currentSort()}&filter=${filter}|`
|
||||
);
|
||||
const currentFiltersStr = searchParams.filter;
|
||||
if (currentFiltersStr) {
|
||||
const newFilters = currentFiltersStr + tag + "|";
|
||||
navigate(
|
||||
`${location.pathname}?sort=${currentSort()}&filter=${newFilters}`
|
||||
);
|
||||
} else {
|
||||
navigate(`${location.pathname}?sort=${currentSort()}&filter=${tag}|`);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleAll = () => {
|
||||
if (allChecked()) {
|
||||
// Uncheck all: Build filter string with all tags
|
||||
const allTags = allTagKeys().join("|") + "|";
|
||||
navigate(`${location.pathname}?sort=${currentSort()}&filter=${allTags}`);
|
||||
if (filterMode() === "whitelist") {
|
||||
if (allChecked()) {
|
||||
// Uncheck all: clear whitelist
|
||||
navigate(`${location.pathname}?sort=${currentSort()}`);
|
||||
} else {
|
||||
// Check all: add all tags to whitelist
|
||||
const allTags = allTagKeys()
|
||||
.map((t) => `#${t}`)
|
||||
.join("|");
|
||||
navigate(
|
||||
`${location.pathname}?sort=${currentSort()}&include=${allTags}`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Check all: Remove filter param
|
||||
navigate(`${location.pathname}?sort=${currentSort()}`);
|
||||
if (allChecked()) {
|
||||
// Uncheck all: Build filter string with all tags
|
||||
const allTags = allTagKeys().join("|") + "|";
|
||||
navigate(
|
||||
`${location.pathname}?sort=${currentSort()}&filter=${allTags}`
|
||||
);
|
||||
} else {
|
||||
// Check all: Remove filter param
|
||||
navigate(`${location.pathname}?sort=${currentSort()}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const toggleFilterMode = () => {
|
||||
const newMode = filterMode() === "whitelist" ? "blacklist" : "whitelist";
|
||||
setFilterMode(newMode);
|
||||
// Clear all filters when switching modes
|
||||
navigate(`${location.pathname}?sort=${currentSort()}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="relative">
|
||||
<button
|
||||
@@ -113,23 +187,51 @@ export default function TagSelector(props: TagSelectorProps) {
|
||||
<Show when={showingMenu()}>
|
||||
<div
|
||||
ref={menuRef}
|
||||
class="bg-surface0 absolute top-full left-0 z-50 mt-2 rounded-lg py-2 pr-4 pl-2 shadow-lg"
|
||||
class="bg-surface0 absolute top-full left-0 z-50 mt-2 min-w-64 rounded-lg py-2 pr-4 pl-2 shadow-lg"
|
||||
>
|
||||
{/* Filter Mode Toggle */}
|
||||
<div class="border-overlay0 mb-2 border-b pb-2">
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<span class="text-subtext0 text-xs font-medium">
|
||||
Filter Mode:
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleFilterMode}
|
||||
class={`rounded px-2 py-1 text-xs font-semibold transition-all duration-200 hover:brightness-110 ${
|
||||
filterMode() === "whitelist"
|
||||
? "bg-green text-base"
|
||||
: "bg-red text-base"
|
||||
}`}
|
||||
>
|
||||
{filterMode() === "whitelist" ? "✓ Whitelist" : "✗ Blacklist"}
|
||||
</button>
|
||||
</div>
|
||||
<div class="text-subtext1 text-xs italic">
|
||||
{filterMode() === "whitelist"
|
||||
? "Check tags to show ONLY those posts"
|
||||
: "Uncheck tags to HIDE those posts"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Toggle All Button */}
|
||||
<div class="border-overlay0 mb-2 flex justify-center border-b pb-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleToggleAll}
|
||||
class="text-text hover:text-red text-xs font-medium underline"
|
||||
class="text-text hover:text-blue text-xs font-medium underline"
|
||||
>
|
||||
{allChecked() ? "Uncheck All" : "Check All"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Frequent Tags */}
|
||||
<For each={frequentTags()}>
|
||||
{([key, value]) => (
|
||||
<div class="mx-auto my-2 flex">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={!currentFilters().includes(key.slice(1))}
|
||||
checked={isTagChecked(key.slice(1))}
|
||||
onChange={(e) =>
|
||||
handleCheck(key.slice(1), e.currentTarget.checked)
|
||||
}
|
||||
@@ -140,6 +242,8 @@ export default function TagSelector(props: TagSelectorProps) {
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
|
||||
{/* Rare Tags Section */}
|
||||
<Show when={rareTags().length > 0}>
|
||||
<div class="border-overlay0 mt-2 border-t pt-2">
|
||||
<button
|
||||
@@ -155,7 +259,7 @@ export default function TagSelector(props: TagSelectorProps) {
|
||||
<div class="mx-auto my-2 flex">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={!currentFilters().includes(key.slice(1))}
|
||||
checked={isTagChecked(key.slice(1))}
|
||||
onChange={(e) =>
|
||||
handleCheck(key.slice(1), e.currentTarget.checked)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user