diff --git a/AGENTS.md b/AGENTS.md index 3f94bbd..1ebbc8c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,12 +1,6 @@ # Agent Guidelines for freno-dev -## Build/Lint/Test Commands -- **Dev**: `bun dev` (starts Vinxi dev server) -- **Build**: `bun build` (production build) -- **Start**: `bun start` (production server) -- **No tests configured** - No test runner or test scripts exist yet - -## Tech Stack +### Tech Stack - **Framework**: SolidJS with SolidStart (Vinxi) - **Routing**: @solidjs/router - **API**: tRPC v10 with Zod validation diff --git a/src/app.css b/src/app.css index 5b39aab..28fa877 100644 --- a/src/app.css +++ b/src/app.css @@ -259,6 +259,33 @@ body { /* Note: JS will add inline styles and reactive classList that override these defaults */ +/* Blog banner fallbacks - similar to main layout */ +.blog-banner-image { + /* Full width by default on mobile */ + width: 100vw; + margin-left: 0; +} + +.blog-banner-text { + /* Full width by default on mobile */ + width: 100vw; + margin-left: 0; +} + +@media (min-width: 768px) { + .blog-banner-image { + /* Account for sidebars on desktop */ + width: calc(100vw - 600px); + margin-left: 300px; + } + + .blog-banner-text { + /* Account for sidebars on desktop */ + width: calc(100vw - 600px); + margin-left: 300px; + } +} + .cursor-typing { display: inline-block; width: 2px; diff --git a/src/app.tsx b/src/app.tsx index 4bf476c..2129817 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -3,9 +3,9 @@ import { FileRoutes } from "@solidjs/start/router"; import { createEffect, ErrorBoundary, - Suspense, onMount, - onCleanup + onCleanup, + Suspense } from "solid-js"; import "./app.css"; import { LeftBar, RightBar } from "./components/Bars"; @@ -189,7 +189,7 @@ function AppLayout(props: { children: any }) {
- }>{props.children} + +
+ }>{props.children} +
diff --git a/src/entry-server.tsx b/src/entry-server.tsx index b65fcb4..401eff8 100644 --- a/src/entry-server.tsx +++ b/src/entry-server.tsx @@ -12,11 +12,6 @@ export default createHandler(() => ( {assets} -
{children}
{scripts} diff --git a/src/routes/blog/[title]/index.tsx b/src/routes/blog/[title]/index.tsx index a0274aa..7ffee8e 100644 --- a/src/routes/blog/[title]/index.tsx +++ b/src/routes/blog/[title]/index.tsx @@ -1,4 +1,4 @@ -import { Show, Suspense, For } from "solid-js"; +import { Show, For } from "solid-js"; import { useParams, A, @@ -222,13 +222,20 @@ const getPostByTitle = query( export default function PostPage() { const params = useParams(); const [searchParams] = useSearchParams(); + const { centerWidth, leftBarSize, barsInitialized } = useBars(); - const data = createAsync(() => { - const sortBy = - (searchParams.sortBy as "newest" | "oldest" | "highest_rated" | "hot") || - "newest"; - return getPostByTitle(params.title, sortBy); - }); + const data = createAsync( + () => { + const sortBy = + (searchParams.sortBy as + | "newest" + | "oldest" + | "highest_rated" + | "hot") || "newest"; + return getPostByTitle(params.title, sortBy); + }, + { deferStream: true } + ); const hasCodeBlock = (str: string): boolean => { return str.includes(""); @@ -236,164 +243,171 @@ export default function PostPage() { return ( <> - }> - - {(loadedData) => ( - }> - {(p) => { - const postData = loadedData(); + }> + {(loadedData) => ( + }> + {(p) => { + const postData = loadedData(); - // Convert arrays back to Maps for component - const userCommentMap = new Map( - postData.userCommentArray || [] - ); - const reactionMap = new Map( - postData.reactionArray || [] - ); - const { centerWidth, leftBarSize } = useBars(); + // Convert arrays back to Maps for component + const userCommentMap = new Map( + postData.userCommentArray || [] + ); + const reactionMap = new Map( + postData.reactionArray || [] + ); - return ( - <> - - {p().title.replaceAll("_", " ")} | Michael Freno - - + return ( + <> + + {p().title.replaceAll("_", " ")} | Michael Freno + + -
- {/* Fixed banner image background */} -
-
- post-cover -
-
-
- {p().title.replaceAll("_", " ")} -
- {p().subtitle} -
-
-
-
- - {/* Spacer to push content down */} -
- - {/* Content that slides over the fixed image */} -
-
-
-
-
- Written {new Date(p().date).toDateString()} -
- By Michael Freno -
-
-
- - {(tag) => ( -
-
{tag.value}
-
- )} -
-
-
- - -
- - {/* Post body */} - + {/* Fixed banner image background */} +
+
+ post-cover - - - +
+
+ {p().title.replaceAll("_", " ")} +
+ {p().subtitle}
- - - {/* Comments section */} -
-
- - ); - }} -
- )} - - + + {/* Spacer to push content down */} +
+ + {/* Content that slides over the fixed image */} +
+
+
+
+
+ Written {new Date(p().date).toDateString()} +
+ By Michael Freno +
+
+
+ + {(tag) => ( +
+
{tag.value}
+
+ )} +
+
+
+ + +
+ + {/* Post body */} + + + + + + + {/* Comments section */} +
+ +
+
+
+ + ); + }} + + )} + ); } diff --git a/src/routes/blog/index.tsx b/src/routes/blog/index.tsx index eee312f..19ffdb7 100644 --- a/src/routes/blog/index.tsx +++ b/src/routes/blog/index.tsx @@ -1,63 +1,129 @@ -import { Show, Suspense } from "solid-js"; -import { useSearchParams, A } from "@solidjs/router"; +import { Show } from "solid-js"; +import { useSearchParams, A, query } from "@solidjs/router"; import { Title } from "@solidjs/meta"; import { createAsync } from "@solidjs/router"; -import { api } from "~/lib/api"; +import { getRequestEvent } from "solid-js/web"; import PostSortingSelect from "~/components/blog/PostSortingSelect"; import TagSelector from "~/components/blog/TagSelector"; import PostSorting from "~/components/blog/PostSorting"; import { TerminalSplash } from "~/components/TerminalSplash"; +// Server function to fetch all posts +const getPosts = query(async () => { + "use server"; + const { ConnectionFactory, getPrivilegeLevel } = + await import("~/server/utils"); + const { withCache } = await import("~/server/cache"); + const event = getRequestEvent()!; + const privilegeLevel = await getPrivilegeLevel(event.nativeEvent); + + return withCache(`posts-${privilegeLevel}`, 5 * 60 * 1000, async () => { + const conn = ConnectionFactory(); + + // Fetch all posts with aggregated data + let postsQuery = ` + SELECT + p.id, + p.title, + p.subtitle, + p.body, + p.banner_photo, + p.date, + p.published, + p.category, + p.author_id, + p.reads, + p.attachments, + COUNT(DISTINCT pl.user_id) as total_likes, + COUNT(DISTINCT c.id) as total_comments + FROM Post p + LEFT JOIN PostLike pl ON p.id = pl.post_id + LEFT JOIN Comment c ON p.id = c.post_id + `; + + if (privilegeLevel !== "admin") { + postsQuery += ` WHERE p.published = TRUE`; + } + + postsQuery += ` GROUP BY p.id, p.title, p.subtitle, p.body, p.banner_photo, p.date, p.published, p.category, p.author_id, p.reads, p.attachments`; + postsQuery += ` ORDER BY p.date ASC;`; + + const postsResult = await conn.execute(postsQuery); + const posts = postsResult.rows; + + const tagsQuery = ` + SELECT t.value, t.post_id + FROM Tag t + JOIN Post p ON t.post_id = p.id + ${privilegeLevel !== "admin" ? "WHERE p.published = TRUE" : ""} + ORDER BY t.value ASC + `; + + const tagsResult = await conn.execute(tagsQuery); + const tags = tagsResult.rows; + + const tagMap: Record = {}; + tags.forEach((tag: any) => { + const key = `${tag.value}`; + tagMap[key] = (tagMap[key] || 0) + 1; + }); + + return { posts, tags, tagMap, privilegeLevel }; + }); +}, "posts"); + export default function BlogIndex() { const [searchParams] = useSearchParams(); const sort = () => searchParams.sort || "newest"; const filters = () => searchParams.filter || ""; - const data = createAsync(() => api.blog.getPosts.query()); + const data = createAsync(() => getPosts(), { deferStream: true }); return ( <> Blog | Michael Freno
- }> -
- + }> + {(loadedData) => ( + <> +
+ - 0}> - - + 0}> + + - - - -
- - }> - 0} - fallback={
No posts yet!
} - > -
- -
-
-
+ 0} + fallback={
No posts yet!
} + > +
+ +
+
+ + )} +
);