import { Show, For, createEffect } from "solid-js"; import { useParams, A, Navigate, query, useSearchParams } from "@solidjs/router"; import { PageHead } from "~/components/PageHead"; import { createAsync } from "@solidjs/router"; import { getRequestEvent } from "solid-js/web"; import AuthenticatedLike from "~/components/blog/AuthenticatedLike"; 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 { Spinner } from "~/components/Spinner"; import { api } from "~/lib/api"; import CustomScrollbar from "~/components/CustomScrollbar"; import "../post.css"; import { Post } from "~/db/types"; const getPostByTitle = query( async ( title: string, sortBy: "newest" | "oldest" | "highest_rated" | "hot" = "newest" ) => { "use server"; const { getUserState } = await import("~/lib/auth-query"); const { ConnectionFactory } = await import("~/server/utils"); const { parseConditionals, getSafeEnvVariables } = await import("~/server/conditional-parser"); const { getFeatureFlags } = await import("~/server/feature-flags"); const event = getRequestEvent()!; const userState = await getUserState(); const isAuthenticated = userState.isAuthenticated; const isAdmin = userState.isAdmin; const userID = userState.userId; const conn = ConnectionFactory(); if (title === "by-id") { const url = new URL(event.request.url); const id = url.searchParams.get("id"); if (!id) { return { post: null, exists: false, comments: [], likes: [], tags: [], userCommentArray: [], reactionArray: [], isAuthenticated: false, isAdmin: false, userID: null }; } const idQuery = "SELECT title FROM Post WHERE id = ?"; const idResult = await conn.execute({ sql: idQuery, args: [id] }); const postData = idResult.rows[0] as any; if (postData?.title) { return { redirect: `/blog/${encodeURIComponent(postData.title)}${sortBy !== "newest" ? `?sortBy=${sortBy}` : ""}` }; } return { post: null, exists: false, comments: [], likes: [], tags: [], userCommentArray: [], reactionArray: [], isAuthenticated: false, isAdmin: false, userID: null }; } let query = "SELECT * FROM Post WHERE title = ?"; if (!isAdmin) { query += ` AND published = TRUE`; } const postResults = await conn.execute({ sql: query, args: [decodeURIComponent(title)] }); const post = postResults.rows[0] as any; if (!post) { const existQuery = "SELECT id FROM Post WHERE title = ?"; const existRes = await conn.execute({ sql: existQuery, args: [decodeURIComponent(title)] }); if (existRes.rows[0]) { return { post: null, exists: true, comments: [], likes: [], tags: [], userCommentArray: [], reactionArray: [], isAuthenticated: false, isAdmin: false, userID: null }; } return { post: null, exists: false, comments: [], likes: [], tags: [], userCommentArray: [], reactionArray: [], isAuthenticated: false, isAdmin: false, userID: null }; } const conditionalContext = { isAuthenticated: userID !== null, isAdmin: isAdmin, userId: userID, currentDate: new Date(), featureFlags: getFeatureFlags(), env: getSafeEnvVariables() }; if (post.body) { try { post.body = parseConditionals(post.body, conditionalContext); } catch (error) { console.error("Error parsing conditionals in post body:", error); } } let commentQuery = "SELECT * FROM Comment WHERE post_id = ?"; switch (sortBy) { case "newest": commentQuery += " ORDER BY date DESC"; break; case "oldest": commentQuery += " ORDER BY date ASC"; break; case "highest_rated": commentQuery = ` SELECT c.*, COALESCE(( SELECT COUNT(*) FROM CommentReaction WHERE comment_id = c.id AND type IN ('tears', 'heartEye', 'moneyEye') ), 0) - COALESCE(( SELECT COUNT(*) FROM CommentReaction WHERE comment_id = c.id AND type IN ('angry', 'sick', 'worried') ), 0) as net_score FROM Comment c WHERE c.post_id = ? ORDER BY net_score DESC, c.date DESC `; break; case "hot": commentQuery = ` SELECT c.*, (COALESCE(( SELECT COUNT(*) FROM CommentReaction WHERE comment_id = c.id AND type IN ('tears', 'heartEye', 'moneyEye') ), 0) - COALESCE(( SELECT COUNT(*) FROM CommentReaction WHERE comment_id = c.id AND type IN ('angry', 'sick', 'worried') ), 0)) / LOG10(((JULIANDAY('now') - JULIANDAY(c.date)) * 24) + 2) as hot_score FROM Comment c WHERE c.post_id = ? ORDER BY hot_score DESC, c.date DESC `; break; } const comments = ( await conn.execute({ sql: commentQuery, args: [post.id] }) ).rows; const likeQuery = "SELECT * FROM PostLike WHERE post_id = ?"; const likes = (await conn.execute({ sql: likeQuery, args: [post.id] })) .rows; const tagQuery = "SELECT * FROM Tag WHERE post_id = ?"; const tags = (await conn.execute({ sql: tagQuery, args: [post.id] })).rows; const commenterToCommentIDMap = new Map(); comments.forEach((comment: any) => { const prev = commenterToCommentIDMap.get(comment.commenter_id) || []; commenterToCommentIDMap.set(comment.commenter_id, [...prev, comment.id]); }); const commenterQuery = "SELECT email, display_name, image FROM User WHERE id = ?"; const userCommentArray: Array<[UserPublicData, number[]]> = []; for (const [key, value] of commenterToCommentIDMap.entries()) { const res = await conn.execute({ sql: commenterQuery, args: [key] }); const user = res.rows[0]; if (user) { userCommentArray.push([user as UserPublicData, value]); } } const reactionArray: Array<[number, CommentReaction[]]> = []; for (const comment of comments) { const reactionQuery = "SELECT * FROM CommentReaction WHERE comment_id = ?"; const res = await conn.execute({ sql: reactionQuery, args: [(comment as any).id] }); reactionArray.push([(comment as any).id, res.rows as CommentReaction[]]); } const topLevelComments = comments.filter( (c: any) => c.parent_comment_id == null ); return { post, exists: true, comments, likes, tags, topLevelComments, userCommentArray, reactionArray, isAuthenticated, isAdmin, userID, sortBy, reads: post.reads || 0 }; }, "post-by-title" ); export default function PostPage() { const params = useParams(); const [searchParams] = useSearchParams(); const data = createAsync( () => { const sortBy = (searchParams.sortBy as | "newest" | "oldest" | "highest_rated" | "hot") || "newest"; return getPostByTitle(params.title, sortBy); }, { deferStream: true } ); 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(""); }; return ( } > {(loadedData) => { if ("redirect" in loadedData()) { return ; } return ( } > {(p) => { const postData = loadedData(); const userCommentMap = new Map( postData.userCommentArray || [] ); const reactionMap = new Map( postData.reactionArray || [] ); return ( <>
post-cover
{p().title.replaceAll("_", " ")}
{p().subtitle}
Written {new Date(p().date).toDateString()}
Edited:{" "} {new Date( p().last_edited_date ).toDateString()}
By Michael Freno
{(tag) => { const tagValue = tag.value; return tagValue ? ( {tagValue} ) : null; }}
{p().title.replaceAll("_", " ")}
{p().subtitle}
); }}
); }}
); }