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 }) {
");
@@ -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 */}
-
-
-
-
-
-
- {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 */}
+
+
+
-
-
-
-
- Edit
-
+
+
- >
- );
- }}
-
- )}
-
-
+
+ {/* 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 */}
+
+
+
+
+
+ Edit
+
+
+
+
+ {/* 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}
- fallback={No posts yet!}
- >
-
-
-
-
-
+ 0}
+ fallback={No posts yet!}
+ >
+
+
+
+
+ >
+ )}
+
>
);