feat: add project schema

This commit is contained in:
Khairul Hidayat 2024-02-22 20:59:20 +00:00
parent b232410d83
commit d579c1b7c5
27 changed files with 338 additions and 147 deletions

View File

@ -1,6 +1,8 @@
import { type ClassValue, clsx } from "clsx"; import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { BASE_URL } from "./consts"; 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[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)); return twMerge(clsx(inputs));
@ -13,3 +15,13 @@ export function getFileExt(filename: string) {
export function getUrl(...path: string[]) { export function getUrl(...path: string[]) {
return BASE_URL + ("/" + path.join("/")).replace(/\/\/+/g, "/"); 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
);
}

View File

@ -6,7 +6,8 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1", "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", "start": "NODE_ENV=production tsx server/index.ts",
"build": "vite build", "build": "vite build",
"generate": "drizzle-kit generate:sqlite", "generate": "drizzle-kit generate:sqlite",
@ -42,6 +43,7 @@
"@codemirror/lang-json": "^6.0.1", "@codemirror/lang-json": "^6.0.1",
"@emmetio/codemirror6-plugin": "^0.3.0", "@emmetio/codemirror6-plugin": "^0.3.0",
"@hookform/resolvers": "^3.3.4", "@hookform/resolvers": "^3.3.4",
"@paralleldrive/cuid2": "^2.2.2",
"@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-slot": "^1.0.2",

View File

@ -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;

View File

@ -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>>;

View File

@ -3,19 +3,15 @@ import { useData } from "~/renderer/hooks";
import Link from "~/renderer/link"; import Link from "~/renderer/link";
const HomePage = () => { const HomePage = () => {
const { posts } = useData<Data>(); const { projects } = useData<Data>();
if (!posts?.length) {
return <p>No posts.</p>;
}
return ( return (
<div> <div>
<h1>Posts</h1> <h1>Posts</h1>
{posts.map((post: any) => ( {projects.map((project: any) => (
<Link key={post.id} href={`/${post.id}`}> <Link key={project.id} href={`/${project.slug}`}>
{post.title} {project.title}
</Link> </Link>
))} ))}
</div> </div>

View File

@ -1,9 +1,12 @@
export const data = async () => { import { PageContext } from "vike/types";
const posts = await fetch( import trpcServer from "~/server/api/trpc/trpc";
"https://jsonplaceholder.typicode.com/posts?_limit=20"
).then((response) => response.json());
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>>; export type Data = Awaited<ReturnType<typeof data>>;

View File

@ -6,22 +6,21 @@ import {
import WebPreview from "./components/web-preview"; import WebPreview from "./components/web-preview";
import Editor from "./components/editor"; import Editor from "./components/editor";
import ProjectContext from "./context/project"; import ProjectContext from "./context/project";
import { cn } from "~/lib/utils"; import { cn, getPreviewUrl } from "~/lib/utils";
import { useParams, useSearchParams } from "~/renderer/hooks"; import { useData, useSearchParams } from "~/renderer/hooks";
import { BASE_URL } from "~/lib/consts";
import { withClientOnly } from "~/renderer/client-only"; import { withClientOnly } from "~/renderer/client-only";
import Spinner from "~/components/ui/spinner"; import Spinner from "~/components/ui/spinner";
import { Data } from "./+data";
const ViewProjectPage = () => { const ViewProjectPage = () => {
const { project } = useData<Data>();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const params = useParams();
const isCompact = const isCompact =
searchParams.get("compact") === "1" || searchParams.get("embed") === "1"; searchParams.get("compact") === "1" || searchParams.get("embed") === "1";
const slug = params["slug"]; const previewUrl = getPreviewUrl(project, "index.html");
const previewUrl = BASE_URL + `/api/preview/${slug}/index.html`;
return ( return (
<ProjectContext.Provider value={{ slug, isCompact }}> <ProjectContext.Provider value={{ project, isCompact }}>
<ResizablePanelGroup <ResizablePanelGroup
autoSaveId="main-panel" autoSaveId="main-panel"
direction={{ sm: "vertical", md: "horizontal" }} direction={{ sm: "vertical", md: "horizontal" }}

View File

@ -1,12 +1,25 @@
import { PageContext } from "vike/types"; import { PageContext } from "vike/types";
import { render } from "vike/abort";
import trpcServer from "~/server/api/trpc/trpc"; import trpcServer from "~/server/api/trpc/trpc";
export const data = async (ctx: PageContext) => { export const data = async (ctx: PageContext) => {
const trpc = await trpcServer(ctx); 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>>; export type Data = Awaited<ReturnType<typeof data>>;

View File

@ -0,0 +1 @@
export default "/@slug";

View File

@ -23,9 +23,9 @@ import { Data } from "../+data";
import { useBreakpoint } from "~/hooks/useBreakpoint"; import { useBreakpoint } from "~/hooks/useBreakpoint";
const Editor = () => { const Editor = () => {
const { pinnedFiles } = useData<Data>(); const { project, pinnedFiles } = useData<Data>();
const trpcUtils = trpc.useUtils(); const trpcUtils = trpc.useUtils();
const project = useProjectContext(); const projectCtx = useProjectContext();
const sidebarPanel = useRef<ImperativePanelHandle>(null); const sidebarPanel = useRef<ImperativePanelHandle>(null);
const [breakpoint] = useBreakpoint(); const [breakpoint] = useBreakpoint();
@ -36,7 +36,7 @@ const Editor = () => {
); );
const openedFilesData = trpc.file.getAll.useQuery( const openedFilesData = trpc.file.getAll.useQuery(
{ id: curOpenFiles }, { projectId: project.id, id: curOpenFiles },
{ enabled: curOpenFiles.length > 0, initialData: pinnedFiles } { enabled: curOpenFiles.length > 0, initialData: pinnedFiles }
); );
const [openedFiles, setOpenedFiles] = useState<any[]>(pinnedFiles); const [openedFiles, setOpenedFiles] = useState<any[]>(pinnedFiles);
@ -156,7 +156,7 @@ const Editor = () => {
}) satisfies Tab[]; }) satisfies Tab[];
}, [curOpenFiles, openedFiles, refreshPreview]); }, [curOpenFiles, openedFiles, refreshPreview]);
const PanelComponent = !project.isCompact ? Panel : "div"; const PanelComponent = !projectCtx.isCompact ? Panel : "div";
return ( return (
<EditorContext.Provider <EditorContext.Provider

View File

@ -1,6 +1,4 @@
"use client"; import { Fragment, useMemo, useState } from "react";
import React, { Fragment, useMemo, useState } from "react";
import { UseDiscloseReturn, useDisclose } from "~/hooks/useDisclose"; import { UseDiscloseReturn, useDisclose } from "~/hooks/useDisclose";
import { import {
FiChevronRight, FiChevronRight,
@ -21,27 +19,28 @@ import {
DropdownMenuTrigger, DropdownMenuTrigger,
DropdownMenuSeparator, DropdownMenuSeparator,
} from "~/components/ui/dropdown-menu"; } 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 FileIcon from "~/components/ui/file-icon";
import copy from "copy-to-clipboard"; import copy from "copy-to-clipboard";
import { useData, useParams } from "~/renderer/hooks"; import { useData } from "~/renderer/hooks";
import Spinner from "~/components/ui/spinner"; import Spinner from "~/components/ui/spinner";
import { Data } from "../+data"; import { Data } from "../+data";
const FileListing = () => { const FileListing = () => {
const pageData = useData<Data>(); const { project, files: initialFiles } = useData<Data>();
const { onOpenFile, onFileChanged } = useEditorContext(); const { onOpenFile, onFileChanged } = useEditorContext();
const createFileDlg = useDisclose<CreateFileSchema>(); const createFileDlg = useDisclose<CreateFileSchema>();
const files = trpc.file.getAll.useQuery(undefined, { const files = trpc.file.getAll.useQuery(
initialData: pageData.files, { projectId: project.id },
}); { initialData: initialFiles }
);
const fileList = useMemo(() => groupFiles(files.data, null), [files.data]); const fileList = useMemo(() => groupFiles(files.data, null), [files.data]);
return ( return (
<Fragment> <Fragment>
<div className="h-10 flex items-center pl-4 pr-1"> <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 <ActionButton
icon={FiFilePlus} icon={FiFilePlus}
onClick={() => createFileDlg.onOpen()} onClick={() => createFileDlg.onOpen()}
@ -104,7 +103,7 @@ type FileItemProps = {
}; };
const FileItem = ({ file, createFileDlg }: FileItemProps) => { const FileItem = ({ file, createFileDlg }: FileItemProps) => {
const { slug } = useParams(); const { project } = useData<Data>();
const { onOpenFile, onDeleteFile } = useEditorContext(); const { onOpenFile, onDeleteFile } = useEditorContext();
const [isCollapsed, setCollapsed] = useState(false); const [isCollapsed, setCollapsed] = useState(false);
const trpcUtils = trpc.useUtils(); const trpcUtils = trpc.useUtils();
@ -186,16 +185,13 @@ const FileItem = ({ file, createFileDlg }: FileItemProps) => {
Copy Path Copy Path
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
onClick={() => copy(getUrl(`api/preview/${slug}`, file.path))} onClick={() => copy(getPreviewUrl(project, file))}
> >
Copy URL Copy URL
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
onClick={() => onClick={() =>
window.open( window.open(getPreviewUrl(project, file), "_blank")
getUrl(`api/preview/${slug}`, file.path),
"_blank"
)
} }
> >
Open in new tab Open in new tab

View File

@ -1,5 +1,3 @@
"use client";
import { getFileExt } from "~/lib/utils"; import { getFileExt } from "~/lib/utils";
import CodeEditor from "../../../../components/ui/code-editor"; import CodeEditor from "../../../../components/ui/code-editor";
import trpc from "~/lib/trpc"; import trpc from "~/lib/trpc";

View File

@ -1,4 +1,3 @@
import React from "react";
import FileListing from "./file-listing"; import FileListing from "./file-listing";
import { FaUserCircle } from "react-icons/fa"; import { FaUserCircle } from "react-icons/fa";
import { Button } from "~/components/ui/button"; import { Button } from "~/components/ui/button";
@ -7,6 +6,7 @@ const Sidebar = () => {
return ( return (
<aside className="flex flex-col items-stretch h-full"> <aside className="flex flex-col items-stretch h-full">
<FileListing /> <FileListing />
<div className="h-12 bg-[#1a1b26] pl-12"> <div className="h-12 bg-[#1a1b26] pl-12">
<Button <Button
variant="ghost" variant="ghost"

View File

@ -1,7 +1,8 @@
import { createContext, useContext } from "react"; import { createContext, useContext } from "react";
import type { ProjectSchema } from "~/server/db/schema/project";
type TProjectContext = { type TProjectContext = {
slug: string; project: ProjectSchema;
isCompact?: boolean; isCompact?: boolean;
}; };

14
pnpm-lock.yaml generated
View File

@ -23,6 +23,9 @@ dependencies:
'@hookform/resolvers': '@hookform/resolvers':
specifier: ^3.3.4 specifier: ^3.3.4
version: 3.3.4(react-hook-form@7.50.1) version: 3.3.4(react-hook-form@7.50.1)
'@paralleldrive/cuid2':
specifier: ^2.2.2
version: 2.2.2
'@radix-ui/react-dialog': '@radix-ui/react-dialog':
specifier: ^1.0.5 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) 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 os-filter-obj: 2.0.0
dev: true 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: /@nodelib/fs.scandir@2.1.5:
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
@ -1342,6 +1350,12 @@ packages:
'@nodelib/fs.scandir': 2.1.5 '@nodelib/fs.scandir': 2.1.5
fastq: 1.17.1 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: /@pkgjs/parseargs@0.11.0:
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'} engines: {node: '>=14'}

View File

@ -2,10 +2,12 @@ import type { PageContext } from "vike/types";
export function getPageMetadata(pageContext: PageContext) { export function getPageMetadata(pageContext: PageContext) {
let title = pageContext.data?.title || pageContext.config.title; let title = pageContext.data?.title || pageContext.config.title;
title = title ? `${title} - Vike` : "Welcome to Vike"; title = title ? `${title} - CodeShare` : "Welcome to CodeShare";
const description = const description =
pageContext.data?.description || pageContext.config.description || ""; pageContext.data?.description ||
pageContext.config.description ||
"Share your frontend result with everyone";
return { title, description }; return { title, description };
} }

View File

@ -7,15 +7,26 @@ import { serveHtml } from "./serve-html";
import { serveJs } from "./serve-js"; import { serveJs } from "./serve-js";
import { getMimeType } from "~/server/lib/mime"; import { getMimeType } from "~/server/lib/mime";
import { postcss } from "./postcss"; import { postcss } from "./postcss";
import { project } from "~/server/db/schema/project";
const get = async (req: Request, res: Response) => { const get = async (req: Request, res: Response) => {
const { slug, ...pathParams } = req.params as any; const { slug, ...pathParams } = req.params as any;
const path = pathParams[0]; const path = pathParams[0];
const fileData = await db.query.file.findFirst({ const projectData = await db.query.project.findFirst({
where: and(eq(file.path, path), isNull(file.deletedAt)), 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) { if (!fileData) {
return res.status(404).send("File not found!"); return res.status(404).send("File not found!");
} }

View File

@ -1,12 +1,12 @@
import db from "~/server/db"; import db from "~/server/db";
import { FileSchema, file } from "~/server/db/schema/file"; import { FileSchema, file } from "~/server/db/schema/file";
import { and, eq, isNull } from "drizzle-orm"; import { and, eq, isNull } from "drizzle-orm";
import { IS_DEV } from "~/server/lib/consts";
const preventHtmlDirectAccess = `<script>if (window === window.parent) {window.location.href = '/';}</script>`;
export const serveHtml = async (fileData: FileSchema) => { export const serveHtml = async (fileData: FileSchema) => {
const layout = await db.query.file.findFirst({ const layout = await db.query.file.findFirst({
where: and( where: and(
eq(file.projectId, fileData.projectId),
eq(file.filename, "_layout.html"), eq(file.filename, "_layout.html"),
fileData.parentId fileData.parentId
? eq(file.parentId, fileData.parentId) ? eq(file.parentId, fileData.parentId)
@ -22,10 +22,14 @@ export const serveHtml = async (fileData: FileSchema) => {
const bodyOpeningTagIdx = content.indexOf("<body"); const bodyOpeningTagIdx = content.indexOf("<body");
const firstScriptTagIdx = content.indexOf("<script", bodyOpeningTagIdx); const firstScriptTagIdx = content.indexOf("<script", bodyOpeningTagIdx);
const injectScripts = [ const injectScripts = ['<script src="/js/hook-console.js"></script>'];
'<script src="/js/hook-console.js"></script>',
preventHtmlDirectAccess, if (!IS_DEV) {
]; // prevent direct access
injectScripts.push(
`<script>if (window === window.parent) {window.location.href = '/';}</script>`
);
}
const importMaps = [ const importMaps = [
{ name: "react", url: "https://esm.sh/react@18.2.0" }, { name: "react", url: "https://esm.sh/react@18.2.0" },

View File

@ -1,2 +1,3 @@
export { user } from "./user"; export { user } from "./user";
export { file } from "./file"; export { project, projectRelations } from "./project";
export { file, fileRelations } from "./file";

View File

@ -1,23 +1,24 @@
import { sql } from "drizzle-orm"; import { relations, sql } from "drizzle-orm";
import { import {
integer, integer,
sqliteTable, sqliteTable,
text, text,
foreignKey, foreignKey,
index,
} from "drizzle-orm/sqlite-core"; } from "drizzle-orm/sqlite-core";
import { createInsertSchema, createSelectSchema } from "drizzle-zod"; import { createInsertSchema, createSelectSchema } from "drizzle-zod";
import { z } from "zod"; import { z } from "zod";
import { user } from "./user"; import { project } from "./project";
export const file = sqliteTable( export const file = sqliteTable(
"files", "files",
{ {
id: integer("id").primaryKey({ autoIncrement: true }), id: integer("id").primaryKey({ autoIncrement: true }),
parentId: integer("parent_id"), projectId: integer("project_id")
userId: integer("user_id")
.notNull() .notNull()
.references(() => user.id), .references(() => project.id),
path: text("path").notNull().unique(), parentId: integer("parent_id"),
path: text("path").notNull(),
filename: text("filename").notNull(), filename: text("filename").notNull(),
isDirectory: integer("is_directory", { mode: "boolean" }) isDirectory: integer("is_directory", { mode: "boolean" })
.notNull() .notNull()
@ -38,9 +39,18 @@ export const file = sqliteTable(
foreignColumns: [table.id], foreignColumns: [table.id],
name: "parent_id_fk", 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 insertFileSchema = createInsertSchema(file);
export const selectFileSchema = createSelectSchema(file); export const selectFileSchema = createSelectSchema(file);

View 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>;

View File

@ -5,6 +5,7 @@ import { z } from "zod";
export const user = sqliteTable("users", { export const user = sqliteTable("users", {
id: integer("id").primaryKey({ autoIncrement: true }), id: integer("id").primaryKey({ autoIncrement: true }),
name: text("name").notNull(),
email: text("email").notNull().unique(), email: text("email").notNull().unique(),
password: text("password").notNull(), password: text("password").notNull(),
createdAt: text("created_at") createdAt: text("created_at")

View File

@ -1,68 +1,83 @@
import db from "."; import db from ".";
import { hashPassword } from "../lib/crypto"; import { hashPassword } from "../lib/crypto";
import { uid } from "../lib/utils";
import { file } from "./schema/file"; import { file } from "./schema/file";
import { project } from "./schema/project";
import { user } from "./schema/user"; import { user } from "./schema/user";
const main = async () => { const main = async () => {
const [adminUser] = await db const [adminUser] = await db
.insert(user) .insert(user)
.values({ .values({
name: "Admin",
email: "admin@mail.com", email: "admin@mail.com",
password: await hashPassword("123456"), password: await hashPassword("123456"),
}) })
.returning(); .returning();
// await db const [vanillaProject] = await db
// .insert(file) .insert(project)
// .values([ .values({ userId: adminUser.id, slug: uid(), title: "Vanilla Project" })
// { .returning();
// 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>
// <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> <link rel="stylesheet" href="styles.css">
// </head> </head>
// <body> <body>
// {CONTENT} {CONTENT}
// <script src="script.js" type="module" defer></script> <script src="scripts.js"></script>
// </body> </body>
// </html>`, </html>`,
// }, },
// ]) ])
// .execute(); .execute();
const [reactProject] = await db
.insert(project)
.values({ userId: adminUser.id, slug: uid(), title: "React Project" })
.returning();
// react template // react template
await db await db
.insert(file) .insert(file)
.values([ .values([
{ {
userId: adminUser.id, projectId: reactProject.id,
path: "index.html", path: "index.html",
filename: "index.html", filename: "index.html",
content: `<!doctype html> content: `<!doctype html>
@ -84,7 +99,7 @@ const main = async () => {
`, `,
}, },
{ {
userId: adminUser.id, projectId: reactProject.id,
path: "globals.css", path: "globals.css",
filename: "globals.css", filename: "globals.css",
content: `@tailwind base; content: `@tailwind base;
@ -97,7 +112,7 @@ body {
`, `,
}, },
{ {
userId: adminUser.id, projectId: reactProject.id,
path: "index.jsx", path: "index.jsx",
filename: "index.jsx", filename: "index.jsx",
content: `import React from "react"; content: `import React from "react";
@ -109,7 +124,7 @@ root.render(<App />);
`, `,
}, },
{ {
userId: adminUser.id, projectId: reactProject.id,
path: "App.jsx", path: "App.jsx",
filename: "App.jsx", filename: "App.jsx",
isPinned: true, isPinned: true,

View File

@ -1,5 +1,6 @@
import fs from "node:fs"; import fs from "node:fs";
import path from "node:path"; import path from "node:path";
import cuid2 from "@paralleldrive/cuid2";
export const fileExists = (path: string) => { export const fileExists = (path: string) => {
try { try {
@ -13,3 +14,7 @@ export const fileExists = (path: string) => {
export const getProjectDir = () => { export const getProjectDir = () => {
return path.resolve(process.cwd(), "storage/tmp/project1"); return path.resolve(process.cwd(), "storage/tmp/project1");
}; };
export const uid = cuid2.init({
length: 8,
});

View File

@ -1,7 +1,9 @@
import { router } from "../api/trpc"; import { router } from "../api/trpc";
import project from "./project";
import file from "./file"; import file from "./file";
export const appRouter = router({ export const appRouter = router({
project,
file, file,
}); });

View File

@ -8,17 +8,19 @@ import { TRPCError } from "@trpc/server";
const fileRouter = router({ const fileRouter = router({
getAll: procedure getAll: procedure
.input( .input(
z z.object({
.object({ id: z.number().array().min(1), isPinned: z.boolean() }) projectId: z.number(),
.partial() id: z.number().array().min(1).optional(),
.optional() isPinned: z.boolean().optional(),
})
) )
.query(async ({ input: opt }) => { .query(async ({ input: opt }) => {
const files = await db.query.file.findMany({ const files = await db.query.file.findMany({
where: and( where: and(
eq(file.projectId, opt.projectId),
isNull(file.deletedAt), isNull(file.deletedAt),
opt?.id && opt.id.length > 0 ? inArray(file.id, opt.id) : undefined, opt.id && opt.id.length > 0 ? inArray(file.id, opt.id) : undefined,
opt?.isPinned ? eq(file.isPinned, true) : undefined opt.isPinned ? eq(file.isPinned, true) : undefined
), ),
orderBy: [desc(file.isDirectory), asc(file.filename)], orderBy: [desc(file.isDirectory), asc(file.filename)],
columns: !file.isPinned ? { content: true } : undefined, columns: !file.isPinned ? { content: true } : undefined,
@ -55,7 +57,7 @@ const fileRouter = router({
} }
const data: z.infer<typeof insertFileSchema> = { const data: z.infer<typeof insertFileSchema> = {
userId: 1, projectId: 1,
parentId: input.parentId, parentId: input.parentId,
path: basePath + input.filename, path: basePath + input.filename,
filename: input.filename, filename: input.filename,

100
server/routers/project.ts Normal file
View 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;