slop
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import type { inferRouterOutputs } from "@trpc/server";
|
||||
import { router } from "../trpc";
|
||||
import type { inferReactQueryProcedureOptions } from "@trpc/react-query";
|
||||
import { blogRouter } from "./blog";
|
||||
import { projectRouter } from "./project";
|
||||
import { techStackRouter } from "./techStack";
|
||||
import { cvCategoryRouter } from "./cvCategory";
|
||||
@@ -11,6 +12,7 @@ import { cvCategory } from "../dbschema/schema";
|
||||
import { chatRouter } from "./chat";
|
||||
|
||||
export const trpcRouter = router({
|
||||
blog: blogRouter,
|
||||
project: trpcCrudRouterFromDrizzleEntity('project').router,
|
||||
projectv2: projectRouter,
|
||||
techStack: trpcCrudRouterFromDrizzleEntity('techStack').router,
|
||||
|
||||
142
src/server/routers/blog.ts
Normal file
142
src/server/routers/blog.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import matter from "gray-matter";
|
||||
import { UTApi } from "uploadthing/server";
|
||||
import z from "zod";
|
||||
import { env } from "~/env.js";
|
||||
import { publicProcedure, router } from "~/server/trpc";
|
||||
|
||||
const utapi = new UTApi({ token: env.UPLOADTHING_TOKEN });
|
||||
|
||||
function fileToSlug(name: string, folder: string): string {
|
||||
const prefix = folder.endsWith("/") ? folder : `${folder}/`;
|
||||
const withoutFolder = name.startsWith(prefix) ? name.slice(prefix.length) : name;
|
||||
return withoutFolder.replace(/\.mdx?$/, "");
|
||||
}
|
||||
|
||||
function fileUrl(key: string): string {
|
||||
return `https://utfs.io/f/${key}`;
|
||||
}
|
||||
|
||||
async function fetchMdx(key: string): Promise<string> {
|
||||
const res = await fetch(fileUrl(key), { next: { revalidate: 3600 } });
|
||||
if (!res.ok) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Failed to fetch MDX file" });
|
||||
return res.text();
|
||||
}
|
||||
|
||||
async function getBlogFiles() {
|
||||
const folder = env.BLOG_MDX_FOLDER;
|
||||
const { files } = await utapi.listFiles();
|
||||
return files.filter(
|
||||
(f) => f.name.startsWith(`${folder}/`) && /\.mdx?$/.test(f.name),
|
||||
);
|
||||
}
|
||||
|
||||
export const blogRouter = router({
|
||||
insert: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
slug: z.string().min(1),
|
||||
title: z.string().min(1),
|
||||
date: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
content: z.string(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
const folder = env.BLOG_MDX_FOLDER;
|
||||
const frontmatter: Record<string, unknown> = { title: input.title };
|
||||
if (input.date) frontmatter.date = input.date;
|
||||
if (input.description) frontmatter.description = input.description;
|
||||
const mdxContent = matter.stringify(input.content, frontmatter);
|
||||
const file = new File([mdxContent], `${folder}/${input.slug}.mdx`, { type: "text/plain" });
|
||||
const result = await utapi.uploadFiles(file);
|
||||
if (result.error) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: result.error.message });
|
||||
return [{ slug: input.slug, title: input.title, date: input.date, description: input.description }];
|
||||
}),
|
||||
|
||||
update: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
slug: z.string().min(1),
|
||||
originalSlug: z.string().min(1),
|
||||
title: z.string().min(1),
|
||||
date: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
content: z.string(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
const folder = env.BLOG_MDX_FOLDER;
|
||||
const files = await getBlogFiles();
|
||||
const old = files.find(
|
||||
(f) => f.name === `${folder}/${input.originalSlug}.mdx` || f.name === `${folder}/${input.originalSlug}.md`,
|
||||
);
|
||||
if (old) await utapi.deleteFiles(old.key);
|
||||
const frontmatter: Record<string, unknown> = { title: input.title };
|
||||
if (input.date) frontmatter.date = input.date;
|
||||
if (input.description) frontmatter.description = input.description;
|
||||
const mdxContent = matter.stringify(input.content, frontmatter);
|
||||
const file = new File([mdxContent], `${folder}/${input.slug}.mdx`, { type: "text/plain" });
|
||||
const result = await utapi.uploadFiles(file);
|
||||
if (result.error) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: result.error.message });
|
||||
return [{ slug: input.slug, title: input.title, date: input.date, description: input.description, content: input.content }];
|
||||
}),
|
||||
|
||||
delete: publicProcedure
|
||||
.input(z.object({ id: z.string() }))
|
||||
.mutation(async ({ input }) => {
|
||||
const folder = env.BLOG_MDX_FOLDER;
|
||||
const files = await getBlogFiles();
|
||||
const file = files.find(
|
||||
(f) => f.name === `${folder}/${input.id}.mdx` || f.name === `${folder}/${input.id}.md`,
|
||||
);
|
||||
if (!file) throw new TRPCError({ code: "NOT_FOUND", message: `Post "${input.id}" not found` });
|
||||
await utapi.deleteFiles(file.key);
|
||||
return [];
|
||||
}),
|
||||
|
||||
list: publicProcedure.query(async () => {
|
||||
const folder = env.BLOG_MDX_FOLDER;
|
||||
const files = await getBlogFiles();
|
||||
|
||||
const posts = await Promise.all(
|
||||
files.map(async (file) => {
|
||||
const raw = await fetchMdx(file.key);
|
||||
const { data } = matter(raw);
|
||||
return {
|
||||
slug: fileToSlug(file.name, folder),
|
||||
title: (data.title as string | undefined) ?? fileToSlug(file.name, folder),
|
||||
date: data.date as string | undefined,
|
||||
description: data.description as string | undefined,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
return posts.sort((a, b) => {
|
||||
if (!a.date || !b.date) return 0;
|
||||
return new Date(b.date).getTime() - new Date(a.date).getTime();
|
||||
});
|
||||
}),
|
||||
|
||||
bySlug: publicProcedure.input(z.string()).query(async ({ input: slug }) => {
|
||||
const folder = env.BLOG_MDX_FOLDER;
|
||||
const files = await getBlogFiles();
|
||||
|
||||
const file = files.find(
|
||||
(f) => f.name === `${folder}/${slug}.mdx` || f.name === `${folder}/${slug}.md`,
|
||||
);
|
||||
|
||||
if (!file) throw new TRPCError({ code: "NOT_FOUND", message: `Post "${slug}" not found` });
|
||||
|
||||
const raw = await fetchMdx(file.key);
|
||||
const { content, data } = matter(raw);
|
||||
|
||||
return {
|
||||
slug,
|
||||
content,
|
||||
title: (data.title as string | undefined) ?? slug,
|
||||
date: data.date as string | undefined,
|
||||
description: data.description as string | undefined,
|
||||
};
|
||||
}),
|
||||
});
|
||||
Reference in New Issue
Block a user