better
This commit is contained in:
@@ -173,7 +173,6 @@ function AppLayout(props: { children: any }) {
|
||||
</div>
|
||||
</noscript>
|
||||
<div
|
||||
class="py-16"
|
||||
onMouseUp={handleCenterTapRelease}
|
||||
onTouchEnd={handleCenterTapRelease}
|
||||
>
|
||||
|
||||
@@ -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,9 +93,31 @@ export default function TagSelector(props: TagSelectorProps) {
|
||||
setShowingMenu(!showingMenu());
|
||||
};
|
||||
|
||||
const handleCheck = (filter: string, isChecked: boolean) => {
|
||||
const handleCheck = (tag: string, isChecked: boolean) => {
|
||||
if (filterMode() === "whitelist") {
|
||||
// Whitelist mode: manage include param
|
||||
let newInclude: string[];
|
||||
if (isChecked) {
|
||||
const newFilters = searchParams.filter?.replace(filter + "|", "");
|
||||
// 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()}&include=${includeStr}`
|
||||
);
|
||||
} else {
|
||||
// If no tags selected, clear whitelist
|
||||
navigate(`${location.pathname}?sort=${currentSort()}`);
|
||||
}
|
||||
} else {
|
||||
// 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}`
|
||||
@@ -77,27 +128,50 @@ export default function TagSelector(props: TagSelectorProps) {
|
||||
} else {
|
||||
const currentFiltersStr = searchParams.filter;
|
||||
if (currentFiltersStr) {
|
||||
const newFilters = currentFiltersStr + filter + "|";
|
||||
const newFilters = currentFiltersStr + tag + "|";
|
||||
navigate(
|
||||
`${location.pathname}?sort=${currentSort()}&filter=${newFilters}`
|
||||
);
|
||||
} else {
|
||||
navigate(
|
||||
`${location.pathname}?sort=${currentSort()}&filter=${filter}|`
|
||||
);
|
||||
navigate(`${location.pathname}?sort=${currentSort()}&filter=${tag}|`);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleAll = () => {
|
||||
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 {
|
||||
if (allChecked()) {
|
||||
// Uncheck all: Build filter string with all tags
|
||||
const allTags = allTagKeys().join("|") + "|";
|
||||
navigate(`${location.pathname}?sort=${currentSort()}&filter=${allTags}`);
|
||||
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 (
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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,6 +153,7 @@ export default function Home() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
@@ -165,21 +166,19 @@ export default function Home() {
|
||||
>
|
||||
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™.
|
||||
- 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 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={50} keepAlive={false}>
|
||||
<Typewriter speed={30} keepAlive={false}>
|
||||
<ul class="list-disc">
|
||||
<li>I use Neovim</li>
|
||||
<li>I use Arch Linux</li>
|
||||
@@ -187,6 +186,8 @@ export default function Home() {
|
||||
</ul>
|
||||
</Typewriter>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user