added seed data; image uploads via server actions; infinite article grid
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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: {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user