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:
+1
-1
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
@@ -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": {}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "7",
|
||||
"when": 1747229716675,
|
||||
"tag": "0000_puzzling_sphinx",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
+12
-2
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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,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
@@ -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
@@ -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
@@ -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
@@ -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[]) {
|
||||
|
||||
Reference in New Issue
Block a user