diff --git a/src/app.tsx b/src/app.tsx index fe3e2b3..1d7cf23 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -173,7 +173,6 @@ function AppLayout(props: { children: any }) {
diff --git a/src/components/blog/PostSorting.tsx b/src/components/blog/PostSorting.tsx index 376bb9c..d12c1c0 100644 --- a/src/components/blog/PostSorting.tsx +++ b/src/components/blog/PostSorting.tsx @@ -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>(); + 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 diff --git a/src/components/blog/PostSortingSelect.tsx b/src/components/blog/PostSortingSelect.tsx index cce1b64..9586ea7 100644 --- a/src/components/blog/PostSortingSelect.tsx +++ b/src/components/blog/PostSortingSelect.tsx @@ -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); }); diff --git a/src/components/blog/TagSelector.tsx b/src/components/blog/TagSelector.tsx index a3b88cd..45ca74d 100644 --- a/src/components/blog/TagSelector.tsx +++ b/src/components/blog/TagSelector.tsx @@ -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 (
+
+
+ {filterMode() === "whitelist" + ? "Check tags to show ONLY those posts" + : "Uncheck tags to HIDE those posts"} +
+
+ + {/* Toggle All Button */}
+ + {/* Frequent Tags */} {([key, value]) => (
handleCheck(key.slice(1), e.currentTarget.checked) } @@ -140,6 +242,8 @@ export default function TagSelector(props: TagSelectorProps) {
)}
+ + {/* Rare Tags Section */} 0}>
- {(tag) => ( -
-
{tag.value}
-
- )} + {(tag) => { + const tagValue = tag.value; + return tagValue ? ( + +
{tagValue}
+
+ ) : null; + }}
@@ -333,7 +339,7 @@ export default function PostPage() {
@@ -344,7 +350,14 @@ export default function PostPage() {
-
+
diff --git a/src/routes/blog/index.tsx b/src/routes/blog/index.tsx index 19ffdb7..9a53318 100644 --- a/src/routes/blog/index.tsx +++ b/src/routes/blog/index.tsx @@ -77,6 +77,7 @@ export default function BlogIndex() { const sort = () => searchParams.sort || "newest"; const filters = () => searchParams.filter || ""; + const include = () => searchParams.include || ""; const data = createAsync(() => getPosts(), { deferStream: true }); @@ -84,7 +85,7 @@ export default function BlogIndex() { <> Blog | Michael Freno -
+
}> {(loadedData) => ( <> @@ -118,6 +119,7 @@ export default function BlogIndex() { privilegeLevel={loadedData().privilegeLevel} filters={filters()} sort={sort()} + include={include()} />
diff --git a/src/routes/downloads.tsx b/src/routes/downloads.tsx index 81076e5..46a3fa5 100644 --- a/src/routes/downloads.tsx +++ b/src/routes/downloads.tsx @@ -1,8 +1,6 @@ import { Title, Meta } from "@solidjs/meta"; import { A } from "@solidjs/router"; import DownloadOnAppStore from "~/components/icons/DownloadOnAppStore"; -import GitHub from "~/components/icons/GitHub"; -import LinkedIn from "~/components/icons/LinkedIn"; export default function DownloadsPage() { const download = (assetName: string) => { @@ -15,12 +13,6 @@ export default function DownloadsPage() { .catch((error) => console.error(error)); }; - const joinBetaPrompt = () => { - window.alert( - "This isn't released yet, if you would like to help test, please go the contact page and include the game and platform you would like to help test in the message. Otherwise the apk is available for direct install. Thanks!" - ); - }; - return ( <> Downloads | Michael Freno @@ -40,12 +32,12 @@ export default function DownloadsPage() {
-
+
-
Android (apk only)
+
Android
@@ -53,20 +45,6 @@ export default function DownloadsPage() { Note the android version is not well tested, and has performance issues.
-
Or
- -
(Coming soon)
-
@@ -88,28 +66,15 @@ export default function DownloadsPage() { (apk and iOS)
-
+
Android
-
Or
-
(Coming soon)
-
@@ -142,33 +107,6 @@ export default function DownloadsPage() { Just unzip and drag into 'Applications' folder
- -
diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 926438c..81d6ca1 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -11,7 +11,7 @@ export default function Home() { content="Michael Freno - Software Engineer based in Brooklyn, NY" /> -
+
Hey!
@@ -153,39 +153,40 @@ export default function Home() {
- - And if you love the color schemes of this site -
- +
+ + And if you love the color schemes of this site +
+ +
+ (which of course you do), you can see{" "} + + here + {" "} + - and also see the rest of my various dot files idk. There's a + macos and arch linux rice in there if you're into that kinda thing + and a home server setup too. Which I will write about soon™. +
+
+ +
+ My Collection of +
+ By-the-ways: +
+
+ +
    +
  • I use Neovim
  • +
  • I use Arch Linux
  • +
  • I use Rust
  • +
+
- (which of course you do), you can see{" "} - - here - {" "} - - and also see the rest of my various dot files idk. There's a macos - and arch linux rice in there if you're into that kinda thing and a - home server setup too. Which I will write about soon™. - -
- -
- -
- My Collection of -
- By-the-ways: -
-
- -
    -
  • I use Neovim
  • -
  • I use Arch Linux
  • -
  • I use Rust
  • -
-
+