mirror of
https://github.com/khairul169/code-share.git
synced 2026-10-24 11:53:58 +07:00
feat: add personal project list, add project settings, etc
This commit is contained in:
@@ -19,6 +19,7 @@ const get = async (req: Request, res: Response) => {
|
||||
if (!projectData) {
|
||||
return res.status(404).send("Project not found!");
|
||||
}
|
||||
const settings = projectData.settings || {};
|
||||
|
||||
const fileData = await db.query.file.findFirst({
|
||||
where: and(
|
||||
@@ -34,16 +35,18 @@ const get = async (req: Request, res: Response) => {
|
||||
const ext = getFileExt(fileData.filename);
|
||||
let content = fileData.content || "";
|
||||
|
||||
if (["html", "htm"].includes(ext)) {
|
||||
content = await serveHtml(fileData);
|
||||
}
|
||||
if (!req.query.raw) {
|
||||
if (["html", "htm"].includes(ext)) {
|
||||
content = await serveHtml(fileData, settings);
|
||||
}
|
||||
|
||||
if (["js", "ts", "jsx", "tsx"].includes(ext)) {
|
||||
content = await serveJs(fileData);
|
||||
}
|
||||
if (["js", "ts", "jsx", "tsx"].includes(ext)) {
|
||||
content = await serveJs(fileData, settings.js);
|
||||
}
|
||||
|
||||
if (["css"].includes(ext)) {
|
||||
content = await postcss(fileData);
|
||||
if (["css"].includes(ext) && settings.css?.preprocessor === "postcss") {
|
||||
content = await postcss(fileData, settings.css);
|
||||
}
|
||||
}
|
||||
|
||||
res.setHeader("Content-Type", getMimeType(fileData.filename));
|
||||
|
||||
@@ -3,17 +3,26 @@ import tailwindcss from "tailwindcss";
|
||||
import cssnano from "cssnano";
|
||||
import { FileSchema } from "~/server/db/schema/file";
|
||||
import { unpackProject } from "~/server/lib/unpack-project";
|
||||
import { ProjectSettingsSchema } from "~/server/db/schema/project";
|
||||
|
||||
export const postcss = async (fileData: FileSchema) => {
|
||||
export const postcss = async (
|
||||
fileData: FileSchema,
|
||||
cfg?: ProjectSettingsSchema["css"]
|
||||
) => {
|
||||
const content = fileData.content || "";
|
||||
|
||||
try {
|
||||
const projectDir = await unpackProject({ ext: "ts,tsx,js,jsx,html" });
|
||||
const plugins: any[] = [];
|
||||
|
||||
if (cfg?.tailwindcss) {
|
||||
plugins.push(
|
||||
tailwindcss({ content: [projectDir + "/**/*.{ts,tsx,js,jsx,html}"] })
|
||||
);
|
||||
}
|
||||
|
||||
const result = await postcssPlugin([
|
||||
tailwindcss({
|
||||
content: [projectDir + "/**/*.{ts,tsx,js,jsx,html}"],
|
||||
}),
|
||||
...plugins,
|
||||
cssnano({
|
||||
preset: ["default", { discardComments: { removeAll: true } }],
|
||||
}),
|
||||
|
||||
@@ -2,8 +2,12 @@ import db from "~/server/db";
|
||||
import { FileSchema, file } from "~/server/db/schema/file";
|
||||
import { and, eq, isNull } from "drizzle-orm";
|
||||
import { IS_DEV } from "~/server/lib/consts";
|
||||
import type { ProjectSettingsSchema } from "~/server/db/schema/project";
|
||||
|
||||
export const serveHtml = async (fileData: FileSchema) => {
|
||||
export const serveHtml = async (
|
||||
fileData: FileSchema,
|
||||
settings: Partial<ProjectSettingsSchema>
|
||||
) => {
|
||||
const layout = await db.query.file.findFirst({
|
||||
where: and(
|
||||
eq(file.projectId, fileData.projectId),
|
||||
@@ -24,18 +28,15 @@ export const serveHtml = async (fileData: FileSchema) => {
|
||||
const firstScriptTagIdx = content.indexOf("<script", bodyOpeningTagIdx);
|
||||
const injectScripts = ['<script src="/js/hook-console.js"></script>'];
|
||||
|
||||
// prevent direct access
|
||||
if (!IS_DEV) {
|
||||
// prevent direct access
|
||||
injectScripts.push(
|
||||
`<script>if (window === window.parent) {window.location.href = '/';}</script>`
|
||||
);
|
||||
}
|
||||
|
||||
const importMaps = [
|
||||
{ name: "react", url: "https://esm.sh/[email protected]" },
|
||||
{ name: "react-dom/client", url: "https://esm.sh/[email protected]/client" },
|
||||
];
|
||||
|
||||
// js import maps
|
||||
const importMaps = settings?.js?.packages || [];
|
||||
if (importMaps.length > 0) {
|
||||
const imports = importMaps.reduce((a: any, b) => {
|
||||
a[b.name] = b.url;
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import { FileSchema } from "~/server/db/schema/file";
|
||||
import { ProjectSettingsSchema } from "~/server/db/schema/project";
|
||||
import { transformJs } from "~/server/lib/transform-js";
|
||||
|
||||
export const serveJs = async (file: FileSchema) => {
|
||||
export const serveJs = async (
|
||||
file: FileSchema,
|
||||
cfg?: ProjectSettingsSchema["js"]
|
||||
) => {
|
||||
let content = file.content || "";
|
||||
|
||||
// transform js
|
||||
content = await transformJs(content);
|
||||
// transpile to es5
|
||||
if (cfg?.transpiler === "swc") {
|
||||
content = await transformJs(content);
|
||||
}
|
||||
|
||||
return content;
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { CreateExpressContextOptions } from "@trpc/server/adapters/express";
|
||||
|
||||
export const createContext = async (ctx: CreateExpressContextOptions) => {
|
||||
return ctx;
|
||||
return { ...ctx, user: ctx.req.user };
|
||||
};
|
||||
|
||||
export type Context = Awaited<ReturnType<typeof createContext>>;
|
||||
|
||||
+61
-15
@@ -1,22 +1,48 @@
|
||||
import { relations, sql } from "drizzle-orm";
|
||||
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
||||
import { index, integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
||||
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
|
||||
import { z } from "zod";
|
||||
import { user } from "./user";
|
||||
import { file } from "./file";
|
||||
|
||||
export const project = sqliteTable("projects", {
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
userId: integer("user_id")
|
||||
.notNull()
|
||||
.references(() => user.id),
|
||||
slug: text("slug").notNull().unique(),
|
||||
title: text("title").notNull(),
|
||||
createdAt: text("created_at")
|
||||
.notNull()
|
||||
.default(sql`CURRENT_TIMESTAMP`),
|
||||
deletedAt: text("deleted_at"),
|
||||
});
|
||||
const defaultSettings: ProjectSettingsSchema = {
|
||||
css: {
|
||||
preprocessor: null,
|
||||
tailwindcss: false,
|
||||
},
|
||||
js: {
|
||||
transpiler: null,
|
||||
packages: [],
|
||||
},
|
||||
};
|
||||
|
||||
export const project = sqliteTable(
|
||||
"projects",
|
||||
{
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
userId: integer("user_id")
|
||||
.notNull()
|
||||
.references(() => user.id),
|
||||
slug: text("slug").notNull().unique(),
|
||||
title: text("title").notNull(),
|
||||
|
||||
visibility: text("visibility", {
|
||||
enum: ["public", "private", "unlisted"],
|
||||
}).default("private"),
|
||||
|
||||
settings: text("settings", { mode: "json" })
|
||||
.$type<Partial<ProjectSettingsSchema>>()
|
||||
.default(defaultSettings),
|
||||
|
||||
createdAt: text("created_at")
|
||||
.notNull()
|
||||
.default(sql`CURRENT_TIMESTAMP`),
|
||||
deletedAt: text("deleted_at"),
|
||||
},
|
||||
(table) => ({
|
||||
visibilityEnum: index("project_visibility_idx").on(table.visibility),
|
||||
})
|
||||
);
|
||||
|
||||
export const projectRelations = relations(project, ({ one, many }) => ({
|
||||
files: many(file),
|
||||
@@ -26,7 +52,27 @@ export const projectRelations = relations(project, ({ one, many }) => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
export const insertProjectSchema = createInsertSchema(project);
|
||||
export const selectProjectSchema = createSelectSchema(project);
|
||||
export const projectSettingsSchema = z.object({
|
||||
css: z.object({
|
||||
preprocessor: z.enum(["", "postcss"]).optional().nullable(),
|
||||
tailwindcss: z.boolean().optional().nullable(),
|
||||
}),
|
||||
|
||||
js: z.object({
|
||||
transpiler: z.enum(["", "swc"]).optional().nullable(),
|
||||
packages: z
|
||||
.object({ name: z.string().min(1), url: z.string().min(1).url() })
|
||||
.array(),
|
||||
}),
|
||||
});
|
||||
|
||||
export type ProjectSettingsSchema = z.infer<typeof projectSettingsSchema>;
|
||||
|
||||
export const insertProjectSchema = createInsertSchema(project, {
|
||||
settings: projectSettingsSchema,
|
||||
});
|
||||
export const selectProjectSchema = createSelectSchema(project, {
|
||||
settings: projectSettingsSchema.partial(),
|
||||
});
|
||||
|
||||
export type ProjectSchema = z.infer<typeof selectProjectSchema>;
|
||||
|
||||
+28
-2
@@ -17,7 +17,12 @@ const main = async () => {
|
||||
|
||||
const [vanillaProject] = await db
|
||||
.insert(project)
|
||||
.values({ userId: adminUser.id, slug: uid(), title: "Vanilla Project" })
|
||||
.values({
|
||||
userId: adminUser.id,
|
||||
slug: uid(),
|
||||
title: "Vanilla Project",
|
||||
visibility: "public",
|
||||
})
|
||||
.returning();
|
||||
|
||||
// vanilla html css js template
|
||||
@@ -69,7 +74,28 @@ const main = async () => {
|
||||
|
||||
const [reactProject] = await db
|
||||
.insert(project)
|
||||
.values({ userId: adminUser.id, slug: uid(), title: "React Project" })
|
||||
.values({
|
||||
userId: adminUser.id,
|
||||
slug: uid(),
|
||||
title: "React Project",
|
||||
visibility: "public",
|
||||
settings: {
|
||||
css: {
|
||||
preprocessor: "postcss",
|
||||
tailwindcss: true,
|
||||
},
|
||||
js: {
|
||||
transpiler: "swc",
|
||||
packages: [
|
||||
{ name: "react", url: "https://esm.sh/[email protected]" },
|
||||
{
|
||||
name: "react-dom/client",
|
||||
url: "https://esm.sh/[email protected]/client",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
.returning();
|
||||
|
||||
// react template
|
||||
|
||||
@@ -23,7 +23,6 @@ export const unpackProject = async (
|
||||
|
||||
const projectDir = getProjectDir();
|
||||
if (!fileExists(projectDir)) {
|
||||
console.log("not exist", projectDir);
|
||||
await fs.mkdir(projectDir, { recursive: true });
|
||||
}
|
||||
|
||||
|
||||
+74
-17
@@ -14,21 +14,34 @@ import { faker } from "@faker-js/faker";
|
||||
import { ucwords } from "~/lib/utils";
|
||||
import { hashPassword } from "../lib/crypto";
|
||||
import { createToken } from "../lib/jwt";
|
||||
import { file } from "../db/schema/file";
|
||||
|
||||
const projectRouter = router({
|
||||
getAll: procedure.query(async () => {
|
||||
const where = and(isNull(project.deletedAt));
|
||||
getAll: procedure
|
||||
.input(z.object({ owned: z.boolean() }).partial().optional())
|
||||
.query(async ({ ctx, input: opt }) => {
|
||||
if (opt?.owned && !ctx.user) {
|
||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
}
|
||||
|
||||
const projects = await db.query.project.findMany({
|
||||
where,
|
||||
with: {
|
||||
user: { columns: { password: false } },
|
||||
},
|
||||
orderBy: [desc(project.id)],
|
||||
});
|
||||
const where = [
|
||||
!opt?.owned
|
||||
? eq(project.visibility, "public")
|
||||
: eq(project.userId, ctx.user!.id),
|
||||
isNull(project.deletedAt),
|
||||
];
|
||||
|
||||
return projects;
|
||||
}),
|
||||
const projects = await db.query.project.findMany({
|
||||
where: and(...(where.filter((i) => i != null) as any)),
|
||||
columns: { settings: false },
|
||||
with: {
|
||||
user: { columns: { password: false } },
|
||||
},
|
||||
orderBy: [desc(project.id)],
|
||||
});
|
||||
|
||||
return projects;
|
||||
}),
|
||||
|
||||
getById: procedure
|
||||
.input(z.number().or(z.string()))
|
||||
@@ -70,10 +83,10 @@ const projectRouter = router({
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const title =
|
||||
input.title.length > 0 ? input.title : ucwords(faker.lorem.words(2));
|
||||
let userId = 0;
|
||||
let userId = ctx.user?.id;
|
||||
|
||||
return db.transaction(async (tx) => {
|
||||
if (input.user) {
|
||||
if (input.user && !userId) {
|
||||
const [usr] = await tx
|
||||
.insert(user)
|
||||
.values({
|
||||
@@ -99,15 +112,52 @@ const projectRouter = router({
|
||||
slug: uid(),
|
||||
};
|
||||
|
||||
const [result] = await tx.insert(project).values(data).returning();
|
||||
const [projectData] = await tx.insert(project).values(data).returning();
|
||||
const projectId = projectData.id;
|
||||
|
||||
return result;
|
||||
if (input.forkFromId) {
|
||||
const forkFiles = await tx.query.file.findMany({
|
||||
where: and(
|
||||
eq(file.projectId, input.forkFromId),
|
||||
isNull(file.deletedAt)
|
||||
),
|
||||
columns: {
|
||||
id: false,
|
||||
projectId: false,
|
||||
createdAt: false,
|
||||
deletedAt: false,
|
||||
},
|
||||
});
|
||||
|
||||
await tx
|
||||
.insert(file)
|
||||
.values(forkFiles.map((file) => ({ ...file, projectId })));
|
||||
} else {
|
||||
await tx.insert(file).values([
|
||||
{
|
||||
projectId,
|
||||
path: "index.html",
|
||||
filename: "index.html",
|
||||
content: "<p>Open index.html to edit this file.</p>",
|
||||
isPinned: true,
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
return projectData;
|
||||
});
|
||||
}),
|
||||
|
||||
update: procedure
|
||||
.input(selectProjectSchema.partial().required({ id: true }))
|
||||
.input(
|
||||
selectProjectSchema
|
||||
.partial()
|
||||
.omit({ slug: true, userId: true })
|
||||
.required({ id: true })
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
const data = { ...input };
|
||||
|
||||
const projectData = await db.query.project.findFirst({
|
||||
where: and(eq(project.id, input.id), isNull(project.deletedAt)),
|
||||
});
|
||||
@@ -115,9 +165,16 @@ const projectRouter = router({
|
||||
throw new TRPCError({ code: "NOT_FOUND" });
|
||||
}
|
||||
|
||||
if (data.settings) {
|
||||
data.settings = Object.assign(
|
||||
projectData.settings || {},
|
||||
data.settings
|
||||
);
|
||||
}
|
||||
|
||||
const [result] = await db
|
||||
.update(project)
|
||||
.set(input)
|
||||
.set(data)
|
||||
.where(and(eq(project.id, input.id), isNull(project.deletedAt)))
|
||||
.returning();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user