diff --git a/public/lineage-home.png b/public/lineage-home.png index 9140c85..90087fd 100644 Binary files a/public/lineage-home.png and b/public/lineage-home.png differ diff --git a/src/app.tsx b/src/app.tsx index d2f54d7..d1aac6f 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -160,6 +160,7 @@ function AppLayout(props: { children: any }) { const handleCenterTapRelease = (e: MouseEvent | TouchEvent) => { const isMobile = window.innerWidth < 768; + // Only hide left bar on mobile when it's visible if (isMobile && leftBarVisible()) { const target = e.target as HTMLElement; const isInteractive = target.closest( diff --git a/src/components/blog/CommentSection.tsx b/src/components/blog/CommentSection.tsx index 18dddb3..f12cc17 100644 --- a/src/components/blog/CommentSection.tsx +++ b/src/components/blog/CommentSection.tsx @@ -1,4 +1,5 @@ import { createSignal, Show } from "solid-js"; +import { useSearchParams } from "@solidjs/router"; import type { Comment, CommentReaction, @@ -42,8 +43,10 @@ interface CommentSectionProps { } export default function CommentSection(props: CommentSectionProps) { + const [searchParams] = useSearchParams(); + const [selectedSorting, setSelectedSorting] = createSignal( - COMMENT_SORTING_OPTIONS[0].val + (searchParams.sortBy as SortingMode) || COMMENT_SORTING_OPTIONS[0].val ); const hasComments = () => diff --git a/src/components/blog/CommentSorting.tsx b/src/components/blog/CommentSorting.tsx index 2e912b7..783be06 100644 --- a/src/components/blog/CommentSorting.tsx +++ b/src/components/blog/CommentSorting.tsx @@ -1,6 +1,5 @@ -import { createSignal, createEffect, For, Show, createMemo } from "solid-js"; +import { createSignal, createEffect, For, Show } from "solid-js"; import type { CommentSortingProps } from "~/types/comment"; -import { sortComments } from "~/lib/comment-utils"; import CommentBlock from "./CommentBlock"; export default function CommentSorting(props: CommentSortingProps) { @@ -35,17 +34,9 @@ export default function CommentSorting(props: CommentSortingProps) { } }; - // Memoized sorted comments - const sortedComments = createMemo(() => { - return sortComments( - props.topLevelComments, - props.selectedSorting.val, - props.reactionMap - ); - }); - + // Comments are already sorted from server, no need for client-side sorting return ( - + {(topLevelComment) => (
checkForDoubleClick(topLevelComment.id)} diff --git a/src/components/blog/CommentSortingSelect.tsx b/src/components/blog/CommentSortingSelect.tsx index c0b5bd8..623386e 100644 --- a/src/components/blog/CommentSortingSelect.tsx +++ b/src/components/blog/CommentSortingSelect.tsx @@ -1,4 +1,5 @@ import { For, Show, createSignal } from "solid-js"; +import { useNavigate, useLocation } from "@solidjs/router"; import type { CommentSortingSelectProps, SortingMode } from "~/types/comment"; import Check from "~/components/icons/Check"; import UpDownArrows from "~/components/icons/UpDownArrows"; @@ -12,6 +13,8 @@ const SORTING_OPTIONS: { val: SortingMode; label: string }[] = [ export default function CommentSortingSelect(props: CommentSortingSelectProps) { const [isOpen, setIsOpen] = createSignal(false); + const navigate = useNavigate(); + const location = useLocation(); const selectedLabel = () => { const option = SORTING_OPTIONS.find( @@ -23,6 +26,14 @@ export default function CommentSortingSelect(props: CommentSortingSelectProps) { const handleSelect = (mode: SortingMode) => { props.setSorting(mode); setIsOpen(false); + + // Update URL with sortBy parameter + const url = new URL(window.location.href); + url.searchParams.set("sortBy", mode); + navigate(`${location.pathname}?${url.searchParams.toString()}#comments`, { + scroll: false, + replace: true + }); }; return ( diff --git a/src/components/blog/PostSorting.tsx b/src/components/blog/PostSorting.tsx index c526e3e..376bb9c 100644 --- a/src/components/blog/PostSorting.tsx +++ b/src/components/blog/PostSorting.tsx @@ -1,31 +1,128 @@ -import { For, Show } from "solid-js"; +import { For, Show, createMemo } from "solid-js"; import Card, { Post } from "./Card"; +export interface Tag { + value: string; + post_id: number; +} + export interface PostSortingProps { posts: Post[]; + tags: Tag[]; privilegeLevel: "anonymous" | "admin" | "user"; + filters?: string; + sort?: string; } -/** - * PostSorting Component - * - * Note: This component has been simplified - filtering and sorting - * are now handled server-side via the blog.getPosts tRPC query. - * - * This component now only renders the posts that have already been - * filtered and sorted by the server. - */ export default function PostSorting(props: PostSortingProps) { + // Build set of tags that are ALLOWED (not filtered out) + const allowedTags = createMemo(() => { + 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(); + allTags.forEach((tag) => { + if (!filteredOutTags.has(tag)) { + allowed.add(tag); + } + }); + + return allowed; + }); + + // Get posts that have at least one allowed tag + const filteredPosts = createMemo(() => { + const allowed = allowedTags(); + + // If all tags are allowed, show all posts + if ( + allowed.size === + props.tags + .map((t) => t.value.slice(1)) + .filter((v, i, a) => a.indexOf(v) === i).length + ) { + return props.posts; + } + + // 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; + }); + }); + + const sortedPosts = createMemo(() => { + let sorted = [...filteredPosts()]; + + switch (props.sort) { + case "newest": + sorted.reverse(); // Posts come oldest first from DB + break; + case "oldest": + // Already in oldest order from DB + break; + case "most_liked": + sorted.sort((a, b) => (b.total_likes || 0) - (a.total_likes || 0)); + break; + case "most_read": + sorted.sort((a, b) => (b.reads || 0) - (a.reads || 0)); + break; + case "most_comments": + sorted.sort( + (a, b) => (b.total_comments || 0) - (a.total_comments || 0) + ); + break; + default: + sorted.reverse(); // Default to newest + } + + return sorted; + }); + return ( 0} + when={sortedPosts().length > 0} fallback={ -
- No posts found! -
+ 0} + fallback={ +
+ No posts found! +
+ } + > +
+ All posts filtered out! +
+
} > - + {(post) => (
diff --git a/src/components/blog/TagSelector.tsx b/src/components/blog/TagSelector.tsx index 1268b79..c649865 100644 --- a/src/components/blog/TagSelector.tsx +++ b/src/components/blog/TagSelector.tsx @@ -65,8 +65,17 @@ 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("|") + "|"; + navigate(`${location.pathname}?sort=${currentSort()}&filter=${allTags}`); + }; + return ( - <> +
+
{([key, value]) => (
@@ -98,6 +116,6 @@ export default function TagSelector(props: TagSelectorProps) {
- +
); } diff --git a/src/lib/SOLID-PATTERNS.md b/src/lib/SOLID-PATTERNS.md deleted file mode 100644 index 177a0fe..0000000 --- a/src/lib/SOLID-PATTERNS.md +++ /dev/null @@ -1,441 +0,0 @@ -# React to SolidJS Conversion Patterns - -This guide documents common patterns for converting React code to SolidJS for this migration. - -## Table of Contents -- [State Management](#state-management) -- [Effects](#effects) -- [Refs](#refs) -- [Routing](#routing) -- [Conditional Rendering](#conditional-rendering) -- [Lists](#lists) -- [Forms](#forms) -- [Event Handlers](#event-handlers) - -## State Management - -### React (useState) -```tsx -import { useState } from "react"; - -const [count, setCount] = useState(0); -const [user, setUser] = useState(null); - -// Update -setCount(count + 1); -setCount(prev => prev + 1); -setUser({ ...user, name: "John" }); -``` - -### Solid (createSignal) -```tsx -import { createSignal } from "solid-js"; - -const [count, setCount] = createSignal(0); -const [user, setUser] = createSignal(null); - -// Update - note the function call to read value -setCount(count() + 1); -setCount(prev => prev + 1); -setUser({ ...user(), name: "John" }); - -// ⚠️ Important: Always call the signal to read its value -console.log(count()); // ✅ Correct -console.log(count); // ❌ Wrong - this is the function itself -``` - -## Effects - -### React (useEffect) -```tsx -import { useEffect } from "react"; - -// Run once on mount -useEffect(() => { - console.log("Mounted"); - - return () => { - console.log("Cleanup"); - }; -}, []); - -// Run when dependency changes -useEffect(() => { - console.log(count); -}, [count]); -``` - -### Solid (createEffect / onMount / onCleanup) -```tsx -import { createEffect, onMount, onCleanup } from "solid-js"; - -// Run once on mount -onMount(() => { - console.log("Mounted"); -}); - -// Cleanup -onCleanup(() => { - console.log("Cleanup"); -}); - -// Run when dependency changes (automatic tracking) -createEffect(() => { - console.log(count()); // Automatically tracks count signal -}); - -// ⚠️ Important: Effects automatically track any signal reads -// No dependency array needed! -``` - -## Refs - -### React (useRef) -```tsx -import { useRef } from "react"; - -const inputRef = useRef(null); - -// Access -inputRef.current?.focus(); - -// In JSX - -``` - -### Solid (let binding or signal) -```tsx -// Method 1: Direct binding (preferred for simple cases) -let inputRef: HTMLInputElement | undefined; - -// Access -inputRef?.focus(); - -// In JSX - - -// Method 2: Using a signal (for reactive refs) -import { createSignal } from "solid-js"; - -const [inputRef, setInputRef] = createSignal(); - -// Access -inputRef()?.focus(); - -// In JSX - -``` - -## Routing - -### React (Next.js) -```tsx -import Link from "next/link"; -import { useRouter } from "next/navigation"; - -// Link component -About - -// Programmatic navigation -const router = useRouter(); -router.push("/dashboard"); -router.back(); -router.refresh(); -``` - -### Solid (SolidStart) -```tsx -import { A, useNavigate } from "@solidjs/router"; - -// Link component -About - -// Programmatic navigation -const navigate = useNavigate(); -navigate("/dashboard"); -navigate(-1); // Go back -// Note: No refresh() - Solid is reactive by default -``` - -## Conditional Rendering - -### React -```tsx -// Using && operator -{isLoggedIn && } - -// Using ternary -{isLoggedIn ? : } - -// Using if statement -if (loading) return ; -return ; -``` - -### Solid -```tsx -import { Show } from "solid-js"; - -// Using Show component (recommended) - - - - -// With fallback -}> - - - -// ⚠️ Important: Can still use && and ternary, but Show is more efficient -// because it doesn't recreate the DOM on every change - -// Early return still works -if (loading()) return ; -return ; -``` - -## Lists - -### React -```tsx -// Using map -{users.map(user => ( -
{user.name}
-))} - -// With index -{users.map((user, index) => ( -
{user.name}
-))} -``` - -### Solid -```tsx -import { For, Index } from "solid-js"; - -// Using For (when items have stable keys) - - {(user) =>
{user.name}
} -
- -// With index - - {(user, index) =>
{index()} - {user.name}
} -
- -// Using Index (when items have no stable identity, keyed by index) - - {(user, index) =>
{index} - {user().name}
} -
- -// ⚠️ Key differences: -// - For: Better when items have stable identity (keyed by reference) -// - Index: Better when items change frequently (keyed by index) -// - Note the () on user in Index component -``` - -## Forms - -### React -```tsx -import { useState } from "react"; - -const [email, setEmail] = useState(""); - -const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - console.log(email); -}; - -
- setEmail(e.target.value)} - /> -
-``` - -### Solid -```tsx -import { createSignal } from "solid-js"; - -const [email, setEmail] = createSignal(""); - -const handleSubmit = (e: Event) => { - e.preventDefault(); - console.log(email()); -}; - -
- setEmail(e.currentTarget.value)} - /> -
- -// ⚠️ Important differences: -// - Use onInput instead of onChange for real-time updates -// - onChange fires on blur in Solid -// - Use e.currentTarget instead of e.target for type safety -``` - -## Event Handlers - -### React -```tsx -// Inline -