fixed filtering
This commit is contained in:
@@ -16,59 +16,38 @@ export interface PostSortingProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function PostSorting(props: PostSortingProps) {
|
export default function PostSorting(props: PostSortingProps) {
|
||||||
// Build set of tags that are ALLOWED
|
const filteredPosts = createMemo(() => {
|
||||||
const allowedTags = createMemo(() => {
|
// Build map of post_id -> tags for that post
|
||||||
// WHITELIST MODE: If 'include' param is present, only show posts with those tags
|
const postTags = new Map<number, Set<string>>();
|
||||||
if (props.include) {
|
props.tags.forEach((tag) => {
|
||||||
const includeList = props.include.split("|").filter(Boolean);
|
if (!postTags.has(tag.post_id)) {
|
||||||
return new Set(includeList);
|
postTags.set(tag.post_id, new Set());
|
||||||
}
|
|
||||||
|
|
||||||
// BLACKLIST MODE: Filter out tags in 'filter' param
|
|
||||||
const filterList = props.filters?.split("|").filter(Boolean) || [];
|
|
||||||
|
|
||||||
// If no filters set, all tags are allowed
|
|
||||||
if (filterList.length === 0) {
|
|
||||||
return new Set(props.tags.map((t) => t.value.slice(1)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build set of tags that are checked (allowed to show)
|
|
||||||
const allTags = new Set(props.tags.map((t) => t.value.slice(1)));
|
|
||||||
const filteredOutTags = new Set(filterList);
|
|
||||||
|
|
||||||
const allowed = new Set<string>();
|
|
||||||
allTags.forEach((tag) => {
|
|
||||||
if (!filteredOutTags.has(tag)) {
|
|
||||||
allowed.add(tag);
|
|
||||||
}
|
}
|
||||||
|
// Tag values in DB have # prefix, remove it for comparison
|
||||||
|
const tagWithoutHash = tag.value.startsWith("#")
|
||||||
|
? tag.value.slice(1)
|
||||||
|
: tag.value;
|
||||||
|
postTags.get(tag.post_id)!.add(tagWithoutHash);
|
||||||
});
|
});
|
||||||
|
|
||||||
return allowed;
|
// WHITELIST MODE: Only show posts that have at least one of the included tags
|
||||||
});
|
if (props.include !== undefined) {
|
||||||
|
const includeList = props.include.split("|").filter(Boolean);
|
||||||
|
|
||||||
// Get posts that have at least one allowed tag
|
// Empty whitelist means show nothing
|
||||||
const filteredPosts = createMemo(() => {
|
if (includeList.length === 0) {
|
||||||
const allowed = allowedTags();
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
// In whitelist mode, only show posts with allowed tags
|
const includeSet = new Set(includeList);
|
||||||
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) => {
|
return props.posts.filter((post) => {
|
||||||
const tags = postTags.get(post.id);
|
const tags = postTags.get(post.id);
|
||||||
if (!tags) return false; // Post has no tags
|
if (!tags || tags.size === 0) return false;
|
||||||
|
|
||||||
// Check if post has at least one allowed tag
|
// Post must have at least one tag from the include list
|
||||||
for (const tag of tags) {
|
for (const tag of tags) {
|
||||||
if (allowed.has(tag)) {
|
if (includeSet.has(tag)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -76,38 +55,33 @@ export default function PostSorting(props: PostSortingProps) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// In blacklist mode, show all posts if all tags are allowed
|
// BLACKLIST MODE: Hide posts that have ANY of the filtered tags
|
||||||
if (
|
if (props.filters !== undefined) {
|
||||||
allowed.size ===
|
const filterList = props.filters.split("|").filter(Boolean);
|
||||||
props.tags
|
|
||||||
.map((t) => t.value.slice(1))
|
// Empty blacklist means show everything
|
||||||
.filter((v, i, a) => a.indexOf(v) === i).length
|
if (filterList.length === 0) {
|
||||||
) {
|
return props.posts;
|
||||||
return props.posts;
|
}
|
||||||
|
|
||||||
|
const filterSet = new Set(filterList);
|
||||||
|
|
||||||
|
return props.posts.filter((post) => {
|
||||||
|
const tags = postTags.get(post.id);
|
||||||
|
if (!tags || tags.size === 0) return true; // Show posts with no tags
|
||||||
|
|
||||||
|
// Post must NOT have any blacklisted tags
|
||||||
|
for (const tag of tags) {
|
||||||
|
if (filterSet.has(tag)) {
|
||||||
|
return false; // Hide this post
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true; // Show this post
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build map of post_id -> tags for that post
|
// No filters: show all posts
|
||||||
const postTags = new Map<number, Set<string>>();
|
return props.posts;
|
||||||
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;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const sortedPosts = createMemo(() => {
|
const sortedPosts = createMemo(() => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createSignal, createEffect, For, Show } from "solid-js";
|
import { createSignal, For, Show } from "solid-js";
|
||||||
import { useNavigate, useLocation, useSearchParams } from "@solidjs/router";
|
import { useNavigate, useLocation, useSearchParams } from "@solidjs/router";
|
||||||
import Check from "~/components/icons/Check";
|
import Check from "~/components/icons/Check";
|
||||||
import UpDownArrows from "~/components/icons/UpDownArrows";
|
import UpDownArrows from "~/components/icons/UpDownArrows";
|
||||||
@@ -14,29 +14,25 @@ const sorting = [
|
|||||||
export interface PostSortingSelectProps {}
|
export interface PostSortingSelectProps {}
|
||||||
|
|
||||||
export default function PostSortingSelect(props: PostSortingSelectProps) {
|
export default function PostSortingSelect(props: PostSortingSelectProps) {
|
||||||
const [selected, setSelected] = createSignal(sorting[0]);
|
|
||||||
const [isOpen, setIsOpen] = createSignal(false);
|
const [isOpen, setIsOpen] = createSignal(false);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
const currentFilters = () => searchParams.filter || null;
|
// Derive selected from URL params instead of local state
|
||||||
const currentInclude = () => searchParams.include || null;
|
const selected = () => {
|
||||||
|
const sortParam = searchParams.sort || "newest";
|
||||||
createEffect(() => {
|
return sorting.find((s) => s.val === sortParam) || sorting[0];
|
||||||
let newRoute = location.pathname + "?sort=" + selected().val;
|
};
|
||||||
if (currentFilters()) {
|
|
||||||
newRoute += "&filter=" + currentFilters();
|
|
||||||
}
|
|
||||||
if (currentInclude()) {
|
|
||||||
newRoute += "&include=" + currentInclude();
|
|
||||||
}
|
|
||||||
navigate(newRoute);
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleSelect = (sort: { val: string; label: string }) => {
|
const handleSelect = (sort: { val: string; label: string }) => {
|
||||||
setSelected(sort);
|
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
|
|
||||||
|
// Build new URL preserving all existing params
|
||||||
|
const params = new URLSearchParams(searchParams as Record<string, string>);
|
||||||
|
params.set("sort", sort.val);
|
||||||
|
|
||||||
|
navigate(`${location.pathname}?${params.toString()}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -24,18 +24,33 @@ export default function TagSelector(props: TagSelectorProps) {
|
|||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
const currentSort = () => searchParams.sort || "";
|
|
||||||
const currentFilters = () =>
|
const currentFilters = () =>
|
||||||
searchParams.filter?.split("|").filter(Boolean) || [];
|
searchParams.filter?.split("|").filter(Boolean) || [];
|
||||||
const currentInclude = () =>
|
const currentInclude = () =>
|
||||||
searchParams.include?.split("|").filter(Boolean) || [];
|
searchParams.include?.split("|").filter(Boolean) || [];
|
||||||
|
|
||||||
// Sync filter mode with URL params
|
// Get currently selected tags based on mode
|
||||||
|
const selectedTags = () => {
|
||||||
|
if (filterMode() === "whitelist") {
|
||||||
|
return currentInclude();
|
||||||
|
} else {
|
||||||
|
return currentFilters();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sync filter mode with URL params and ensure one is always present
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (searchParams.include) {
|
if ("include" in searchParams) {
|
||||||
setFilterMode("whitelist");
|
setFilterMode("whitelist");
|
||||||
} else if (searchParams.filter) {
|
} else if ("filter" in searchParams) {
|
||||||
setFilterMode("blacklist");
|
setFilterMode("blacklist");
|
||||||
|
} else {
|
||||||
|
// No filter param exists, default to blacklist mode with empty filter
|
||||||
|
const params = new URLSearchParams(
|
||||||
|
searchParams as Record<string, string>
|
||||||
|
);
|
||||||
|
params.set("filter", "");
|
||||||
|
navigate(`${location.pathname}?${params.toString()}`, { replace: true });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -51,22 +66,15 @@ export default function TagSelector(props: TagSelectorProps) {
|
|||||||
Object.keys(props.tagMap).map((key) => key.slice(1))
|
Object.keys(props.tagMap).map((key) => key.slice(1))
|
||||||
);
|
);
|
||||||
|
|
||||||
// In blacklist mode: checked = not filtered out
|
// Check if a tag is currently selected
|
||||||
// In whitelist mode: checked = included in whitelist
|
|
||||||
const isTagChecked = (tag: string) => {
|
const isTagChecked = (tag: string) => {
|
||||||
if (filterMode() === "whitelist") {
|
return selectedTags().includes(tag);
|
||||||
return currentInclude().includes(tag);
|
|
||||||
} else {
|
|
||||||
return !currentFilters().includes(tag);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const allChecked = createMemo(() => {
|
const allChecked = createMemo(() => {
|
||||||
if (filterMode() === "whitelist") {
|
return (
|
||||||
return currentInclude().length === allTagKeys().length;
|
selectedTags().length === allTagKeys().length && allTagKeys().length > 0
|
||||||
} else {
|
);
|
||||||
return allTagKeys().every((tag) => !currentFilters().includes(tag));
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleClickOutside = (e: MouseEvent) => {
|
const handleClickOutside = (e: MouseEvent) => {
|
||||||
@@ -94,84 +102,80 @@ export default function TagSelector(props: TagSelectorProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleCheck = (tag: string, isChecked: boolean) => {
|
const handleCheck = (tag: string, isChecked: boolean) => {
|
||||||
if (filterMode() === "whitelist") {
|
const currentSelected = selectedTags();
|
||||||
// Whitelist mode: manage include param
|
let newSelected: string[];
|
||||||
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) {
|
if (isChecked) {
|
||||||
const includeStr = newInclude.map((t) => `#${t}`).join("|");
|
// Add tag to selection
|
||||||
navigate(
|
newSelected = [...currentSelected, tag];
|
||||||
`${location.pathname}?sort=${currentSort()}&include=${includeStr}`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// If no tags selected, clear whitelist
|
|
||||||
navigate(`${location.pathname}?sort=${currentSort()}`);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Blacklist mode: manage filter param
|
// Remove tag from selection
|
||||||
if (isChecked) {
|
newSelected = currentSelected.filter((t) => t !== tag);
|
||||||
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 {
|
|
||||||
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}|`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build URL preserving all existing params
|
||||||
|
const params = new URLSearchParams(searchParams as Record<string, string>);
|
||||||
|
const paramName = filterMode() === "whitelist" ? "include" : "filter";
|
||||||
|
const otherParamName = filterMode() === "whitelist" ? "filter" : "include";
|
||||||
|
|
||||||
|
// Remove the other mode's param
|
||||||
|
params.delete(otherParamName);
|
||||||
|
|
||||||
|
if (newSelected.length > 0) {
|
||||||
|
const paramValue = newSelected.join("|");
|
||||||
|
params.set(paramName, paramValue);
|
||||||
|
} else {
|
||||||
|
// Keep empty param to preserve mode (especially important for whitelist)
|
||||||
|
params.set(paramName, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
navigate(`${location.pathname}?${params.toString()}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleToggleAll = () => {
|
const handleToggleAll = () => {
|
||||||
if (filterMode() === "whitelist") {
|
const params = new URLSearchParams(searchParams as Record<string, string>);
|
||||||
if (allChecked()) {
|
const paramName = filterMode() === "whitelist" ? "include" : "filter";
|
||||||
// Uncheck all: clear whitelist
|
const otherParamName = filterMode() === "whitelist" ? "filter" : "include";
|
||||||
navigate(`${location.pathname}?sort=${currentSort()}`);
|
|
||||||
} else {
|
// Remove the other mode's param
|
||||||
// Check all: add all tags to whitelist
|
params.delete(otherParamName);
|
||||||
const allTags = allTagKeys()
|
|
||||||
.map((t) => `#${t}`)
|
if (allChecked()) {
|
||||||
.join("|");
|
// Uncheck all: keep empty param to preserve mode
|
||||||
navigate(
|
params.set(paramName, "");
|
||||||
`${location.pathname}?sort=${currentSort()}&include=${allTags}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if (allChecked()) {
|
// Check all: select all tags
|
||||||
// Uncheck all: Build filter string with all tags
|
const allTags = allTagKeys().join("|");
|
||||||
const allTags = allTagKeys().join("|") + "|";
|
params.set(paramName, allTags);
|
||||||
navigate(
|
|
||||||
`${location.pathname}?sort=${currentSort()}&filter=${allTags}`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// Check all: Remove filter param
|
|
||||||
navigate(`${location.pathname}?sort=${currentSort()}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
navigate(`${location.pathname}?${params.toString()}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleFilterMode = () => {
|
const toggleFilterMode = () => {
|
||||||
|
// Get current tags BEFORE changing mode
|
||||||
|
const currentSelected = selectedTags();
|
||||||
|
|
||||||
const newMode = filterMode() === "whitelist" ? "blacklist" : "whitelist";
|
const newMode = filterMode() === "whitelist" ? "blacklist" : "whitelist";
|
||||||
setFilterMode(newMode);
|
setFilterMode(newMode);
|
||||||
// Clear all filters when switching modes
|
|
||||||
navigate(`${location.pathname}?sort=${currentSort()}`);
|
// Keep the same selected tags, just change the param name
|
||||||
|
const params = new URLSearchParams(searchParams as Record<string, string>);
|
||||||
|
const newParamName = newMode === "whitelist" ? "include" : "filter";
|
||||||
|
const oldParamName = newMode === "whitelist" ? "filter" : "include";
|
||||||
|
|
||||||
|
// Remove old param and set new one
|
||||||
|
params.delete(oldParamName);
|
||||||
|
|
||||||
|
if (currentSelected.length > 0) {
|
||||||
|
const paramValue = currentSelected.join("|");
|
||||||
|
params.set(newParamName, paramValue);
|
||||||
|
} else {
|
||||||
|
// Always keep the param, even if empty
|
||||||
|
params.set(newParamName, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
navigate(`${location.pathname}?${params.toString()}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -209,8 +213,8 @@ export default function TagSelector(props: TagSelectorProps) {
|
|||||||
</div>
|
</div>
|
||||||
<div class="text-subtext1 text-xs italic">
|
<div class="text-subtext1 text-xs italic">
|
||||||
{filterMode() === "whitelist"
|
{filterMode() === "whitelist"
|
||||||
? "Check tags to show ONLY those posts"
|
? "Check tags to show ONLY posts with those tags"
|
||||||
: "Uncheck tags to HIDE those posts"}
|
: "Check tags to HIDE posts with those tags"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -76,8 +76,10 @@ export default function BlogIndex() {
|
|||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
const sort = () => searchParams.sort || "newest";
|
const sort = () => searchParams.sort || "newest";
|
||||||
const filters = () => searchParams.filter || "";
|
const filters = () =>
|
||||||
const include = () => searchParams.include || "";
|
"filter" in searchParams ? searchParams.filter : undefined;
|
||||||
|
const include = () =>
|
||||||
|
"include" in searchParams ? searchParams.include : undefined;
|
||||||
|
|
||||||
const data = createAsync(() => getPosts(), { deferStream: true });
|
const data = createAsync(() => getPosts(), { deferStream: true });
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user