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 { 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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -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",
|
||||||
|
@ -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";
|
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>
|
||||||
|
@ -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>>;
|
||||||
|
@ -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" }}
|
||||||
|
@ -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>>;
|
||||||
|
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";
|
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
|
||||||
|
@ -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
|
||||||
|
@ -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";
|
||||||
|
@ -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"
|
||||||
|
@ -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
14
pnpm-lock.yaml
generated
@ -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'}
|
||||||
|
@ -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 };
|
||||||
}
|
}
|
||||||
|
@ -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!");
|
||||||
}
|
}
|
||||||
|
@ -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" },
|
||||||
|
@ -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";
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
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", {
|
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")
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
});
|
||||||
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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
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