mirror of
https://github.com/khairul169/code-share.git
synced 2025-04-28 16:49:36 +07:00
feat: add project schema
This commit is contained in:
parent
b232410d83
commit
d579c1b7c5
12
lib/utils.ts
12
lib/utils.ts
@ -1,6 +1,8 @@
|
||||
import { type ClassValue, clsx } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { BASE_URL } from "./consts";
|
||||
import type { ProjectSchema } from "~/server/db/schema/project";
|
||||
import type { FileSchema } from "~/server/db/schema/file";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
@ -13,3 +15,13 @@ export function getFileExt(filename: string) {
|
||||
export function getUrl(...path: string[]) {
|
||||
return BASE_URL + ("/" + path.join("/")).replace(/\/\/+/g, "/");
|
||||
}
|
||||
|
||||
export function getPreviewUrl(
|
||||
project: Pick<ProjectSchema, "slug">,
|
||||
file: string | Pick<FileSchema, "path">
|
||||
) {
|
||||
return getUrl(
|
||||
`api/preview/${project.slug}`,
|
||||
typeof file === "string" ? file : file.path
|
||||
);
|
||||
}
|
||||
|
@ -6,7 +6,8 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"dev": "tsx watch --ignore *.mjs server/index.ts",
|
||||
"dev": "tsx server/index.ts",
|
||||
"dev:watch": "tsx watch --ignore *.mjs server/index.ts",
|
||||
"start": "NODE_ENV=production tsx server/index.ts",
|
||||
"build": "vite build",
|
||||
"generate": "drizzle-kit generate:sqlite",
|
||||
@ -42,6 +43,7 @@
|
||||
"@codemirror/lang-json": "^6.0.1",
|
||||
"@emmetio/codemirror6-plugin": "^0.3.0",
|
||||
"@hookform/resolvers": "^3.3.4",
|
||||
"@paralleldrive/cuid2": "^2.2.2",
|
||||
"@radix-ui/react-dialog": "^1.0.5",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||
"@radix-ui/react-slot": "^1.0.2",
|
||||
|
@ -1,17 +0,0 @@
|
||||
import { useData } from "~/renderer/hooks";
|
||||
import { Data } from "./+data";
|
||||
import Link from "~/renderer/link";
|
||||
|
||||
const ViewPostPage = () => {
|
||||
const { post } = useData<Data>();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Link href="/">Go Back</Link>
|
||||
<h1>{post.title}</h1>
|
||||
<p>{post.body}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ViewPostPage;
|
@ -1,12 +0,0 @@
|
||||
import { PageContext } from "vike/types";
|
||||
|
||||
export const data = async (ctx: PageContext) => {
|
||||
const id = ctx.routeParams?.id;
|
||||
const post = await fetch(
|
||||
"https://jsonplaceholder.typicode.com/posts/" + id
|
||||
).then((response) => response.json());
|
||||
|
||||
return { post, title: post?.title };
|
||||
};
|
||||
|
||||
export type Data = Awaited<ReturnType<typeof data>>;
|
@ -3,19 +3,15 @@ import { useData } from "~/renderer/hooks";
|
||||
import Link from "~/renderer/link";
|
||||
|
||||
const HomePage = () => {
|
||||
const { posts } = useData<Data>();
|
||||
|
||||
if (!posts?.length) {
|
||||
return <p>No posts.</p>;
|
||||
}
|
||||
const { projects } = useData<Data>();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Posts</h1>
|
||||
|
||||
{posts.map((post: any) => (
|
||||
<Link key={post.id} href={`/${post.id}`}>
|
||||
{post.title}
|
||||
{projects.map((project: any) => (
|
||||
<Link key={project.id} href={`/${project.slug}`}>
|
||||
{project.title}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
@ -1,9 +1,12 @@
|
||||
export const data = async () => {
|
||||
const posts = await fetch(
|
||||
"https://jsonplaceholder.typicode.com/posts?_limit=20"
|
||||
).then((response) => response.json());
|
||||
import { PageContext } from "vike/types";
|
||||
import trpcServer from "~/server/api/trpc/trpc";
|
||||
|
||||
return { posts };
|
||||
export const data = async (ctx: PageContext) => {
|
||||
const trpc = await trpcServer(ctx);
|
||||
|
||||
const projects = await trpc.project.getAll();
|
||||
|
||||
return { projects };
|
||||
};
|
||||
|
||||
export type Data = Awaited<ReturnType<typeof data>>;
|
||||
|
@ -6,22 +6,21 @@ import {
|
||||
import WebPreview from "./components/web-preview";
|
||||
import Editor from "./components/editor";
|
||||
import ProjectContext from "./context/project";
|
||||
import { cn } from "~/lib/utils";
|
||||
import { useParams, useSearchParams } from "~/renderer/hooks";
|
||||
import { BASE_URL } from "~/lib/consts";
|
||||
import { cn, getPreviewUrl } from "~/lib/utils";
|
||||
import { useData, useSearchParams } from "~/renderer/hooks";
|
||||
import { withClientOnly } from "~/renderer/client-only";
|
||||
import Spinner from "~/components/ui/spinner";
|
||||
import { Data } from "./+data";
|
||||
|
||||
const ViewProjectPage = () => {
|
||||
const { project } = useData<Data>();
|
||||
const searchParams = useSearchParams();
|
||||
const params = useParams();
|
||||
const isCompact =
|
||||
searchParams.get("compact") === "1" || searchParams.get("embed") === "1";
|
||||
const slug = params["slug"];
|
||||
const previewUrl = BASE_URL + `/api/preview/${slug}/index.html`;
|
||||
const previewUrl = getPreviewUrl(project, "index.html");
|
||||
|
||||
return (
|
||||
<ProjectContext.Provider value={{ slug, isCompact }}>
|
||||
<ProjectContext.Provider value={{ project, isCompact }}>
|
||||
<ResizablePanelGroup
|
||||
autoSaveId="main-panel"
|
||||
direction={{ sm: "vertical", md: "horizontal" }}
|
||||
|
@ -1,12 +1,25 @@
|
||||
import { PageContext } from "vike/types";
|
||||
import { render } from "vike/abort";
|
||||
import trpcServer from "~/server/api/trpc/trpc";
|
||||
|
||||
export const data = async (ctx: PageContext) => {
|
||||
const trpc = await trpcServer(ctx);
|
||||
const pinnedFiles = await trpc.file.getAll({ isPinned: true });
|
||||
const files = await trpc.file.getAll();
|
||||
|
||||
return { files, pinnedFiles };
|
||||
const project = await trpc.project.getById(ctx.routeParams?.slug!);
|
||||
if (!project) {
|
||||
throw render(404, "Project not found!");
|
||||
}
|
||||
|
||||
const files = await trpc.file.getAll({ projectId: project.id });
|
||||
const pinnedFiles = files.filter((i) => i.isPinned);
|
||||
|
||||
return {
|
||||
title: project.title,
|
||||
description: `Check ${project.title} on CodeShare!`,
|
||||
project,
|
||||
files,
|
||||
pinnedFiles,
|
||||
};
|
||||
};
|
||||
|
||||
export type Data = Awaited<ReturnType<typeof data>>;
|
||||
|
1
pages/project/@slug/+route.ts
Normal file
1
pages/project/@slug/+route.ts
Normal file
@ -0,0 +1 @@
|
||||
export default "/@slug";
|
@ -23,9 +23,9 @@ import { Data } from "../+data";
|
||||
import { useBreakpoint } from "~/hooks/useBreakpoint";
|
||||
|
||||
const Editor = () => {
|
||||
const { pinnedFiles } = useData<Data>();
|
||||
const { project, pinnedFiles } = useData<Data>();
|
||||
const trpcUtils = trpc.useUtils();
|
||||
const project = useProjectContext();
|
||||
const projectCtx = useProjectContext();
|
||||
const sidebarPanel = useRef<ImperativePanelHandle>(null);
|
||||
const [breakpoint] = useBreakpoint();
|
||||
|
||||
@ -36,7 +36,7 @@ const Editor = () => {
|
||||
);
|
||||
|
||||
const openedFilesData = trpc.file.getAll.useQuery(
|
||||
{ id: curOpenFiles },
|
||||
{ projectId: project.id, id: curOpenFiles },
|
||||
{ enabled: curOpenFiles.length > 0, initialData: pinnedFiles }
|
||||
);
|
||||
const [openedFiles, setOpenedFiles] = useState<any[]>(pinnedFiles);
|
||||
@ -156,7 +156,7 @@ const Editor = () => {
|
||||
}) satisfies Tab[];
|
||||
}, [curOpenFiles, openedFiles, refreshPreview]);
|
||||
|
||||
const PanelComponent = !project.isCompact ? Panel : "div";
|
||||
const PanelComponent = !projectCtx.isCompact ? Panel : "div";
|
||||
|
||||
return (
|
||||
<EditorContext.Provider
|
||||
|
@ -1,6 +1,4 @@
|
||||
"use client";
|
||||
|
||||
import React, { Fragment, useMemo, useState } from "react";
|
||||
import { Fragment, useMemo, useState } from "react";
|
||||
import { UseDiscloseReturn, useDisclose } from "~/hooks/useDisclose";
|
||||
import {
|
||||
FiChevronRight,
|
||||
@ -21,27 +19,28 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuSeparator,
|
||||
} from "~/components/ui/dropdown-menu";
|
||||
import { cn, getUrl } from "~/lib/utils";
|
||||
import { cn, getPreviewUrl } from "~/lib/utils";
|
||||
import FileIcon from "~/components/ui/file-icon";
|
||||
import copy from "copy-to-clipboard";
|
||||
import { useData, useParams } from "~/renderer/hooks";
|
||||
import { useData } from "~/renderer/hooks";
|
||||
import Spinner from "~/components/ui/spinner";
|
||||
import { Data } from "../+data";
|
||||
|
||||
const FileListing = () => {
|
||||
const pageData = useData<Data>();
|
||||
const { project, files: initialFiles } = useData<Data>();
|
||||
const { onOpenFile, onFileChanged } = useEditorContext();
|
||||
const createFileDlg = useDisclose<CreateFileSchema>();
|
||||
const files = trpc.file.getAll.useQuery(undefined, {
|
||||
initialData: pageData.files,
|
||||
});
|
||||
const files = trpc.file.getAll.useQuery(
|
||||
{ projectId: project.id },
|
||||
{ initialData: initialFiles }
|
||||
);
|
||||
|
||||
const fileList = useMemo(() => groupFiles(files.data, null), [files.data]);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div className="h-10 flex items-center pl-4 pr-1">
|
||||
<p className="text-xs uppercase truncate flex-1">My Project</p>
|
||||
<p className="text-xs uppercase truncate flex-1">{project.title}</p>
|
||||
<ActionButton
|
||||
icon={FiFilePlus}
|
||||
onClick={() => createFileDlg.onOpen()}
|
||||
@ -104,7 +103,7 @@ type FileItemProps = {
|
||||
};
|
||||
|
||||
const FileItem = ({ file, createFileDlg }: FileItemProps) => {
|
||||
const { slug } = useParams();
|
||||
const { project } = useData<Data>();
|
||||
const { onOpenFile, onDeleteFile } = useEditorContext();
|
||||
const [isCollapsed, setCollapsed] = useState(false);
|
||||
const trpcUtils = trpc.useUtils();
|
||||
@ -186,16 +185,13 @@ const FileItem = ({ file, createFileDlg }: FileItemProps) => {
|
||||
Copy Path
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => copy(getUrl(`api/preview/${slug}`, file.path))}
|
||||
onClick={() => copy(getPreviewUrl(project, file))}
|
||||
>
|
||||
Copy URL
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
window.open(
|
||||
getUrl(`api/preview/${slug}`, file.path),
|
||||
"_blank"
|
||||
)
|
||||
window.open(getPreviewUrl(project, file), "_blank")
|
||||
}
|
||||
>
|
||||
Open in new tab
|
||||
|
@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { getFileExt } from "~/lib/utils";
|
||||
import CodeEditor from "../../../../components/ui/code-editor";
|
||||
import trpc from "~/lib/trpc";
|
||||
|
@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import FileListing from "./file-listing";
|
||||
import { FaUserCircle } from "react-icons/fa";
|
||||
import { Button } from "~/components/ui/button";
|
||||
@ -7,6 +6,7 @@ const Sidebar = () => {
|
||||
return (
|
||||
<aside className="flex flex-col items-stretch h-full">
|
||||
<FileListing />
|
||||
|
||||
<div className="h-12 bg-[#1a1b26] pl-12">
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { createContext, useContext } from "react";
|
||||
import type { ProjectSchema } from "~/server/db/schema/project";
|
||||
|
||||
type TProjectContext = {
|
||||
slug: string;
|
||||
project: ProjectSchema;
|
||||
isCompact?: boolean;
|
||||
};
|
||||
|
||||
|
14
pnpm-lock.yaml
generated
14
pnpm-lock.yaml
generated
@ -23,6 +23,9 @@ dependencies:
|
||||
'@hookform/resolvers':
|
||||
specifier: ^3.3.4
|
||||
version: 3.3.4(react-hook-form@7.50.1)
|
||||
'@paralleldrive/cuid2':
|
||||
specifier: ^2.2.2
|
||||
version: 2.2.2
|
||||
'@radix-ui/react-dialog':
|
||||
specifier: ^1.0.5
|
||||
version: 1.0.5(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0)
|
||||
@ -1324,6 +1327,11 @@ packages:
|
||||
os-filter-obj: 2.0.0
|
||||
dev: true
|
||||
|
||||
/@noble/hashes@1.3.3:
|
||||
resolution: {integrity: sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==}
|
||||
engines: {node: '>= 16'}
|
||||
dev: false
|
||||
|
||||
/@nodelib/fs.scandir@2.1.5:
|
||||
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
||||
engines: {node: '>= 8'}
|
||||
@ -1342,6 +1350,12 @@ packages:
|
||||
'@nodelib/fs.scandir': 2.1.5
|
||||
fastq: 1.17.1
|
||||
|
||||
/@paralleldrive/cuid2@2.2.2:
|
||||
resolution: {integrity: sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==}
|
||||
dependencies:
|
||||
'@noble/hashes': 1.3.3
|
||||
dev: false
|
||||
|
||||
/@pkgjs/parseargs@0.11.0:
|
||||
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
||||
engines: {node: '>=14'}
|
||||
|
@ -2,10 +2,12 @@ import type { PageContext } from "vike/types";
|
||||
|
||||
export function getPageMetadata(pageContext: PageContext) {
|
||||
let title = pageContext.data?.title || pageContext.config.title;
|
||||
title = title ? `${title} - Vike` : "Welcome to Vike";
|
||||
title = title ? `${title} - CodeShare` : "Welcome to CodeShare";
|
||||
|
||||
const description =
|
||||
pageContext.data?.description || pageContext.config.description || "";
|
||||
pageContext.data?.description ||
|
||||
pageContext.config.description ||
|
||||
"Share your frontend result with everyone";
|
||||
|
||||
return { title, description };
|
||||
}
|
||||
|
@ -7,15 +7,26 @@ import { serveHtml } from "./serve-html";
|
||||
import { serveJs } from "./serve-js";
|
||||
import { getMimeType } from "~/server/lib/mime";
|
||||
import { postcss } from "./postcss";
|
||||
import { project } from "~/server/db/schema/project";
|
||||
|
||||
const get = async (req: Request, res: Response) => {
|
||||
const { slug, ...pathParams } = req.params as any;
|
||||
const path = pathParams[0];
|
||||
|
||||
const fileData = await db.query.file.findFirst({
|
||||
where: and(eq(file.path, path), isNull(file.deletedAt)),
|
||||
const projectData = await db.query.project.findFirst({
|
||||
where: and(eq(project.slug, slug), isNull(project.deletedAt)),
|
||||
});
|
||||
if (!projectData) {
|
||||
return res.status(404).send("Project not found!");
|
||||
}
|
||||
|
||||
const fileData = await db.query.file.findFirst({
|
||||
where: and(
|
||||
eq(file.projectId, projectData.id),
|
||||
eq(file.path, path),
|
||||
isNull(file.deletedAt)
|
||||
),
|
||||
});
|
||||
if (!fileData) {
|
||||
return res.status(404).send("File not found!");
|
||||
}
|
||||
|
@ -1,12 +1,12 @@
|
||||
import db from "~/server/db";
|
||||
import { FileSchema, file } from "~/server/db/schema/file";
|
||||
import { and, eq, isNull } from "drizzle-orm";
|
||||
|
||||
const preventHtmlDirectAccess = `<script>if (window === window.parent) {window.location.href = '/';}</script>`;
|
||||
import { IS_DEV } from "~/server/lib/consts";
|
||||
|
||||
export const serveHtml = async (fileData: FileSchema) => {
|
||||
const layout = await db.query.file.findFirst({
|
||||
where: and(
|
||||
eq(file.projectId, fileData.projectId),
|
||||
eq(file.filename, "_layout.html"),
|
||||
fileData.parentId
|
||||
? eq(file.parentId, fileData.parentId)
|
||||
@ -22,10 +22,14 @@ export const serveHtml = async (fileData: FileSchema) => {
|
||||
|
||||
const bodyOpeningTagIdx = content.indexOf("<body");
|
||||
const firstScriptTagIdx = content.indexOf("<script", bodyOpeningTagIdx);
|
||||
const injectScripts = [
|
||||
'<script src="/js/hook-console.js"></script>',
|
||||
preventHtmlDirectAccess,
|
||||
];
|
||||
const injectScripts = ['<script src="/js/hook-console.js"></script>'];
|
||||
|
||||
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/react@18.2.0" },
|
||||
|
@ -1,2 +1,3 @@
|
||||
export { user } from "./user";
|
||||
export { file } from "./file";
|
||||
export { project, projectRelations } from "./project";
|
||||
export { file, fileRelations } from "./file";
|
||||
|
@ -1,23 +1,24 @@
|
||||
import { sql } from "drizzle-orm";
|
||||
import { relations, sql } from "drizzle-orm";
|
||||
import {
|
||||
integer,
|
||||
sqliteTable,
|
||||
text,
|
||||
foreignKey,
|
||||
index,
|
||||
} from "drizzle-orm/sqlite-core";
|
||||
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
|
||||
import { z } from "zod";
|
||||
import { user } from "./user";
|
||||
import { project } from "./project";
|
||||
|
||||
export const file = sqliteTable(
|
||||
"files",
|
||||
{
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
parentId: integer("parent_id"),
|
||||
userId: integer("user_id")
|
||||
projectId: integer("project_id")
|
||||
.notNull()
|
||||
.references(() => user.id),
|
||||
path: text("path").notNull().unique(),
|
||||
.references(() => project.id),
|
||||
parentId: integer("parent_id"),
|
||||
path: text("path").notNull(),
|
||||
filename: text("filename").notNull(),
|
||||
isDirectory: integer("is_directory", { mode: "boolean" })
|
||||
.notNull()
|
||||
@ -38,9 +39,18 @@ export const file = sqliteTable(
|
||||
foreignColumns: [table.id],
|
||||
name: "parent_id_fk",
|
||||
}),
|
||||
pathIdx: index("file_path_idx").on(table.path),
|
||||
filenameIdx: index("file_name_idx").on(table.filename),
|
||||
})
|
||||
);
|
||||
|
||||
export const fileRelations = relations(file, ({ one }) => ({
|
||||
project: one(project, {
|
||||
fields: [file.projectId],
|
||||
references: [project.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const insertFileSchema = createInsertSchema(file);
|
||||
export const selectFileSchema = createSelectSchema(file);
|
||||
|
||||
|
32
server/db/schema/project.ts
Normal file
32
server/db/schema/project.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { relations, sql } from "drizzle-orm";
|
||||
import { 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"),
|
||||
});
|
||||
|
||||
export const projectRelations = relations(project, ({ one, many }) => ({
|
||||
files: many(file),
|
||||
user: one(user, {
|
||||
fields: [project.userId],
|
||||
references: [user.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const insertProjectSchema = createInsertSchema(project);
|
||||
export const selectProjectSchema = createSelectSchema(project);
|
||||
|
||||
export type ProjectSchema = z.infer<typeof selectProjectSchema>;
|
@ -5,6 +5,7 @@ import { z } from "zod";
|
||||
|
||||
export const user = sqliteTable("users", {
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
name: text("name").notNull(),
|
||||
email: text("email").notNull().unique(),
|
||||
password: text("password").notNull(),
|
||||
createdAt: text("created_at")
|
||||
|
@ -1,68 +1,83 @@
|
||||
import db from ".";
|
||||
import { hashPassword } from "../lib/crypto";
|
||||
import { uid } from "../lib/utils";
|
||||
import { file } from "./schema/file";
|
||||
import { project } from "./schema/project";
|
||||
import { user } from "./schema/user";
|
||||
|
||||
const main = async () => {
|
||||
const [adminUser] = await db
|
||||
.insert(user)
|
||||
.values({
|
||||
name: "Admin",
|
||||
email: "admin@mail.com",
|
||||
password: await hashPassword("123456"),
|
||||
})
|
||||
.returning();
|
||||
|
||||
// await db
|
||||
// .insert(file)
|
||||
// .values([
|
||||
// {
|
||||
// userId: adminUser.id,
|
||||
// path: "index.html",
|
||||
// filename: "index.html",
|
||||
// content: '<p class="text-lg text-red-500">Hello world!</p>',
|
||||
// },
|
||||
// {
|
||||
// userId: adminUser.id,
|
||||
// path: "styles.css",
|
||||
// filename: "styles.css",
|
||||
// content: "body { padding: 16px; }",
|
||||
// },
|
||||
// {
|
||||
// userId: adminUser.id,
|
||||
// path: "script.js",
|
||||
// filename: "script.js",
|
||||
// content: "console.log('hello world!');",
|
||||
// },
|
||||
// {
|
||||
// userId: adminUser.id,
|
||||
// path: "_layout.html",
|
||||
// filename: "_layout.html",
|
||||
// content: `<!DOCTYPE html>
|
||||
// <html lang="en">
|
||||
// <head>
|
||||
// <meta charset="UTF-8">
|
||||
// <meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
// <title>Document</title>
|
||||
const [vanillaProject] = await db
|
||||
.insert(project)
|
||||
.values({ userId: adminUser.id, slug: uid(), title: "Vanilla Project" })
|
||||
.returning();
|
||||
|
||||
// <link rel="stylesheet" href="styles.css">
|
||||
// vanilla html css js template
|
||||
await db
|
||||
.insert(file)
|
||||
.values([
|
||||
{
|
||||
projectId: vanillaProject.id,
|
||||
path: "index.html",
|
||||
filename: "index.html",
|
||||
content: "<p>Hello world!</p>",
|
||||
isPinned: true,
|
||||
},
|
||||
{
|
||||
projectId: vanillaProject.id,
|
||||
path: "styles.css",
|
||||
filename: "styles.css",
|
||||
content: "body { padding: 16px; }",
|
||||
isPinned: true,
|
||||
},
|
||||
{
|
||||
projectId: vanillaProject.id,
|
||||
path: "scripts.js",
|
||||
filename: "scripts.js",
|
||||
content: "console.log('hello world!');",
|
||||
isPinned: true,
|
||||
},
|
||||
{
|
||||
projectId: vanillaProject.id,
|
||||
path: "_layout.html",
|
||||
filename: "_layout.html",
|
||||
content: `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Document</title>
|
||||
|
||||
// <script src="https://cdn.tailwindcss.com"></script>
|
||||
// </head>
|
||||
// <body>
|
||||
// {CONTENT}
|
||||
// <script src="script.js" type="module" defer></script>
|
||||
// </body>
|
||||
// </html>`,
|
||||
// },
|
||||
// ])
|
||||
// .execute();
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
{CONTENT}
|
||||
<script src="scripts.js"></script>
|
||||
</body>
|
||||
</html>`,
|
||||
},
|
||||
])
|
||||
.execute();
|
||||
|
||||
const [reactProject] = await db
|
||||
.insert(project)
|
||||
.values({ userId: adminUser.id, slug: uid(), title: "React Project" })
|
||||
.returning();
|
||||
|
||||
// react template
|
||||
await db
|
||||
.insert(file)
|
||||
.values([
|
||||
{
|
||||
userId: adminUser.id,
|
||||
projectId: reactProject.id,
|
||||
path: "index.html",
|
||||
filename: "index.html",
|
||||
content: `<!doctype html>
|
||||
@ -84,7 +99,7 @@ const main = async () => {
|
||||
`,
|
||||
},
|
||||
{
|
||||
userId: adminUser.id,
|
||||
projectId: reactProject.id,
|
||||
path: "globals.css",
|
||||
filename: "globals.css",
|
||||
content: `@tailwind base;
|
||||
@ -97,7 +112,7 @@ body {
|
||||
`,
|
||||
},
|
||||
{
|
||||
userId: adminUser.id,
|
||||
projectId: reactProject.id,
|
||||
path: "index.jsx",
|
||||
filename: "index.jsx",
|
||||
content: `import React from "react";
|
||||
@ -109,7 +124,7 @@ root.render(<App />);
|
||||
`,
|
||||
},
|
||||
{
|
||||
userId: adminUser.id,
|
||||
projectId: reactProject.id,
|
||||
path: "App.jsx",
|
||||
filename: "App.jsx",
|
||||
isPinned: true,
|
||||
|
@ -1,5 +1,6 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import cuid2 from "@paralleldrive/cuid2";
|
||||
|
||||
export const fileExists = (path: string) => {
|
||||
try {
|
||||
@ -13,3 +14,7 @@ export const fileExists = (path: string) => {
|
||||
export const getProjectDir = () => {
|
||||
return path.resolve(process.cwd(), "storage/tmp/project1");
|
||||
};
|
||||
|
||||
export const uid = cuid2.init({
|
||||
length: 8,
|
||||
});
|
||||
|
@ -1,7 +1,9 @@
|
||||
import { router } from "../api/trpc";
|
||||
import project from "./project";
|
||||
import file from "./file";
|
||||
|
||||
export const appRouter = router({
|
||||
project,
|
||||
file,
|
||||
});
|
||||
|
||||
|
@ -8,17 +8,19 @@ import { TRPCError } from "@trpc/server";
|
||||
const fileRouter = router({
|
||||
getAll: procedure
|
||||
.input(
|
||||
z
|
||||
.object({ id: z.number().array().min(1), isPinned: z.boolean() })
|
||||
.partial()
|
||||
.optional()
|
||||
z.object({
|
||||
projectId: z.number(),
|
||||
id: z.number().array().min(1).optional(),
|
||||
isPinned: z.boolean().optional(),
|
||||
})
|
||||
)
|
||||
.query(async ({ input: opt }) => {
|
||||
const files = await db.query.file.findMany({
|
||||
where: and(
|
||||
eq(file.projectId, opt.projectId),
|
||||
isNull(file.deletedAt),
|
||||
opt?.id && opt.id.length > 0 ? inArray(file.id, opt.id) : undefined,
|
||||
opt?.isPinned ? eq(file.isPinned, true) : undefined
|
||||
opt.id && opt.id.length > 0 ? inArray(file.id, opt.id) : undefined,
|
||||
opt.isPinned ? eq(file.isPinned, true) : undefined
|
||||
),
|
||||
orderBy: [desc(file.isDirectory), asc(file.filename)],
|
||||
columns: !file.isPinned ? { content: true } : undefined,
|
||||
@ -55,7 +57,7 @@ const fileRouter = router({
|
||||
}
|
||||
|
||||
const data: z.infer<typeof insertFileSchema> = {
|
||||
userId: 1,
|
||||
projectId: 1,
|
||||
parentId: input.parentId,
|
||||
path: basePath + input.filename,
|
||||
filename: input.filename,
|
||||
|
100
server/routers/project.ts
Normal file
100
server/routers/project.ts
Normal file
@ -0,0 +1,100 @@
|
||||
import { and, desc, eq, isNull, sql } from "drizzle-orm";
|
||||
import db from "../db";
|
||||
import { procedure, router } from "../api/trpc";
|
||||
import {
|
||||
project,
|
||||
insertProjectSchema,
|
||||
selectProjectSchema,
|
||||
} from "../db/schema/project";
|
||||
import { z } from "zod";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { uid } from "../lib/utils";
|
||||
|
||||
const projectRouter = router({
|
||||
getAll: procedure.query(async () => {
|
||||
const where = and(isNull(project.deletedAt));
|
||||
|
||||
const projects = await db.query.project.findMany({
|
||||
where,
|
||||
with: {
|
||||
user: { columns: { password: false } },
|
||||
},
|
||||
orderBy: [desc(project.id)],
|
||||
});
|
||||
|
||||
return projects;
|
||||
}),
|
||||
|
||||
getById: procedure
|
||||
.input(z.number().or(z.string()))
|
||||
.query(async ({ input }) => {
|
||||
const where = and(
|
||||
typeof input === "string"
|
||||
? eq(project.slug, input)
|
||||
: eq(project.id, input),
|
||||
isNull(project.deletedAt)
|
||||
);
|
||||
|
||||
return db.query.project.findFirst({
|
||||
where,
|
||||
with: {
|
||||
user: { columns: { password: false } },
|
||||
},
|
||||
});
|
||||
}),
|
||||
|
||||
create: procedure
|
||||
.input(
|
||||
insertProjectSchema.pick({
|
||||
title: true,
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
const data: z.infer<typeof insertProjectSchema> = {
|
||||
userId: 1,
|
||||
title: input.title,
|
||||
slug: uid(),
|
||||
};
|
||||
|
||||
const [result] = await db.insert(project).values(data).returning();
|
||||
return result;
|
||||
}),
|
||||
|
||||
update: procedure
|
||||
.input(selectProjectSchema.partial().required({ id: true }))
|
||||
.mutation(async ({ input }) => {
|
||||
const projectData = await db.query.project.findFirst({
|
||||
where: and(eq(project.id, input.id), isNull(project.deletedAt)),
|
||||
});
|
||||
if (!projectData) {
|
||||
throw new TRPCError({ code: "NOT_FOUND" });
|
||||
}
|
||||
|
||||
const [result] = await db
|
||||
.update(project)
|
||||
.set(input)
|
||||
.where(and(eq(project.id, input.id), isNull(project.deletedAt)))
|
||||
.returning();
|
||||
|
||||
return result;
|
||||
}),
|
||||
|
||||
delete: procedure.input(z.number()).mutation(async ({ input }) => {
|
||||
const projectData = await db.query.project.findFirst({
|
||||
where: and(eq(project.id, input), isNull(project.deletedAt)),
|
||||
});
|
||||
if (!projectData) {
|
||||
throw new TRPCError({ code: "NOT_FOUND" });
|
||||
}
|
||||
|
||||
const [result] = await db
|
||||
.update(project)
|
||||
.set({ deletedAt: sql`CURRENT_TIMESTAMP` })
|
||||
.where(eq(project.id, projectData.id))
|
||||
.returning();
|
||||
|
||||
return result;
|
||||
}),
|
||||
});
|
||||
|
||||
export default projectRouter;
|
Loading…
x
Reference in New Issue
Block a user