This commit is contained in:
Michael Freno
2025-12-23 09:35:55 -05:00
parent be3e9a9e0b
commit 2ebd7840b7
8 changed files with 235 additions and 140 deletions

View File

@@ -173,7 +173,6 @@ function AppLayout(props: { children: any }) {
</div>
</noscript>
<div
class="py-16"
onMouseUp={handleCenterTapRelease}
onTouchEnd={handleCenterTapRelease}
>

View File

@@ -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

View File

@@ -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);
});

View File

@@ -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)
}

View File

@@ -317,11 +317,17 @@ export default function PostPage() {
</div>
<div class="flex max-w-105 flex-wrap justify-center italic md:justify-start md:pl-24">
<For each={postData.tags as any[]}>
{(tag) => (
<div class="group relative m-1 h-fit w-fit rounded-xl bg-purple-600 px-2 py-1 text-sm">
<div class="text-white">{tag.value}</div>
</div>
)}
{(tag) => {
const tagValue = tag.value;
return tagValue ? (
<A
href={`/blog?include=${encodeURIComponent(tagValue.split("#")[1])}`}
class="group bg-rosewater relative m-1 h-fit w-fit rounded-xl px-2 py-1 text-sm transition-all duration-200 hover:brightness-110 active:scale-95"
>
<div class="text-white">{tagValue}</div>
</A>
) : null;
}}
</For>
</div>
</div>
@@ -333,7 +339,7 @@ export default function PostPage() {
<Fire
height={32}
width={32}
color="var(--color-text)"
color="var(--color-red)"
/>
</div>
<div class="text-text my-auto pt-0.5 pl-2 text-sm">
@@ -344,7 +350,14 @@ export default function PostPage() {
</div>
<a href="#comments" class="mx-2">
<div class="tooltip flex flex-col">
<button
onClick={() => {
document
.getElementById("comments")
?.scrollIntoView({ behavior: "smooth" });
}}
class="tooltip flex flex-col"
>
<div class="mx-auto hover:brightness-125">
<CommentIcon
strokeWidth={1}
@@ -358,7 +371,7 @@ export default function PostPage() {
? "Comment"
: "Comments"}
</div>
</div>
</button>
</a>
<div class="mx-2">

View File

@@ -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() {
<>
<Title>Blog | Michael Freno</Title>
<div class="mx-auto pt-8 pb-24">
<div class="mx-auto py-16 pb-24">
<Show when={data()} fallback={<TerminalSplash />}>
{(loadedData) => (
<>
@@ -118,6 +119,7 @@ export default function BlogIndex() {
privilegeLevel={loadedData().privilegeLevel}
filters={filters()}
sort={sort()}
include={include()}
/>
</div>
</Show>

View File

@@ -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 (
<>
<Title>Downloads | Michael Freno</Title>
@@ -40,12 +32,12 @@ export default function DownloadsPage() {
<br />
</div>
<div class="flex justify-evenly md:mx-[25vw]">
<div class="flex justify-evenly">
<div class="flex w-1/3 flex-col">
<div class="text-center text-lg">Android (apk only)</div>
<div class="text-center text-lg">Android</div>
<button
onClick={() => download("lineage")}
class="bg-blue mt-2 rounded-md px-4 py-2 text-base shadow-lg transition-all duration-200 ease-out hover:brightness-125 active:scale-95"
class="bg-blue mx-auto mt-2 rounded-md px-4 py-2 text-base shadow-lg transition-all duration-200 ease-out hover:brightness-125 active:scale-95"
>
Download APK
</button>
@@ -53,20 +45,6 @@ export default function DownloadsPage() {
Note the android version is not well tested, and has performance
issues.
</div>
<div class="rule-around">Or</div>
<div class="mx-auto italic">(Coming soon)</div>
<button
onClick={joinBetaPrompt}
class="mx-auto transition-all duration-200 ease-out active:scale-95"
>
<img
src="/google-play-badge.png"
alt="google-play"
width={180}
height={60}
/>
</button>
</div>
<div class="flex flex-col">
@@ -88,28 +66,15 @@ export default function DownloadsPage() {
(apk and iOS)
</div>
<div class="flex justify-evenly md:mx-[25vw]">
<div class="flex justify-evenly">
<div class="flex flex-col">
<div class="text-center text-lg">Android</div>
<button
onClick={() => download("shapes-with-abigail")}
class="bg-blue mt-2 rounded-md px-4 py-2 text-base shadow-lg transition-all duration-200 ease-out hover:brightness-125 active:scale-95"
class="bg-blue mx-auto mt-2 rounded-md px-4 py-2 text-base shadow-lg transition-all duration-200 ease-out hover:brightness-125 active:scale-95"
>
Download APK
</button>
<div class="rule-around">Or</div>
<div class="mx-auto italic">(Coming soon)</div>
<button
onClick={joinBetaPrompt}
class="transition-all duration-200 ease-out active:scale-95"
>
<img
src="/google-play-badge.png"
alt="google-play"
width={180}
height={60}
/>
</button>
</div>
<div class="flex flex-col">
@@ -142,33 +107,6 @@ export default function DownloadsPage() {
Just unzip and drag into 'Applications' folder
</div>
</div>
<ul class="icons flex justify-center gap-4 pt-24 pb-6">
<li>
<A
href="https://github.com/MikeFreno/"
target="_blank"
rel="noreferrer"
class="shaker border-text inline-block rounded-full border transition-transform hover:scale-110"
>
<span class="m-auto block p-2">
<GitHub height={24} width={24} fill={undefined} />
</span>
</A>
</li>
<li>
<A
href="https://www.linkedin.com/in/michael-freno-176001256/"
target="_blank"
rel="noreferrer"
class="shaker border-text inline-block rounded-full border transition-transform hover:scale-110"
>
<span class="m-auto block rounded-md p-2">
<LinkedIn height={24} width={24} fill={undefined} />
</span>
</A>
</li>
</ul>
</div>
</div>
</>

View File

@@ -11,7 +11,7 @@ export default function Home() {
content="Michael Freno - Software Engineer based in Brooklyn, NY"
/>
<main class="flex h-full flex-col gap-8 px-4 text-xl">
<main class="flex h-full flex-col gap-8 px-4 py-16 text-xl">
<div class="flex-1">
<Typewriter speed={30} keepAlive={2000}>
<div class="text-4xl">Hey!</div>
@@ -153,39 +153,40 @@ export default function Home() {
</div>
</div>
</div>
<Typewriter speed={120} class="mx-auto max-w-3/4 pt-8 md:max-w-1/2">
And if you love the color schemes of this site
<div class="mx-auto w-fit">
<DarkModeToggle />
<div class="flex justify-between">
<Typewriter speed={120} class="mx-auto max-w-3/4 pt-8 md:max-w-1/2">
And if you love the color schemes of this site
<div class="mx-auto w-fit">
<DarkModeToggle />
</div>
(which of course you do), you can see{" "}
<a
href="https://github.com/mikefreno/dots/blob/master/mac/nvim/lua/colors.lua"
class="text-blue hover-underline-animation"
>
here
</a>{" "}
- 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.
</Typewriter>
<div class="flex flex-col items-end justify-center gap-4 pr-4">
<Typewriter speed={30} keepAlive={false}>
<div>
My Collection of
<br />
By-the-ways:
</div>
</Typewriter>
<Typewriter speed={30} keepAlive={false}>
<ul class="list-disc">
<li>I use Neovim</li>
<li>I use Arch Linux</li>
<li>I use Rust</li>
</ul>
</Typewriter>
</div>
(which of course you do), you can see{" "}
<a
href="https://github.com/mikefreno/dots/blob/master/mac/nvim/lua/colors.lua"
class="text-blue hover-underline-animation"
>
here
</a>{" "}
- 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.
</Typewriter>
</div>
<div class="flex flex-col items-end gap-4 pr-4">
<Typewriter speed={50} keepAlive={false}>
<div>
My Collection of
<br />
By-the-ways:
</div>
</Typewriter>
<Typewriter speed={50} keepAlive={false}>
<ul class="list-disc">
<li>I use Neovim</li>
<li>I use Arch Linux</li>
<li>I use Rust</li>
</ul>
</Typewriter>
</div>
</div>
</main>
</>