added seed data; image uploads via server actions; infinite article grid

This commit is contained in:
2025-03-14 20:48:17 +01:00
parent 7135e34699
commit 37233db0ec
73 changed files with 4578 additions and 1137 deletions
+3 -3
View File
@@ -6,6 +6,7 @@ import {
createArticleSchema,
} from "@/lib/validation/zod/article";
import { api } from "@/trpc/server";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { z } from "zod";
@@ -16,16 +17,15 @@ export async function createArticle(
article,
});
if (!result[0]?.slug?.length) return false;
revalidatePath("/");
return redirect(appRoutes.editArticle(result[0].slug));
}
export async function updateArticle(
article: z.infer<typeof articleSchema>,
articleId: string,
) {
console.log("Content :: action", JSON.stringify(article.content));
const result = await api.article.update({
article,
article: { ...article, content: JSON.parse(article.content) },
articleId,
});
// if (!result[0]?.id?.length) return false;
+13 -6
View File
@@ -16,12 +16,19 @@ export async function updateCategory(
category: z.infer<typeof categorySchema>,
categoryId: string,
) {
const result = await api.category.update({
category,
categoryId,
});
// if (!result[0]?.id?.length) return false;
// return revalidatePath(`/artikel/${result[0].id}/edit`);
try {
const result = await api.category.update({
category,
categoryId,
});
if (!result[0]?.id?.length) return false;
revalidatePath(`/artikel/${result[0].id}/edit`);
return true;
} catch (e) {
console.error(e);
return false;
}
}
export async function deleteCategory(categoryId: string) {
const result = await api.category.delete({
+17
View File
@@ -0,0 +1,17 @@
"use server";
import fs from "node:fs/promises";
import { revalidatePath } from "next/cache";
export async function uploadFile(formData: FormData) {
const file = formData.get("file") as File;
const arrayBuffer = await file.arrayBuffer();
const buffer = new Uint8Array(arrayBuffer);
const filename = `upload-${Date.now()}-${file.name}.png`;
try {
await fs.writeFile(`./public/uploads/${filename}`, buffer);
revalidatePath("/");
return `/uploads/${filename}`;
} catch (e) {
return false;
}
}
+23
View File
@@ -1,6 +1,29 @@
import { z } from "zod";
import { createTRPCRouter, publicProcedure } from "../trpc";
import { desc, ilike, like } from "drizzle-orm";
import {
articles as articlesTable,
categories as categoriesTable,
lower,
} from "@/server/db/schema";
export const appRouter = createTRPCRouter({
searchContent: publicProcedure
.input(
z.object({
query: z.string().optional(),
}),
)
.query(async ({ ctx, input }) => {
const articles = await ctx.db.query.articles.findMany({
where: ilike(articlesTable.title, "%" + input.query + "%"),
});
const categories = await ctx.db.query.categories.findMany({
where: like(categoriesTable.name, "%" + input.query + "%"),
});
return { articles, categories };
}),
getSidebarContent: publicProcedure.query(async ({ ctx }) => {
return await ctx.db.query.categories.findMany({
with: {
+60 -11
View File
@@ -5,17 +5,25 @@ import {
protectedProcedure,
publicProcedure,
} from "@/server/api/trpc";
import { articles } from "@/server/db/schema";
import { Article, articles } from "@/server/db/schema";
import {
articleSchema,
createArticleSchema,
} from "@/lib/validation/zod/article";
import { count, eq } from "drizzle-orm";
import { and, count, eq, gt, like, sql } from "drizzle-orm";
import { hasPermission, Role } from "@/lib/validation/permissions";
import { generateSlug } from "@/lib/utils";
export const articleRouter = createTRPCRouter({
// queries
search: publicProcedure
.input(z.object({ query: z.string() }))
.query(async ({ ctx, input }) => {
return (await ctx.db.query.articles.findMany({
where: like(articles.title, "%" + input.query + "%"),
with: { category: true },
})) as Article[];
}),
get: publicProcedure
.input(z.object({ slug: z.string() }))
.query(async ({ ctx, input }) => {
@@ -24,23 +32,57 @@ export const articleRouter = createTRPCRouter({
with: { category: true },
});
}),
getAll: publicProcedure
.input(z.object({ categoryId: z.string() }).optional())
getByPage: publicProcedure
.input(
z.object({
categoryId: z.string().optional(),
limit: z.number().optional(),
cursor: z.string().optional(),
}),
)
.query(async ({ ctx, input }) => {
return await ctx.db.query.articles.findMany({
where: input?.categoryId
? eq(articles.categoryId, input.categoryId)
: undefined,
const { categoryId, cursor } = input!;
const limit = input?.limit ?? 50;
const cursorArg = cursor ? gt(articles.slug, cursor) : undefined;
const categoryArg = categoryId
? eq(articles.categoryId, categoryId)
: undefined;
const items = await ctx.db.query.articles.findMany({
where: and(cursorArg, categoryArg),
limit: limit + 1,
orderBy: articles.slug,
columns: {
title: true,
slug: true,
createdAt: true,
},
});
let nextCursor: typeof cursor | undefined = undefined;
if (items.length > limit) {
const nextItem = items.pop();
nextCursor = nextItem!.slug;
}
return {
items,
nextCursor,
previousCursor: cursor,
};
}),
getAllPreviews: publicProcedure
.input(z.object({ categoryId: z.string() }).optional())
getAll: publicProcedure
.input(
z
.object({
categoryId: z.string().optional(),
limit: z.number().optional(),
})
.optional(),
)
.query(async ({ ctx, input }) => {
return await ctx.db.query.articles.findMany({
where: input?.categoryId
? eq(articles.categoryId, input.categoryId)
: undefined,
limit: input?.limit,
columns: {
title: true,
slug: true,
@@ -48,6 +90,7 @@ export const articleRouter = createTRPCRouter({
},
});
}),
getCount: publicProcedure
.input(z.object({ categoryId: z.string() }).optional())
.query(async ({ ctx, input }) => {
@@ -76,6 +119,12 @@ export const articleRouter = createTRPCRouter({
.values({ ...input.article, slug })
.returning({
slug: articles.slug,
})
.onConflictDoUpdate({
target: articles.slug,
set: {
slug: sql`${slug} || '-' || (SELECT COUNT(*) FROM ${articles} WHERE slug LIKE ${slug + "-%"})`,
},
});
}),
update: protectedProcedure
+27 -9
View File
@@ -4,25 +4,43 @@ import {
protectedProcedure,
publicProcedure,
} from "@/server/api/trpc";
import { categories } from "@/server/db/schema";
import { categories, Category } from "@/server/db/schema";
import { count, eq } from "drizzle-orm";
import { count, eq, like } from "drizzle-orm";
import { hasPermission, Role } from "@/lib/validation/permissions";
import { categorySchema } from "@/lib/validation/zod/category";
import { generateSlug } from "@/lib/utils";
export const categoryRouter = createTRPCRouter({
get: publicProcedure
.input(z.object({ slug: z.string() }))
search: publicProcedure
.input(z.object({ query: z.string() }))
.query(async ({ ctx, input }) => {
return await ctx.db.query.categories.findFirst({
where: eq(categories.slug, input.slug),
return await ctx.db.query.categories.findMany({
where: like(categories.name, "%" + input.query + "%"),
});
}),
get: publicProcedure
.input(z.object({ slug: z.string(), with: z.any() }))
.query(async ({ ctx, input }) => {
return (await ctx.db.query.categories.findFirst({
where: eq(categories.slug, input.slug),
with: input?.with,
})) as Category;
}),
getAll: publicProcedure.query(async ({ ctx }) => {
return await ctx.db.query.categories.findMany();
}),
getAll: publicProcedure
.input(
z
.object({
limit: z.number().optional(),
})
.optional(),
)
.query(async ({ ctx, input }) => {
return await ctx.db.query.categories.findMany({
limit: input?.limit,
});
}),
getCount: publicProcedure.query(async ({ ctx }) => {
return (await ctx.db.select({ count: count() }).from(categories))[0]?.count;
+2
View File
@@ -16,3 +16,5 @@ const conn = globalForDb.conn ?? postgres(env.DATABASE_URL);
if (env.NODE_ENV !== "production") globalForDb.conn = conn;
export const db = drizzle(conn, { schema });
export type DBType = typeof db;
+22 -11
View File
@@ -1,6 +1,7 @@
import { createId } from "@paralleldrive/cuid2";
import { relations, sql } from "drizzle-orm";
import { relations, SQL, sql } from "drizzle-orm";
import {
AnyPgColumn,
boolean,
index,
integer,
@@ -11,15 +12,14 @@ import {
timestamp,
varchar,
} from "drizzle-orm/pg-core";
import { User } from "next-auth";
import { type AdapterAccount } from "next-auth/adapters";
import { type JSONContent } from "@tiptap/react";
import { JSONContent } from "novel";
export function lower(value: AnyPgColumn): SQL {
return sql`lower(${value})`;
}
/**
* This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same
* database instance for multiple projects.
*
* @see https://orm.drizzle.team/docs/goodies#multi-project-schema
*/
export const createTable = pgTableCreator((name) => `wiki-antifa_${name}`);
export const articles = createTable(
@@ -32,7 +32,7 @@ export const articles = createTable(
title: varchar("title", { length: 256 }),
slug: varchar("slug", { length: 256 }).unique().notNull(),
authorId: varchar("author_id", { length: 255 }),
content: text("content"),
content: jsonb("content").$type<JSONContent>(),
categoryId: varchar("category_id", { length: 255 }),
published: boolean("published").default(false),
createdAt: timestamp("created_at", { withTimezone: true })
@@ -46,12 +46,19 @@ export const articles = createTable(
articleTitleIndex: index("article_title_idx").on(example.title),
}),
);
export type Article = typeof articles.$inferSelect;
export type Article = typeof articles.$inferSelect & {
author?: User;
category?: Category;
};
export const articleRelations = relations(articles, ({ one }) => ({
category: one(categories, {
fields: [articles.categoryId],
references: [categories.id],
}),
author: one(users, {
fields: [articles.authorId],
references: [users.id],
}),
}));
export const categories = createTable(
@@ -62,8 +69,10 @@ export const categories = createTable(
.$defaultFn(() => createId())
.notNull(),
name: varchar("name", { length: 256 }),
description: text("description"),
slug: varchar("slug", { length: 256 }).unique().notNull(),
image: varchar("image", { length: 255 }),
createdAt: timestamp("created_at", { withTimezone: true })
.default(sql`CURRENT_TIMESTAMP`)
.notNull(),
@@ -75,7 +84,9 @@ export const categories = createTable(
categoryNameIndex: index("category_name_idx").on(example.name),
}),
);
export type Category = typeof categories.$inferSelect;
export type Category = typeof categories.$inferSelect & {
articles?: Article[];
};
export const categoryRelations = relations(categories, ({ many }) => ({
articles: many(articles),