1
mirror of https://github.com/jakejarvis/jarv.is.git synced 2026-06-05 19:15:30 -04:00

refactor: eslint/prettier ➡️ biome

This commit is contained in:
2026-02-19 14:02:03 -05:00
parent 4858c8928c
commit c30197ccc5
115 changed files with 2584 additions and 5683 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
import { env } from "@/lib/env";
import { createAuthClient } from "better-auth/react";
import { env } from "@/lib/env";
export const authClient = createAuthClient({
baseURL: env.NEXT_PUBLIC_BASE_URL,
+3 -3
View File
@@ -1,9 +1,9 @@
import { env } from "@/lib/env";
import { betterAuth, type BetterAuthOptions } from "better-auth";
import { nextCookies } from "better-auth/next-js";
import { type BetterAuthOptions, betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { nextCookies } from "better-auth/next-js";
import { db } from "@/lib/db";
import * as schema from "@/lib/db/schema";
import { env } from "@/lib/env";
export const auth = betterAuth({
baseURL: env.NEXT_PUBLIC_BASE_URL,
+9 -7
View File
@@ -1,10 +1,9 @@
import { env } from "@/lib/env";
import { Feed, type Item as FeedItem } from "feed";
import { getFrontMatter, getContent } from "@/lib/posts";
import siteConfig from "@/lib/config/site";
import authorConfig from "@/lib/config/author";
import ogImage from "@/app/opengraph-image.jpg";
import authorConfig from "@/lib/config/author";
import siteConfig from "@/lib/config/site";
import { env } from "@/lib/env";
import { getContent, getFrontMatter } from "@/lib/posts";
/**
* Returns a `Feed` object, which can then be processed with `feed.rss2()`, `feed.atom1()`, or `feed.json1()`.
@@ -49,11 +48,14 @@ export const buildFeed = async (): Promise<Feed> => {
${await getContent(post.slug)}
<p><a href="${post.permalink}"><strong>Continue reading...</strong></a></p>
`.trim(),
}))
})),
);
// sort posts reverse chronologically in case the promises resolved out of order
posts.sort((post1, post2) => new Date(post2.date).getTime() - new Date(post1.date).getTime());
posts.sort(
(post1, post2) =>
new Date(post2.date).getTime() - new Date(post1.date).getTime(),
);
// officially add each post to the feed
posts.forEach((post) => {
+2 -2
View File
@@ -1,8 +1,8 @@
import { env } from "@/lib/env";
import * as schema from "@/lib/db/schema";
import { attachDatabasePool } from "@vercel/functions";
import { drizzle } from "drizzle-orm/node-postgres";
import { Pool } from "pg";
import * as schema from "@/lib/db/schema";
import { env } from "@/lib/env";
// Create explicit pool instance for better connection management
const pool = new Pool({
@@ -1,68 +0,0 @@
CREATE TABLE "account" (
"id" text PRIMARY KEY NOT NULL,
"account_id" text NOT NULL,
"provider_id" text NOT NULL,
"user_id" text NOT NULL,
"access_token" text,
"refresh_token" text,
"id_token" text,
"access_token_expires_at" timestamp,
"refresh_token_expires_at" timestamp,
"scope" text,
"password" text,
"created_at" timestamp NOT NULL,
"updated_at" timestamp NOT NULL
);
--> statement-breakpoint
CREATE TABLE "comment" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"content" text NOT NULL,
"page_slug" text NOT NULL,
"parent_id" uuid,
"user_id" text NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "page" (
"slug" text PRIMARY KEY NOT NULL,
"views" integer DEFAULT 1 NOT NULL
);
--> statement-breakpoint
CREATE TABLE "session" (
"id" text PRIMARY KEY NOT NULL,
"expires_at" timestamp NOT NULL,
"token" text NOT NULL,
"created_at" timestamp NOT NULL,
"updated_at" timestamp NOT NULL,
"ip_address" text,
"user_agent" text,
"user_id" text NOT NULL,
CONSTRAINT "session_token_unique" UNIQUE("token")
);
--> statement-breakpoint
CREATE TABLE "user" (
"id" text PRIMARY KEY NOT NULL,
"name" text NOT NULL,
"email" text NOT NULL,
"email_verified" boolean NOT NULL,
"image" text,
"created_at" timestamp NOT NULL,
"updated_at" timestamp NOT NULL,
CONSTRAINT "user_email_unique" UNIQUE("email")
);
--> statement-breakpoint
CREATE TABLE "verification" (
"id" text PRIMARY KEY NOT NULL,
"identifier" text NOT NULL,
"value" text NOT NULL,
"expires_at" timestamp NOT NULL,
"created_at" timestamp,
"updated_at" timestamp
);
--> statement-breakpoint
ALTER TABLE "account" ADD CONSTRAINT "account_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "comment" ADD CONSTRAINT "comment_page_slug_page_slug_fk" FOREIGN KEY ("page_slug") REFERENCES "public"."page"("slug") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "comment" ADD CONSTRAINT "comment_parent_id_comment_id_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."comment"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "comment" ADD CONSTRAINT "comment_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "session" ADD CONSTRAINT "session_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;
-443
View File
@@ -1,443 +0,0 @@
{
"id": "d126927d-35cf-4b6f-ab97-3872b8db26a7",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.account": {
"name": "account",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"account_id": {
"name": "account_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"provider_id": {
"name": "provider_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"access_token": {
"name": "access_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"refresh_token": {
"name": "refresh_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"id_token": {
"name": "id_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"access_token_expires_at": {
"name": "access_token_expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"refresh_token_expires_at": {
"name": "refresh_token_expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"scope": {
"name": "scope",
"type": "text",
"primaryKey": false,
"notNull": false
},
"password": {
"name": "password",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"account_user_id_user_id_fk": {
"name": "account_user_id_user_id_fk",
"tableFrom": "account",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.comment": {
"name": "comment",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"content": {
"name": "content",
"type": "text",
"primaryKey": false,
"notNull": true
},
"page_slug": {
"name": "page_slug",
"type": "text",
"primaryKey": false,
"notNull": true
},
"parent_id": {
"name": "parent_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"comment_page_slug_page_slug_fk": {
"name": "comment_page_slug_page_slug_fk",
"tableFrom": "comment",
"tableTo": "page",
"columnsFrom": [
"page_slug"
],
"columnsTo": [
"slug"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"comment_parent_id_comment_id_fk": {
"name": "comment_parent_id_comment_id_fk",
"tableFrom": "comment",
"tableTo": "comment",
"columnsFrom": [
"parent_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"comment_user_id_user_id_fk": {
"name": "comment_user_id_user_id_fk",
"tableFrom": "comment",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.page": {
"name": "page",
"schema": "",
"columns": {
"slug": {
"name": "slug",
"type": "text",
"primaryKey": true,
"notNull": true
},
"views": {
"name": "views",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 1
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.session": {
"name": "session",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"expires_at": {
"name": "expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"token": {
"name": "token",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"ip_address": {
"name": "ip_address",
"type": "text",
"primaryKey": false,
"notNull": false
},
"user_agent": {
"name": "user_agent",
"type": "text",
"primaryKey": false,
"notNull": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"session_user_id_user_id_fk": {
"name": "session_user_id_user_id_fk",
"tableFrom": "session",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"session_token_unique": {
"name": "session_token_unique",
"nullsNotDistinct": false,
"columns": [
"token"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.user": {
"name": "user",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true
},
"email_verified": {
"name": "email_verified",
"type": "boolean",
"primaryKey": false,
"notNull": true
},
"image": {
"name": "image",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"user_email_unique": {
"name": "user_email_unique",
"nullsNotDistinct": false,
"columns": [
"email"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.verification": {
"name": "verification",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"identifier": {
"name": "identifier",
"type": "text",
"primaryKey": false,
"notNull": true
},
"value": {
"name": "value",
"type": "text",
"primaryKey": false,
"notNull": true
},
"expires_at": {
"name": "expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}
-13
View File
@@ -1,13 +0,0 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1747229716675,
"tag": "0000_puzzling_sphinx",
"breakpoints": true
}
]
}
+12 -2
View File
@@ -1,4 +1,12 @@
import { pgTable, text, timestamp, boolean, integer, uuid, type AnyPgColumn } from "drizzle-orm/pg-core";
import {
type AnyPgColumn,
boolean,
integer,
pgTable,
text,
timestamp,
uuid,
} from "drizzle-orm/pg-core";
export const user = pgTable("user", {
id: text("id").primaryKey(),
@@ -61,7 +69,9 @@ export const comment = pgTable("comment", {
pageSlug: text("page_slug")
.notNull()
.references(() => page.slug),
parentId: uuid("parent_id").references((): AnyPgColumn => comment.id, { onDelete: "cascade" }),
parentId: uuid("parent_id").references((): AnyPgColumn => comment.id, {
onDelete: "cascade",
}),
userId: text("user_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
+2 -2
View File
@@ -89,7 +89,7 @@ export const env = createEnv({
? `${process.env.DEPLOY_URL}`
: undefined
: undefined) ||
`http://localhost:${process.env.PORT || 3000}`)()
`http://localhost:${process.env.PORT || 3000}`)(),
),
/**
@@ -102,7 +102,7 @@ export const env = createEnv({
(process.env.VERCEL && process.env.VERCEL_ENV === "production") ||
(process.env.NETLIFY && process.env.CONTEXT === "production")
? "production"
: "development")()
: "development")(),
),
/** Required. GitHub repository for the site in the format of `{username}/{repo}`. */
+1 -1
View File
@@ -2,9 +2,9 @@
// https://nextjs.org/docs/pages/building-your-application/optimizing/fonts#reusing-fonts
import {
Comic_Neue as ComicNeueLoader,
Inter as InterLoader,
JetBrains_Mono as JetBrainsMonoLoader,
Comic_Neue as ComicNeueLoader,
} from "next/font/google";
export const Inter = InterLoader({
+31 -29
View File
@@ -1,7 +1,7 @@
import { env } from "@/lib/env";
import siteConfig from "@/lib/config/site";
import authorConfig from "@/lib/config/author";
import type { Metadata } from "next";
import authorConfig from "@/lib/config/author";
import siteConfig from "@/lib/config/site";
import { env } from "@/lib/env";
export const defaultMetadata: Metadata = {
metadataBase: new URL(env.NEXT_PUBLIC_BASE_URL),
@@ -49,29 +49,31 @@ export const defaultMetadata: Metadata = {
* Helper function to deep merge a page's metadata into the default site metadata
* @see https://nextjs.org/docs/app/api-reference/functions/generate-metadata
*/
export const createMetadata = (metadata: Metadata & { canonical: string }): Metadata => {
return {
...defaultMetadata,
...metadata,
openGraph: {
...defaultMetadata.openGraph,
title: metadata.title!,
description: metadata.description!,
url: metadata.canonical,
...metadata.openGraph,
},
twitter: {
...defaultMetadata.twitter,
...metadata.twitter,
},
alternates: {
...defaultMetadata.alternates,
canonical: metadata.canonical,
...metadata.alternates,
},
other: {
...defaultMetadata.other,
...metadata.other,
},
};
};
export const createMetadata = (
metadata: Metadata & { canonical: string },
): Metadata => ({
...defaultMetadata,
...metadata,
openGraph: {
...defaultMetadata.openGraph,
// biome-ignore lint/style/noNonNullAssertion: title is always provided by callers
title: metadata.title!,
// biome-ignore lint/style/noNonNullAssertion: description is always provided by callers
description: metadata.description!,
url: metadata.canonical,
...metadata.openGraph,
},
twitter: {
...defaultMetadata.twitter,
...metadata.twitter,
},
alternates: {
...defaultMetadata.alternates,
canonical: metadata.canonical,
...metadata.alternates,
},
other: {
...defaultMetadata.other,
...metadata.other,
},
});
+7 -2
View File
@@ -2,7 +2,10 @@ import { cacheLife } from "next/cache";
// Load a Google Font from the Google Fonts API
// Adapted from https://github.com/brianlovin/briOS/blob/f72dc33a11194de45c80337b22be4560da62ad7e/src/lib/og-utils.tsx#L32
export async function loadGoogleFont(font: string, weight: number): Promise<ArrayBuffer> {
export async function loadGoogleFont(
font: string,
weight: number,
): Promise<ArrayBuffer> {
"use cache";
const url = `https://fonts.googleapis.com/css2?family=${font}:wght@${weight}`;
@@ -13,7 +16,9 @@ export async function loadGoogleFont(font: string, weight: number): Promise<Arra
},
});
const css = await cssResponse.text();
const resource = css.match(/src: url\((.+)\) format\('(opentype|truetype)'\)/);
const resource = css.match(
/src: url\((.+)\) format\('(opentype|truetype)'\)/,
);
if (resource) {
const fontResponse = await fetch(resource[1], {
+33 -14
View File
@@ -1,18 +1,18 @@
import { env } from "@/lib/env";
import path from "path";
import fs from "fs/promises";
import fs from "node:fs/promises";
import path from "node:path";
import glob from "fast-glob";
import { unified } from "unified";
import { decode } from "html-entities";
import { unified } from "unified";
import { env } from "@/lib/env";
import { rehypeSanitize, rehypeStringify } from "@/lib/rehype";
import {
remarkParse,
remarkSmartypants,
remarkFrontmatter,
remarkRehype,
remarkMdx,
remarkParse,
remarkRehype,
remarkSmartypants,
remarkStripMdxImportsExports,
} from "@/lib/remark";
import { rehypeSanitize, rehypeStringify } from "@/lib/rehype";
export type FrontMatter = {
slug: string;
@@ -40,7 +40,9 @@ export const getSlugs = async (): Promise<string[]> => {
});
// strip the .mdx extensions from filenames
const slugs = mdxFiles.map((fileName) => fileName.replace(/\/index\.mdx$/, ""));
const slugs = mdxFiles.map((fileName) =>
fileName.replace(/\/index\.mdx$/, ""),
);
return slugs;
};
@@ -90,7 +92,10 @@ export const getFrontMatter: {
permalink: `${env.NEXT_PUBLIC_BASE_URL}/${POSTS_DIR}/${slug}`,
} as FrontMatter;
} catch (error) {
console.error(`Failed to load front matter for post with slug "${slug}":`, error);
console.error(
`Failed to load front matter for post with slug "${slug}":`,
error,
);
return undefined;
}
}
@@ -104,7 +109,10 @@ export const getFrontMatter: {
const posts = allPosts.filter((p): p is FrontMatter => !!p);
// sort the results reverse chronologically and return
return posts.sort((post1, post2) => new Date(post2.date).getTime() - new Date(post1.date).getTime());
return posts.sort(
(post1, post2) =>
new Date(post2.date).getTime() - new Date(post1.date).getTime(),
);
}
throw new Error("getFrontMatter() called with invalid argument.");
@@ -145,12 +153,23 @@ export const getContent = async (slug: string): Promise<string | undefined> => {
],
})
.use(rehypeStringify)
.process(await fs.readFile(path.join(process.cwd(), `${POSTS_DIR}/${slug}/index.mdx`)));
.process(
await fs.readFile(
path.join(process.cwd(), `${POSTS_DIR}/${slug}/index.mdx`),
),
);
// convert the parsed content to a string with "safe" HTML
return content.toString().replaceAll("/* prettier-ignore */", "").replaceAll("<p></p>", "").trim();
return content
.toString()
.replaceAll("/* prettier-ignore */", "")
.replaceAll("<p></p>", "")
.trim();
} catch (error) {
console.error(`Failed to load/parse content for post with slug "${slug}":`, error);
console.error(
`Failed to load/parse content for post with slug "${slug}":`,
error,
);
return undefined;
}
};
+4 -1
View File
@@ -4,7 +4,10 @@ export const ContactSchema = z
.object({
name: z.string().trim().min(1, { message: "Your name is required." }),
email: z.string().email({ message: "Your email address is required." }),
message: z.string().trim().min(15, { message: "Your message must be at least 15 characters." }),
message: z
.string()
.trim()
.min(15, { message: "Your message must be at least 15 characters." }),
})
.readonly();
+29 -11
View File
@@ -1,18 +1,20 @@
"use server";
import { headers } from "next/headers";
import { revalidatePath } from "next/cache";
import { eq, desc, inArray, sql } from "drizzle-orm";
import { checkBotId } from "botid/server";
import { desc, eq, inArray, sql } from "drizzle-orm";
import { revalidatePath } from "next/cache";
import { headers } from "next/headers";
import { auth } from "@/lib/auth";
import { db } from "@/lib/db";
import * as schema from "@/lib/db/schema";
import { auth } from "@/lib/auth";
export type CommentWithUser = typeof schema.comment.$inferSelect & {
user: Pick<typeof schema.user.$inferSelect, "id" | "name" | "image">;
};
export const getComments = async (pageSlug: string): Promise<CommentWithUser[]> => {
export const getComments = async (
pageSlug: string,
): Promise<CommentWithUser[]> => {
try {
// Fetch all comments for the page with user details
const commentsWithUsers = await db
@@ -60,7 +62,9 @@ export const getCommentCount = async (slug: string): Promise<number> => {
/**
* Retrieves the numbers of comments for an array of slugs
*/
export const getCommentCountsForSlugs = async (slugs: string[]): Promise<Record<string, number>> => {
export const getCommentCountsForSlugs = async (
slugs: string[],
): Promise<Record<string, number>> => {
try {
const rows = await db
.select({
@@ -71,7 +75,9 @@ export const getCommentCountsForSlugs = async (slugs: string[]): Promise<Record<
.where(inArray(schema.comment.pageSlug, slugs))
.groupBy(schema.comment.pageSlug);
const map: Record<string, number> = Object.fromEntries(slugs.map((s) => [s, 0]));
const map: Record<string, number> = Object.fromEntries(
slugs.map((s) => [s, 0]),
);
for (const row of rows) {
map[row.pageSlug] = Number(row.count ?? 0);
}
@@ -85,7 +91,9 @@ export const getCommentCountsForSlugs = async (slugs: string[]): Promise<Record<
/**
* Retrieves the numbers of comments for ALL slugs
*/
export const getAllCommentCounts = async (): Promise<Record<string, number>> => {
export const getAllCommentCounts = async (): Promise<
Record<string, number>
> => {
try {
const rows = await db
.select({
@@ -106,7 +114,11 @@ export const getAllCommentCounts = async (): Promise<Record<string, number>> =>
}
};
export const createComment = async (data: { content: string; pageSlug: string; parentId?: string }) => {
export const createComment = async (data: {
content: string;
pageSlug: string;
parentId?: string;
}) => {
// BotID server-side verification
const verification = await checkBotId();
if (verification.isBot) {
@@ -158,7 +170,10 @@ export const updateComment = async (commentId: string, content: string) => {
try {
// Get the comment to verify ownership
const comment = await db
.select({ userId: schema.comment.userId, pageSlug: schema.comment.pageSlug })
.select({
userId: schema.comment.userId,
pageSlug: schema.comment.pageSlug,
})
.from(schema.comment)
.where(eq(schema.comment.id, commentId))
.then((results) => results[0]);
@@ -208,7 +223,10 @@ export const deleteComment = async (commentId: string) => {
try {
// Get the comment to verify ownership and get the page_slug for revalidation
const comment = await db
.select({ userId: schema.comment.userId, pageSlug: schema.comment.pageSlug })
.select({
userId: schema.comment.userId,
pageSlug: schema.comment.pageSlug,
})
.from(schema.comment)
.where(eq(schema.comment.id, commentId))
.then((results) => results[0]);
+12 -7
View File
@@ -1,10 +1,10 @@
"use server";
import { env } from "@/lib/env";
import { Resend } from "resend";
import { ContactSchema } from "@/lib/schemas/contact";
import siteConfig from "@/lib/config/site";
import { checkBotId } from "botid/server";
import { Resend } from "resend";
import siteConfig from "@/lib/config/site";
import { env } from "@/lib/env";
import { ContactSchema } from "@/lib/schemas/contact";
export type ContactResult = {
success: boolean;
@@ -12,7 +12,9 @@ export type ContactResult = {
errors?: Record<string, string[]>;
};
export const sendContactForm = async (formData: FormData): Promise<ContactResult> => {
export const sendContactForm = async (
formData: FormData,
): Promise<ContactResult> => {
// TODO: remove after debugging why automated spam entries are causing 500 errors
console.debug("[server/contact] received payload:", formData);
@@ -39,7 +41,9 @@ export const sendContactForm = async (formData: FormData): Promise<ContactResult
try {
if (env.RESEND_FROM_EMAIL === "onboarding@resend.dev") {
// https://resend.com/docs/api-reference/emails/send-email
console.warn("[server/contact] 'RESEND_FROM_EMAIL' is not set, falling back to onboarding@resend.dev.");
console.warn(
"[server/contact] 'RESEND_FROM_EMAIL' is not set, falling back to onboarding@resend.dev.",
);
}
const resend = new Resend(env.RESEND_API_KEY);
@@ -57,7 +61,8 @@ export const sendContactForm = async (formData: FormData): Promise<ContactResult
return {
success: false,
message: "Internal server error. Please try again later or shoot me an email.",
message:
"Internal server error. Please try again later or shoot me an email.",
};
}
};
+12 -4
View File
@@ -10,7 +10,11 @@ import { page } from "@/lib/db/schema";
*/
export const getViewCount = async (slug: string): Promise<number> => {
try {
const pages = await db.select().from(page).where(eq(page.slug, slug)).limit(1);
const pages = await db
.select()
.from(page)
.where(eq(page.slug, slug))
.limit(1);
return pages[0]?.views ?? 0;
} catch (error) {
console.error("[server/views] fatal error:", error);
@@ -21,10 +25,14 @@ export const getViewCount = async (slug: string): Promise<number> => {
/**
* Retrieves the numbers of views for an array of slugs, returning 0 for any that don't exist
*/
export const getViewCountsForSlugs = async (slugs: string[]): Promise<Record<string, number>> => {
export const getViewCountsForSlugs = async (
slugs: string[],
): Promise<Record<string, number>> => {
try {
const pages = await db.select().from(page).where(inArray(page.slug, slugs));
const viewMap: Record<string, number> = Object.fromEntries(slugs.map((s) => [s, 0]));
const viewMap: Record<string, number> = Object.fromEntries(
slugs.map((s) => [s, 0]),
);
for (const p of pages) {
viewMap[p.slug] = p.views;
}
@@ -46,7 +54,7 @@ export const getAllViewCounts = async (): Promise<Record<string, number>> => {
acc[p.slug] = p.views;
return acc;
},
{} as Record<string, number>
{} as Record<string, number>,
);
} catch (error) {
console.error("[server/views] fatal error:", error);
+1 -1
View File
@@ -1,4 +1,4 @@
import { clsx, type ClassValue } from "clsx";
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {