filtering cleanup, hits rendered

This commit is contained in:
Michael Freno
2025-12-23 01:02:55 -05:00
parent dc8111e7b6
commit 9cc682bda4
4 changed files with 133 additions and 15 deletions

View File

@@ -1,4 +1,11 @@
import { createSignal, createEffect, For, Show, onCleanup } from "solid-js";
import {
createSignal,
createEffect,
createMemo,
For,
Show,
onCleanup
} from "solid-js";
import { useNavigate, useLocation, useSearchParams } from "@solidjs/router";
export interface TagSelectorProps {
@@ -7,6 +14,7 @@ export interface TagSelectorProps {
export default function TagSelector(props: TagSelectorProps) {
const [showingMenu, setShowingMenu] = createSignal(false);
const [showingRareTags, setShowingRareTags] = createSignal(false);
let buttonRef: HTMLButtonElement | undefined;
let menuRef: HTMLDivElement | undefined;
const navigate = useNavigate();
@@ -16,6 +24,22 @@ export default function TagSelector(props: TagSelectorProps) {
const currentSort = () => searchParams.sort || "";
const currentFilters = () => searchParams.filter?.split("|") || [];
const frequentTags = createMemo(() =>
Object.entries(props.tagMap).filter(([_, count]) => count > 1)
);
const rareTags = createMemo(() =>
Object.entries(props.tagMap).filter(([_, count]) => count <= 1)
);
const allTagKeys = createMemo(() =>
Object.keys(props.tagMap).map((key) => key.slice(1))
);
const allChecked = createMemo(() =>
allTagKeys().every((tag) => !currentFilters().includes(tag))
);
const handleClickOutside = (e: MouseEvent) => {
if (
buttonRef &&
@@ -65,13 +89,15 @@ export default function TagSelector(props: TagSelectorProps) {
}
};
const handleUncheckAll = () => {
// Build filter string with all tags
const allTags =
Object.keys(props.tagMap)
.map((key) => key.slice(1))
.join("|") + "|";
const handleToggleAll = () => {
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()}`);
}
};
return (
@@ -92,13 +118,13 @@ export default function TagSelector(props: TagSelectorProps) {
<div class="border-overlay0 mb-2 flex justify-center border-b pb-2">
<button
type="button"
onClick={handleUncheckAll}
onClick={handleToggleAll}
class="text-text hover:text-red text-xs font-medium underline"
>
Uncheck All
{allChecked() ? "Uncheck All" : "Check All"}
</button>
</div>
<For each={Object.entries(props.tagMap)}>
<For each={frequentTags()}>
{([key, value]) => (
<div class="mx-auto my-2 flex">
<input
@@ -114,6 +140,35 @@ export default function TagSelector(props: TagSelectorProps) {
</div>
)}
</For>
<Show when={rareTags().length > 0}>
<div class="border-overlay0 mt-2 border-t pt-2">
<button
type="button"
onClick={() => setShowingRareTags(!showingRareTags())}
class="text-subtext0 hover:text-text mb-1 w-full text-left text-xs font-medium"
>
{showingRareTags() ? "▼" : "▶"} Rare tags ({rareTags().length})
</button>
<Show when={showingRareTags()}>
<For each={rareTags()}>
{([key, value]) => (
<div class="mx-auto my-2 flex">
<input
type="checkbox"
checked={!currentFilters().includes(key.slice(1))}
onChange={(e) =>
handleCheck(key.slice(1), e.currentTarget.checked)
}
/>
<div class="-mt-0.5 pl-1 text-sm font-normal">
{`${key.slice(1)} (${value}) `}
</div>
</div>
)}
</For>
</Show>
</div>
</Show>
</div>
</Show>
</div>

View File

@@ -0,0 +1,19 @@
export const Fire = ({
color = "#ea580c",
height = 120,
width = 120,
secondaryOpacity = 0.3,
...props
}) => (
<svg viewBox="0 0 384 512" {...props} height={height} width={width}>
<path
d="M216 23.859C216 23.857 215.998 23.854 215.998 23.852C215.998 23.848 215.996 23.848 215.996 23.844C215.988 9.082 204.184 0.029 192 0C189.076 -0.008 186.209 0.959 183.398 2.037C183.393 2.039 183.385 2.039 183.379 2.041H183.375H183.373H183.371C183.143 2.133 182.9 2.162 182.67 2.215C182.904 2.17 183.145 2.135 183.369 2.041C179.029 3.711 174.949 6.275 171.844 10.813C48 191.844 224 200 224 288C224 323.625 194.875 352.453 159.156 351.984C123.969 351.547 96 322.219 96 287.047V201.547C96 179.844 69.531 169.313 54.562 185.047C27.812 213.156 0 261.328 0 320C0 425.875 86.125 512 192 512S384 425.875 384 320C384 149.703 216 127 216 23.859ZM54.586 185.033C54.578 185.041 54.57 185.043 54.562 185.051C54.572 185.041 54.582 185.037 54.592 185.027L54.586 185.033ZM192 448.012C148.307 448.012 109.654 426.004 86.549 392.49C107.936 407.699 133.316 416 160.016 416C230.586 416 288 358.584 288 288.008C288 230.084 251.104 194.232 224.164 168.057C238.865 177.807 320 230.459 320 320.008C320 390.59 262.58 448.012 192 448.012Z"
fill={color}
opacity={secondaryOpacity}
/>
<path
d="M192.451 448.012C148.758 448.012 110.105 426.004 87 392.49C108.386 407.699 133.768 416 160.467 416C231.037 416 288.451 358.584 288.451 288.008C288.451 230.084 251.555 194.232 224.615 168.057C239.316 177.807 320.451 230.459 320.451 320.008C320.451 390.59 263.031 448.012 192.451 448.012Z"
fill={color}
/>
</svg>
);

View File

@@ -1,4 +1,4 @@
import { Show, For } from "solid-js";
import { Show, For, createEffect } from "solid-js";
import {
useParams,
A,
@@ -11,10 +11,12 @@ import { createAsync } from "@solidjs/router";
import { getRequestEvent } from "solid-js/web";
import SessionDependantLike from "~/components/blog/SessionDependantLike";
import CommentIcon from "~/components/icons/CommentIcon";
import { Fire } from "~/components/icons/Fire";
import CommentSectionWrapper from "~/components/blog/CommentSectionWrapper";
import PostBodyClient from "~/components/blog/PostBodyClient";
import type { Comment, CommentReaction, UserPublicData } from "~/types/comment";
import { TerminalSplash } from "~/components/TerminalSplash";
import { api } from "~/lib/api";
// Server function to fetch post by title
const getPostByTitle = query(
@@ -90,7 +92,6 @@ const getPostByTitle = query(
env: getSafeEnvVariables()
};
// Parse conditionals in post body
if (post.body) {
try {
post.body = parseConditionals(post.body, conditionalContext);
@@ -212,7 +213,8 @@ const getPostByTitle = query(
reactionArray,
privilegeLevel,
userID,
sortBy
sortBy,
reads: post.reads || 0
};
},
"post-by-title"
@@ -235,6 +237,18 @@ export default function PostPage() {
{ deferStream: true }
);
// Increment read count when post loads
createEffect(() => {
const postData = data();
if (postData?.post?.id) {
api.blog.incrementPostRead
.mutate({ postId: postData.post.id })
.catch((err) => {
console.error("Failed to increment read count:", err);
});
}
});
const hasCodeBlock = (str: string): boolean => {
return str.includes("<code") && str.includes("</code>");
};
@@ -313,6 +327,22 @@ export default function PostPage() {
</div>
<div class="flex flex-row justify-center pt-4 md:pt-0 md:pr-8">
<div class="mx-2">
<div class="tooltip flex flex-col">
<div class="mx-auto">
<Fire
height={32}
width={32}
color="var(--color-text)"
/>
</div>
<div class="text-text my-auto pt-0.5 pl-2 text-sm">
{postData.reads || 0}{" "}
{postData.reads === 1 ? "Hit" : "Hits"}
</div>
</div>
</div>
<a href="#comments" class="mx-2">
<div class="tooltip flex flex-col">
<div class="mx-auto hover:brightness-125">

View File

@@ -1,6 +1,7 @@
import { createTRPCRouter, publicProcedure } from "../utils";
import { ConnectionFactory } from "~/server/utils";
import { withCacheAndStale } from "~/server/cache";
import { z } from "zod";
const BLOG_CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours
@@ -98,5 +99,18 @@ export const blogRouter = createTRPCRouter({
return { posts, tags, tagMap, privilegeLevel };
}
);
}),
incrementPostRead: publicProcedure
.input(z.object({ postId: z.number() }))
.mutation(async ({ input }) => {
const conn = ConnectionFactory();
await conn.execute({
sql: "UPDATE Post SET reads = reads + 1 WHERE id = ?",
args: [input.postId]
});
return { success: true };
})
});