Merge pull request #1 from khairul169/rebuild

feat: rebuild app using vike
This commit is contained in:
Khairul Hidayat 2024-02-22 19:16:09 +07:00 committed by GitHub
commit 2c9faa332a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
104 changed files with 2214 additions and 2976 deletions

View File

@ -1,3 +0,0 @@
{
"extends": "next/core-web-vitals"
}

43
.gitignore vendored
View File

@ -1,42 +1,5 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
node_modules/
dist/
build/
storage/**/*
!.gitkeep
.env
dist/

23
.swcrc Normal file
View File

@ -0,0 +1,23 @@
{
"$schema": "https://json.schemastore.org/swcrc",
"jsc": {
"parser": {
"syntax": "ecmascript",
"jsx": false,
"dynamicImport": false,
"privateMethod": false,
"functionBind": false,
"exportDefaultFrom": false,
"exportNamespaceFrom": false,
"decorators": false,
"decoratorsBeforeExport": false,
"topLevelAwait": false,
"importMeta": false
},
"target": "es2020",
"loose": false,
"externalHelpers": false,
"keepClassNames": false
},
"minify": false
}

View File

@ -1,36 +0,0 @@
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.

View File

@ -5,13 +5,13 @@
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "src/app/globals.css",
"css": "renderer/globals.css",
"baseColor": "slate",
"cssVariables": false,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils"
"components": "~/components",
"utils": "~/lib/utils"
}
}

View File

@ -0,0 +1,45 @@
import React from "react";
type ErrorBoundaryProps = {
fallback?: React.ReactNode;
children?: React.ReactNode;
};
type ErrorBoundaryState = {
error?: Error | null;
};
class ErrorBoundary extends React.Component<
ErrorBoundaryProps,
ErrorBoundaryState
> {
constructor(props: ErrorBoundaryProps) {
super(props);
this.state = { error: null };
}
static getDerivedStateFromError(error: unknown) {
return { error };
}
render() {
const { children, fallback } = this.props;
const { error } = this.state;
if (error) {
if (!fallback) {
return (
<div className="p-4 text-sm">
<p>An error occured!</p>
</div>
);
}
return fallback;
}
return children;
}
}
export default ErrorBoundary;

View File

@ -1,6 +1,6 @@
/* eslint-disable react/display-name */
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { Button } from "~/components/ui/button";
import { cn } from "~/lib/utils";
import React, { forwardRef } from "react";
import { IconType } from "react-icons/lib";

View File

@ -2,7 +2,7 @@ import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
import { cn } from "~/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-white transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-950 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 dark:ring-offset-slate-950 dark:focus-visible:ring-slate-300",

View File

@ -17,8 +17,8 @@ import prettierCssPlugin from "prettier/plugins/postcss";
import prettierBabelPlugin from "prettier/plugins/babel";
import * as prettierPluginEstree from "prettier/plugins/estree";
import { abbreviationTracker } from "@emmetio/codemirror6-plugin";
import { useDebounce } from "@/hooks/useDebounce";
import useCommandKey from "@/hooks/useCommandKey";
import { useDebounce } from "~/hooks/useDebounce";
import useCommandKey from "~/hooks/useCommandKey";
type Props = {
lang?: string;

View File

@ -4,7 +4,7 @@ import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { X } from "lucide-react";
import { cn } from "@/lib/utils";
import { cn } from "~/lib/utils";
const Dialog = DialogPrimitive.Root;

View File

@ -4,7 +4,7 @@ import * as React from "react";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { Check, ChevronRight, Circle } from "lucide-react";
import { cn } from "@/lib/utils";
import { cn } from "~/lib/utils";
const DropdownMenu = DropdownMenuPrimitive.Root;

View File

@ -1,4 +1,4 @@
import { FileSchema } from "@/server/db/schema/file";
import { FileSchema } from "~/server/db/schema/file";
import { ComponentProps } from "react";
import { FiFile, FiFolder } from "react-icons/fi";

View File

@ -1,4 +1,4 @@
import { useForm } from "@/hooks/useForm";
import { useForm } from "~/hooks/useForm";
import { FieldValues } from "react-hook-form";
import React from "react";

View File

@ -1,6 +1,6 @@
import * as React from "react"
import * as React from "react";
import { cn } from "@/lib/utils"
import { cn } from "~/lib/utils";
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
@ -17,9 +17,9 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
ref={ref}
{...props}
/>
)
);
}
)
Input.displayName = "Input"
);
Input.displayName = "Input";
export { Input }
export { Input };

View File

@ -1,4 +1,4 @@
import { cn } from "@/lib/utils";
import { cn } from "~/lib/utils";
import React from "react";
type Props = {

View File

@ -3,7 +3,7 @@
import { GripVertical } from "lucide-react";
import * as ResizablePrimitive from "react-resizable-panels";
import { cn } from "@/lib/utils";
import { cn } from "~/lib/utils";
const ResizablePanelGroup = ({
className,

12
components/ui/spinner.tsx Normal file
View File

@ -0,0 +1,12 @@
import { Loader2 } from "lucide-react";
import { cn } from "~/lib/utils";
type Spinner = {
className?: string;
};
const Spinner = ({ className }: Spinner) => {
return <Loader2 className={cn("size-8 animate-spin", className)} />;
};
export default Spinner;

View File

@ -1,4 +1,4 @@
import { cn } from "@/lib/utils";
import { cn } from "~/lib/utils";
import React, { useEffect, useMemo, useRef } from "react";
import ActionButton from "./action-button";
import { FiX } from "react-icons/fi";

View File

@ -1,11 +1,10 @@
import path from "node:path";
import type { Config } from "drizzle-kit";
export default {
schema: "./src/server/db/schema/_schema.ts",
out: "./src/server/db/drizzle",
schema: "./server/db/schema/_schema.ts",
out: "./server/db/drizzle",
driver: "better-sqlite",
dbCredentials: {
url: path.join(process.cwd(), "storage/database.db"),
url: "./storage/database.db",
},
} satisfies Config;

2
lib/consts.ts Normal file
View File

@ -0,0 +1,2 @@
export const BASE_URL =
typeof window !== "undefined" ? location.protocol + "//" + location.host : "";

View File

@ -1,5 +1,5 @@
import { createTRPCReact } from "@trpc/react-query";
import type { AppRouter } from "@/server/routers/_app";
import type { AppRouter } from "~/server/routers/_app";
export const getBaseUrl = () => {
if (typeof window !== "undefined") return "";

View File

@ -1,4 +0,0 @@
/** @type {import('next').NextConfig} */
const nextConfig = {};
export default nextConfig;

View File

@ -1,24 +1,41 @@
{
"name": "code-share",
"version": "0.1.0",
"private": true,
"name": "vike",
"version": "1.0.0",
"description": "",
"main": "server/index.ts",
"type": "module",
"scripts": {
"dev": "concurrently --kill-others \"next dev\" \"npm run transformer:dev\"",
"build": "next build && npm run transformer:build",
"start": "concurrently --kill-others \"next start\" \"npm run transformer:start\"",
"lint": "next lint",
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "tsx watch --ignore *.mjs server/index.ts",
"start": "NODE_ENV=production tsx server/index.ts",
"build": "vite build",
"generate": "drizzle-kit generate:sqlite",
"drop": "drizzle-kit drop",
"push": "drizzle-kit push:sqlite",
"migrate": "tsx src/server/db/migrate.ts",
"seed": "tsx src/server/db/seed.ts",
"reset": "rm -f storage/database.db && npm run push && npm run seed",
"transformer:start": "node dist/transformer/server.js",
"transformer:dev": "tsx --watch src/server/transformer/server.ts",
"transformer:build": "tsc --project tsconfig-server.json"
"migrate": "tsx server/db/migrate.ts",
"seed": "tsx server/db/seed.ts",
"reset": "rm -f storage/database.db && npm run push && npm run seed"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@swc/cli": "^0.3.9",
"@types/express": "^4.17.21",
"@types/node": "^20.11.19",
"@types/nprogress": "^0.2.3",
"@types/react": "^18.2.57",
"@types/react-dom": "^18.2.19",
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.0.1",
"drizzle-kit": "^0.20.14",
"postcss": "^8",
"tailwindcss": "^3.3.0",
"tsx": "^4.7.1",
"typescript": "^5.3.3",
"vite": "^5.1.4"
},
"dependencies": {
"@babel/preset-typescript": "^7.23.3",
"@codemirror/lang-css": "^6.2.1",
"@codemirror/lang-html": "^6.4.8",
"@codemirror/lang-javascript": "^6.2.1",
@ -32,49 +49,33 @@
"@swc/core": "^1.4.2",
"@tanstack/react-query": "^5.21.7",
"@trpc/client": "11.0.0-next-beta.289",
"@trpc/next": "11.0.0-next-beta.289",
"@trpc/react-query": "11.0.0-next-beta.289",
"@trpc/server": "11.0.0-next-beta.289",
"@types/express": "^4.17.21",
"@uiw/codemirror-theme-tokyo-night": "^4.21.22",
"@uiw/react-codemirror": "^4.21.22",
"bcrypt": "^5.1.1",
"better-sqlite3": "^9.4.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"console-feed": "^3.5.0",
"copy-to-clipboard": "^3.3.3",
"drizzle-orm": "^0.29.3",
"drizzle-zod": "^0.5.1",
"express": "^4.18.2",
"lucide-react": "^0.331.0",
"mime": "^4.0.1",
"next": "14.1.0",
"nprogress": "^0.2.0",
"prettier": "^3.2.5",
"react": "^18",
"react-dom": "^18",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.50.1",
"react-icons": "^5.0.1",
"react-resizable-panels": "^2.0.9",
"tailwind-merge": "^2.2.1",
"tailwindcss-animate": "^1.0.7",
"usehooks-ts": "^2.14.0",
"vike": "^0.4.162",
"zod": "^3.22.4",
"zustand": "^4.5.1"
},
"devDependencies": {
"@types/bcrypt": "^5.0.2",
"@types/better-sqlite3": "^7.6.9",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"autoprefixer": "^10.0.1",
"concurrently": "^8.2.2",
"drizzle-kit": "^0.20.14",
"eslint": "^8",
"eslint-config-next": "14.1.0",
"postcss": "^8",
"tailwindcss": "^3.3.0",
"tsx": "^4.7.1",
"typescript": "^5"
}
}

17
pages/@id/+Page.tsx Normal file
View File

@ -0,0 +1,17 @@
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;

12
pages/@id/+data.ts Normal file
View File

@ -0,0 +1,12 @@
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>>;

20
pages/_error/+Page.tsx Normal file
View File

@ -0,0 +1,20 @@
import { usePageContext } from "~/renderer/context";
const Page = () => {
const pageContext = usePageContext();
let { abortReason } = pageContext;
if (!abortReason) {
abortReason = pageContext.is404
? "Page not found."
: "Something went wrong.";
}
return (
<div>
<p style={{ fontSize: "1.3em" }}>{abortReason}</p>
</div>
);
};
export default Page;

24
pages/index/+Page.tsx Normal file
View File

@ -0,0 +1,24 @@
import type { Data } from "./+data";
import { useData } from "~/renderer/hooks";
import Link from "~/renderer/link";
const HomePage = () => {
const { posts } = useData<Data>();
if (!posts?.length) {
return <p>No posts.</p>;
}
return (
<div>
<h1>Posts</h1>
{posts.map((post: any) => (
<Link key={post.id} href={`/${post.id}`}>
{post.title}
</Link>
))}
</div>
);
};
export default HomePage;

9
pages/index/+data.ts Normal file
View File

@ -0,0 +1,9 @@
export const data = async () => {
const posts = await fetch(
"https://jsonplaceholder.typicode.com/posts?_limit=20"
).then((response) => response.json());
return { posts };
};
export type Data = Awaited<ReturnType<typeof data>>;

View File

@ -1,39 +1,32 @@
"use client";
import React, { useCallback, useEffect, useRef, useState } from "react";
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from "@/components/ui/resizable";
import WebPreview from "./_components/web-preview";
import { usePortrait } from "@/hooks/usePortrait";
import Editor from "./_components/editor";
} from "~/components/ui/resizable";
import WebPreview from "./components/web-preview";
import { usePortrait } from "~/hooks/usePortrait";
import Editor from "./components/editor";
import ProjectContext from "./context/project";
import { cn } from "@/lib/utils";
import { useSearchParams } from "next/navigation";
import { cn } from "~/lib/utils";
import { withClientOnly } from "~/renderer/client-only";
import { useParams, useSearchParams } from "~/renderer/hooks";
import { BASE_URL } from "~/lib/consts";
const ViewProjectPage = () => {
const [isMounted, setMounted] = useState(false);
const isPortrait = usePortrait();
const searchParams = useSearchParams();
const params = useParams();
const isCompact =
searchParams.get("compact") === "1" || searchParams.get("embed") === "1";
useEffect(() => {
setMounted(true);
}, []);
if (!isMounted) {
return null;
}
const slug = params["slug"];
const previewUrl = BASE_URL + `/api/preview/${slug}/index.html`;
return (
<ProjectContext.Provider value={{ isCompact }}>
<ProjectContext.Provider value={{ slug, isCompact }}>
<ResizablePanelGroup
autoSaveId="main-panel"
direction={isPortrait ? "vertical" : "horizontal"}
className={cn("w-full !h-dvh", !isCompact ? "md:p-4" : "")}
className={cn("w-full !h-dvh bg-slate-600", !isCompact ? "md:p-4" : "")}
>
<ResizablePanel
defaultSize={isPortrait ? 50 : 60}
@ -57,11 +50,11 @@ const ViewProjectPage = () => {
collapsedSize={0}
minSize={10}
>
<WebPreview />
<WebPreview url={previewUrl} />
</ResizablePanel>
</ResizablePanelGroup>
</ProjectContext.Provider>
);
};
export default ViewProjectPage;
export default withClientOnly(ViewProjectPage);

View File

@ -0,0 +1,42 @@
import { useEffect, useState } from "react";
import { Console, Decode } from "console-feed";
import type { Message } from "console-feed/lib/definitions/Console";
import ErrorBoundary from "~/components/containers/error-boundary";
const ConsoleLogger = () => {
const [logs, setLogs] = useState<any[]>([]);
useEffect(() => {
const onMessage = (event: MessageEvent<any>) => {
const { data: eventData } = event;
if (!eventData || eventData.type !== "console") {
return;
}
const data = Decode(eventData.data);
if (!data || !data.method || !data.data) {
return;
}
setLogs((i) => [data, ...i]);
};
window.addEventListener("message", onMessage);
return () => {
window.removeEventListener("message", onMessage);
};
}, []);
return (
<div className="h-full flex flex-col bg-[#242424] border-t border-t-gray-600">
<p className="py-2 px-3 uppercase text-xs">Console</p>
<ErrorBoundary>
<div className="overflow-y-auto flex-1">
<Console logs={logs} variant="dark" />
</div>
</ErrorBoundary>
</div>
);
};
export default ConsoleLogger;

View File

@ -1,18 +1,17 @@
import React, { useMemo } from "react";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "../../../../components/ui/dialog";
import { UseDiscloseReturn } from "@/hooks/useDisclose";
import { UseDiscloseReturn } from "~/hooks/useDisclose";
import { Input } from "../../../../components/ui/input";
import { Button } from "../../../../components/ui/button";
import { useForm } from "@/hooks/useForm";
import { useForm } from "~/hooks/useForm";
import { z } from "zod";
import FormErrorMessage from "../../../../components/ui/form-error-message";
import trpc from "@/lib/trpc";
import type { FileSchema } from "@/server/db/schema/file";
import trpc from "~/lib/trpc";
import type { FileSchema } from "~/server/db/schema/file";
import { useWatch } from "react-hook-form";
type Props = {

View File

@ -1,28 +1,22 @@
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from "@/components/ui/resizable";
import Tabs, { Tab } from "@/components/ui/tabs";
} from "~/components/ui/resizable";
import Tabs, { Tab } from "~/components/ui/tabs";
import FileViewer from "./file-viewer";
import trpc from "@/lib/trpc";
import trpc from "~/lib/trpc";
import EditorContext from "../context/editor";
import type { FileSchema } from "@/server/db/schema/file";
import { usePortrait } from "@/hooks/usePortrait";
import Panel from "@/components/ui/panel";
import { previewStore } from "./web-preview";
import type { FileSchema } from "~/server/db/schema/file";
import { usePortrait } from "~/hooks/usePortrait";
import Panel from "~/components/ui/panel";
import { previewStore } from "../stores/web-preview";
import { useProjectContext } from "../context/project";
import { ImperativePanelHandle } from "react-resizable-panels";
import Sidebar from "./sidebar";
import useCommandKey from "@/hooks/useCommandKey";
import { Button } from "@/components/ui/button";
import useCommandKey from "~/hooks/useCommandKey";
import { Button } from "~/components/ui/button";
import { FaCompress, FaCompressArrowsAlt } from "react-icons/fa";
import ConsoleLogger from "./console-logger";
@ -194,7 +188,7 @@ const Editor = () => {
autoCapitalize="code-editor"
direction="vertical"
>
<ResizablePanel defaultSize={100} minSize={20}>
<ResizablePanel defaultSize={isPortrait ? 100 : 80} minSize={20}>
<Tabs
tabs={openFileList}
current={curTabIdx}

View File

@ -1,7 +1,7 @@
"use client";
import React, { Fragment, useMemo, useState } from "react";
import { UseDiscloseReturn, useDisclose } from "@/hooks/useDisclose";
import { UseDiscloseReturn, useDisclose } from "~/hooks/useDisclose";
import {
FiChevronRight,
FiFilePlus,
@ -9,8 +9,8 @@ import {
FiMoreVertical,
} from "react-icons/fi";
import { FaCheck, FaThumbtack } from "react-icons/fa";
import trpc from "@/lib/trpc";
import type { FileSchema } from "@/server/db/schema/file";
import trpc from "~/lib/trpc";
import type { FileSchema } from "~/server/db/schema/file";
import CreateFileDialog, { CreateFileSchema } from "./createfile-dialog";
import ActionButton from "../../../../components/ui/action-button";
import { useEditorContext } from "../context/editor";
@ -20,11 +20,11 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
DropdownMenuSeparator,
} from "@/components/ui/dropdown-menu";
import { cn, getUrl } from "@/lib/utils";
import FileIcon from "@/components/ui/file-icon";
} from "~/components/ui/dropdown-menu";
import { cn, getUrl } from "~/lib/utils";
import FileIcon from "~/components/ui/file-icon";
import copy from "copy-to-clipboard";
import { useParams } from "next/navigation";
import { useParams } from "~/renderer/hooks";
const FileListing = () => {
const { onOpenFile, onFileChanged } = useEditorContext();
@ -175,14 +175,14 @@ const FileItem = ({ file, createFileDlg }: FileItemProps) => {
Copy Path
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => copy(getUrl(`project/${slug}/file`, file.path))}
onClick={() => copy(getUrl(`api/preview/${slug}`, file.path))}
>
Copy URL
</DropdownMenuItem>
<DropdownMenuItem
onClick={() =>
window.open(
getUrl(`project/${slug}/file`, file.path),
getUrl(`api/preview/${slug}`, file.path),
"_blank"
)
}

View File

@ -1,9 +1,9 @@
"use client";
import { getFileExt } from "@/lib/utils";
import { getFileExt } from "~/lib/utils";
import React from "react";
import CodeEditor from "../../../../components/ui/code-editor";
import trpc from "@/lib/trpc";
import trpc from "~/lib/trpc";
type Props = {
id: number;

View File

@ -1,7 +1,7 @@
import React from "react";
import FileListing from "./file-listing";
import { FaUserCircle } from "react-icons/fa";
import { Button } from "@/components/ui/button";
import { Button } from "~/components/ui/button";
const Sidebar = () => {
return (

View File

@ -0,0 +1,67 @@
/* eslint-disable react/display-name */
import Panel from "~/components/ui/panel";
import { useCallback, useEffect, useRef } from "react";
import { useProjectContext } from "../context/project";
import { Button } from "~/components/ui/button";
import { FaEllipsisV, FaRedo } from "react-icons/fa";
import { Input } from "~/components/ui/input";
import { previewStore } from "../stores/web-preview";
type WebPreviewProps = {
url?: string | null;
};
const WebPreview = ({ url }: WebPreviewProps) => {
const frameRef = useRef<HTMLIFrameElement>(null);
const project = useProjectContext();
const refresh = useCallback(() => {
if (frameRef.current) {
frameRef.current.src = `${url}?t=${Date.now()}`;
}
}, [url]);
useEffect(() => {
previewStore.setState({ refresh });
refresh();
}, [refresh]);
const PanelComponent = !project.isCompact ? Panel : "div";
return (
<PanelComponent className="h-full flex flex-col bg-slate-800">
<div className="h-10 flex items-center">
<Button
variant="ghost"
className="dark:hover:bg-slate-700"
onClick={refresh}
>
<FaRedo />
</Button>
<Input
className="flex-1 dark:bg-gray-900 dark:hover:bg-gray-950 h-8 rounded-full"
value={url || ""}
readOnly
/>
<Button
variant="ghost"
className="dark:hover:bg-slate-700"
onClick={() => {}}
>
<FaEllipsisV />
</Button>
</div>
{url != null ? (
<iframe
id="web-preview"
ref={frameRef}
className="border-none w-full flex-1 overflow-hidden bg-white"
sandbox="allow-scripts"
/>
) : null}
</PanelComponent>
);
};
export default WebPreview;

View File

@ -1,4 +1,4 @@
import type { FileSchema } from "@/server/db/schema/file";
import type { FileSchema } from "~/server/db/schema/file";
import { createContext, useContext } from "react";
type TEditorContext = {

View File

@ -1,6 +1,7 @@
import { createContext, useContext } from "react";
type TProjectContext = {
slug: string;
isCompact?: boolean;
};

View File

@ -0,0 +1,9 @@
import { createStore } from "zustand";
type PreviewStore = {
refresh: () => void;
};
export const previewStore = createStore<PreviewStore>(() => ({
refresh: () => {},
}));

3509
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -1,18 +0,0 @@
if (window.parent !== window) {
const _log = console.log;
const _error = console.error;
const _warn = console.warn;
console.log = function (...args) {
parent.window.postMessage({ type: "log", args: args }, "*");
_log(...args);
};
console.error = function (...args) {
parent.window.postMessage({ type: "error", args: args }, "*");
_error(...args);
};
console.warn = function (...args) {
parent.window.postMessage({ type: "warn", args: args }, "*");
_warn(...args);
};
}

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>

Before

Width:  |  Height:  |  Size: 629 B

15
renderer/+config.ts Normal file
View File

@ -0,0 +1,15 @@
import type { Config } from "vike/types";
export default {
clientRouting: true,
passToClient: ["routeParams"],
meta: {
title: {
env: { server: true, client: true },
},
description: {
env: { server: true },
},
},
hydrationCanBeAborted: true,
} satisfies Config;

View File

@ -0,0 +1,5 @@
import NProgress from "nprogress";
export const onPageTransitionEnd = async () => {
NProgress.done();
};

View File

@ -0,0 +1,5 @@
import NProgress from "nprogress";
export const onPageTransitionStart = async () => {
NProgress.start();
};

View File

@ -0,0 +1,38 @@
import ReactDOM from "react-dom/client";
import { getPageMetadata } from "./utils";
import type { OnRenderClientAsync } from "vike/types";
import Layout from "./layout";
let root: ReactDOM.Root;
export const onRenderClient: OnRenderClientAsync = async (
pageContext
): ReturnType<OnRenderClientAsync> => {
const { Page } = pageContext;
if (!Page)
throw new Error(
"My onRenderClient() hook expects pageContext.Page to be defined"
);
const container = document.getElementById("react-root");
if (!container) throw new Error("DOM element #react-root not found");
const page = (
<Layout pageContext={pageContext}>
<Page />
</Layout>
);
if (pageContext.isHydration) {
root = ReactDOM.hydrateRoot(container, page);
} else {
if (!root) {
root = ReactDOM.createRoot(container);
}
root.render(page);
}
const meta = getPageMetadata(pageContext);
document.title = meta.title;
};

View File

@ -0,0 +1,44 @@
import ReactDOMServer from "react-dom/server";
import { escapeInject, dangerouslySkipEscape } from "vike/server";
import type { OnRenderHtmlAsync } from "vike/types";
import { getPageMetadata } from "./utils";
import Layout from "./layout";
export const onRenderHtml: OnRenderHtmlAsync = async (
pageContext
): ReturnType<OnRenderHtmlAsync> => {
const { Page } = pageContext;
if (!Page)
throw new Error(
"My onRenderHtml() hook expects pageContext.Page to be defined"
);
const page = ReactDOMServer.renderToString(
<Layout pageContext={pageContext}>
<Page />
</Layout>
);
// See https://vike.dev/head
const meta = getPageMetadata(pageContext);
const documentHtml = escapeInject`<!DOCTYPE html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="${meta.description}" />
<title>${meta.title}</title>
</head>
<body>
<div id="react-root">${dangerouslySkipEscape(page)}</div>
</body>
</html>`;
return {
documentHtml,
pageContext: {},
};
};

34
renderer/client-only.tsx Normal file
View File

@ -0,0 +1,34 @@
import React, { useEffect, useState } from "react";
import type { ReactNode } from "react";
type ClientOnlyProps = {
children: ReactNode;
fallback?: ReactNode | null;
};
const ClientOnly = ({ children, fallback }: ClientOnlyProps) => {
const [isMounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (typeof window === "undefined") {
return fallback;
}
return isMounted ? children : fallback;
};
export const withClientOnly = <T extends unknown>(
Component: React.ComponentType<T>,
fallback?: ReactNode | null
): React.ComponentType<T> => {
return (props: any) => (
<ClientOnly fallback={fallback}>
<Component {...props} />
</ClientOnly>
);
};
export default ClientOnly;

26
renderer/context.tsx Normal file
View File

@ -0,0 +1,26 @@
// https://vike.dev/usePageContext
// eslint-disable-next-line react-refresh/only-export-components
export { usePageContext };
export { PageContextProvider };
import React, { useContext } from "react";
import type { PageContext } from "vike/types";
const Context = React.createContext<PageContext>(
undefined as unknown as PageContext
);
type Props = {
pageContext: PageContext;
children: React.ReactNode;
};
const PageContextProvider = ({ pageContext, children }: Props) => {
return <Context.Provider value={pageContext}>{children}</Context.Provider>;
};
/** https://vike.dev/usePageContext */
function usePageContext() {
const pageContext = useContext(Context);
return pageContext;
}

View File

@ -3,7 +3,7 @@
@tailwind utilities;
body {
@apply bg-slate-600 text-white;
@apply bg-slate-900 text-white;
}
.cm-theme {

16
renderer/hooks.ts Normal file
View File

@ -0,0 +1,16 @@
import { usePageContext } from "./context";
export const useData = <T = any>() => {
const { data } = usePageContext();
return data as T;
};
export const useParams = <T = any>() => {
const { routeParams } = usePageContext();
return (routeParams || {}) as T;
};
export const useSearchParams = () => {
const { urlParsed } = usePageContext();
return new URLSearchParams(urlParsed.searchOriginal || "");
};

23
renderer/layout.tsx Normal file
View File

@ -0,0 +1,23 @@
import React from "react";
import { PageContextProvider } from "./context";
import type { PageContext } from "vike/types";
import Providers from "./providers";
import "./globals.css";
import "nprogress/nprogress.css";
type LayoutProps = {
children: React.ReactNode;
pageContext: PageContext;
};
const Layout = ({ children, pageContext }: LayoutProps) => {
return (
<React.StrictMode>
<PageContextProvider pageContext={pageContext}>
<Providers>{children}</Providers>
</PageContextProvider>
</React.StrictMode>
);
};
export default Layout;

19
renderer/link.tsx Normal file
View File

@ -0,0 +1,19 @@
import { ComponentProps } from "react";
import { usePageContext } from "./context";
const Link = (props: ComponentProps<"a">) => {
const pageContext = usePageContext();
const { urlPathname } = pageContext;
const { href } = props;
const isActive =
href === "/" ? urlPathname === href : urlPathname.startsWith(href!);
const className = [props.className, isActive && "is-active"]
.filter(Boolean)
.join(" ");
return <a {...props} className={className} />;
};
export default Link;

View File

@ -1,8 +1,6 @@
"use client";
import React, { useState } from "react";
import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
import trpc, { getBaseUrl } from "@/lib/trpc";
import trpc, { getBaseUrl } from "~/lib/trpc";
import { httpBatchLink } from "@trpc/react-query";
type Props = {
@ -19,9 +17,6 @@ const Providers = ({ children }: Props) => {
headers() {
return {};
},
fetch(input, init = {}) {
return fetch(input, { ...init, cache: "no-store" });
},
}),
],
})

19
renderer/types.ts Normal file
View File

@ -0,0 +1,19 @@
//
declare global {
namespace Vike {
interface PageContext {
Page: () => React.ReactElement;
data?: {
title?: string;
description?: string;
};
config: {
title?: string;
description?: string;
};
abortReason?: string;
}
}
}
export {};

11
renderer/utils.ts Normal file
View File

@ -0,0 +1,11 @@
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";
const description =
pageContext.data?.description || pageContext.config.description || "";
return { title, description };
}

10
server/api/index.ts Normal file
View File

@ -0,0 +1,10 @@
import { Router } from "express";
import preview from "./preview";
import trpc from "./trpc/handler";
const api = Router();
api.use("/trpc", trpc);
api.use("/preview", preview);
export default api;

View File

@ -0,0 +1,51 @@
import { getFileExt } from "~/lib/utils";
import db from "~/server/db";
import { file } from "~/server/db/schema/file";
import { and, eq, isNull } from "drizzle-orm";
import { serveHtml } from "./serve-html";
import { Mime } from "mime/lite";
import standardTypes from "mime/types/standard.js";
import otherTypes from "mime/types/other.js";
import { serveJs } from "./serve-js";
import { type Request, type Response, Router } from "express";
const mime = new Mime(standardTypes, otherTypes);
mime.define({ "text/javascript": ["jsx", "tsx"] }, true);
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)),
});
if (!fileData) {
return res.status(404).send("File not found!");
}
const ext = getFileExt(fileData.filename);
let content = fileData.content || "";
if (["html", "htm"].includes(ext)) {
content = await serveHtml(fileData);
}
if (["js", "ts", "jsx", "tsx"].includes(ext)) {
content = await serveJs(fileData);
}
res.setHeader(
"Content-Type",
mime.getType(fileData.filename) || "application/octet-stream"
);
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Methods", "GET");
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
res.send(content);
};
const router = Router();
router.get("/:slug/*", get);
export default router;

View File

@ -1,5 +1,5 @@
import db from "@/server/db";
import { FileSchema, file } from "@/server/db/schema/file";
import db from "~/server/db";
import { FileSchema, file } from "~/server/db/schema/file";
import { and, eq, isNull } from "drizzle-orm";
export const serveHtml = async (fileData: FileSchema) => {
@ -22,7 +22,7 @@ export const serveHtml = async (fileData: FileSchema) => {
const bodyOpeningTagIdx = content.indexOf("<body");
const firstScriptTagIdx = content.indexOf("<script", bodyOpeningTagIdx);
const injectScripts = ['<script src="/js/debug-console.js"></script>'];
const injectScripts = ['<script src="/js/hook-console.js"></script>'];
const importMaps = [
{ name: "react", url: "https://esm.sh/react@18.2.0" },
@ -35,7 +35,7 @@ export const serveHtml = async (fileData: FileSchema) => {
return a;
}, {});
const json = JSON.stringify({ imports });
injectScripts.push(`<script type="importmap">${json}</script>`);
injectScripts.unshift(`<script type="importmap">${json}</script>`);
}
if (firstScriptTagIdx >= 0 && injectScripts.length > 0) {

View File

@ -0,0 +1,11 @@
import { FileSchema } from "~/server/db/schema/file";
import { transformJs } from "~/server/lib/transform-js";
export const serveJs = async (file: FileSchema) => {
let content = file.content || "";
// transform js
content = await transformJs(content);
return content;
};

View File

@ -0,0 +1,10 @@
import { CreateExpressContextOptions } from "@trpc/server/adapters/express";
export const createContext = async ({
req,
res,
}: CreateExpressContextOptions) => {
return {};
};
export type Context = Awaited<ReturnType<typeof createContext>>;

View File

@ -0,0 +1,10 @@
import { createExpressMiddleware } from "@trpc/server/adapters/express";
import { appRouter } from "~/server/routers/_app";
import { createContext } from "./context";
const trpc = createExpressMiddleware({
router: appRouter,
createContext,
});
export default trpc;

7
server/api/trpc/trpc.ts Normal file
View File

@ -0,0 +1,7 @@
import { createContext } from "./context";
import { appRouter } from "../../routers/_app";
import { createCallerFactory } from ".";
const trpcServer = createCallerFactory(appRouter)(createContext);
export default trpcServer;

View File

@ -19,7 +19,7 @@ const main = async () => {
userId: adminUser.id,
path: "index.html",
filename: "index.html",
content: "<p>Hello world!</p>",
content: '<p class="text-lg text-red-500">Hello world!</p>',
},
{
userId: adminUser.id,
@ -42,16 +42,15 @@ const main = async () => {
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Code-Share</title>
<title>Document</title>
<link rel="stylesheet" href="styles.css">
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
{CONTENT}
<script type="text/babel" src="script.js" data-type="module"></script>
<script src="script.js" type="module" defer></script>
</body>
</html>`,
},

54
server/index.ts Normal file
View File

@ -0,0 +1,54 @@
import express from "express";
import { renderPage } from "vike/server";
import { IS_DEV } from "./lib/consts";
import api from "./api";
async function createServer() {
const app = express();
const root = process.cwd();
if (IS_DEV) {
// start vite development server
const { createServer: createViteServer } = await import("vite");
const vite = await createViteServer({
root,
server: { middlewareMode: true },
appType: "custom",
});
app.use(vite.middlewares);
} else {
// serve client assets
app.use(express.static(root + "/dist/client"));
}
app.use("/api", api);
app.use("*", async (req, res, next) => {
const url = req.originalUrl;
const pageContext = {};
const ctx = await renderPage({ urlOriginal: url, ...pageContext });
const { httpResponse } = ctx;
if (!httpResponse) {
return next();
}
const { body, statusCode, headers, earlyHints } = httpResponse;
if (res.writeEarlyHints) {
res.writeEarlyHints({ link: earlyHints.map((e) => e.earlyHintLink) });
}
headers.forEach(([name, value]) => res.setHeader(name, value));
res.status(statusCode).send(body);
});
const host = process.env.HOST || "127.0.0.1";
const port = Number(process.env.PORT) || 3000;
app.listen(port, host, () => {
console.log(`Server listening on http://${host}:${port}`);
});
}
createServer();

2
server/lib/consts.ts Normal file
View File

@ -0,0 +1,2 @@
export const IS_DEV = process.env.NODE_ENV !== "production";
export const BASE_URL = process.env.BASE_URL || "";

View File

@ -0,0 +1,19 @@
import * as swc from "@swc/core";
export const transformJs = async (code: string) => {
try {
const result = await swc.transform(code, {
jsc: {
parser: {
jsx: true,
syntax: "ecmascript",
},
target: "es5",
},
});
return result.code;
} catch (err) {
return code;
}
};

View File

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

View File

@ -1,6 +1,6 @@
import { and, asc, desc, eq, inArray, isNull, sql } from "drizzle-orm";
import db from "../db";
import { procedure, router } from "../trpc";
import { procedure, router } from "../api/trpc";
import { file, insertFileSchema, selectFileSchema } from "../db/schema/file";
import { z } from "zod";
import { TRPCError } from "@trpc/server";

View File

@ -1,13 +0,0 @@
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
import { appRouter } from "@/server/routers/_app";
import { createContext } from "@/server/context";
const handler = (req: Request) =>
fetchRequestHandler({
endpoint: "/api/trpc",
req,
router: appRouter,
createContext,
});
export { handler as GET, handler as POST };

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

View File

@ -1,28 +0,0 @@
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import Providers from "./providers";
import { IS_DEV } from "@/lib/consts";
import "./globals.css";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Code Share",
description: "Code sharing app",
};
type Props = {
children: React.ReactNode;
};
const RootLayout = ({ children }: Props) => {
return (
<html lang="en" className="dark" suppressHydrationWarning={IS_DEV}>
<body className={inter.className}>
<Providers>{children}</Providers>
</body>
</html>
);
};
export default RootLayout;

View File

@ -1,63 +0,0 @@
/* eslint-disable react/display-name */
import React, { forwardRef, useEffect, useState } from "react";
const ConsoleLogger = forwardRef((_, ref: any) => {
const [iframeLogs, setIframeLogs] = useState<any[]>([]);
useEffect(() => {
if (ref) {
ref.current = {
clear: () => setIframeLogs([]),
};
}
const onMessage = (event: MessageEvent<any>) => {
const { data } = event;
if (!data) {
return;
}
if (!["log", "warn", "error"].includes(data.type) || !data.args?.length) {
return;
}
if (
typeof data.args[0] === "string" &&
data.args[0]?.includes("Babel transformer")
) {
return;
}
setIframeLogs((i) => [data, ...i]);
};
window.addEventListener("message", onMessage);
return () => {
window.removeEventListener("message", onMessage);
};
}, [ref]);
return (
<div className="pt-2 h-full border-t border-slate-700">
<div className="flex flex-col-reverse overflow-y-auto items-stretch h-full font-mono text-slate-400">
{iframeLogs.map((item, idx) => (
<p
key={idx}
className="text-xs border-b border-slate-900 first:border-b-0 px-2 py-1"
>
{item.args
?.map((arg: any) => {
if (typeof arg === "object") {
return JSON.stringify(arg, null, 2);
}
return arg;
})
.join(" ")}
</p>
))}
</div>
</div>
);
});
export default ConsoleLogger;

View File

@ -1,48 +0,0 @@
"use client";
/* eslint-disable react/display-name */
import Panel from "@/components/ui/panel";
import { useParams } from "next/navigation";
import React, { Fragment, useCallback, useEffect, useRef } from "react";
import { createStore } from "zustand";
import { useProjectContext } from "../context/project";
type PreviewStore = {
refresh: () => void;
};
export const previewStore = createStore<PreviewStore>(() => ({
refresh: () => {},
}));
const WebPreview = () => {
const { slug } = useParams();
const frameRef = useRef<HTMLIFrameElement>(null);
const project = useProjectContext();
const refresh = useCallback(() => {
if (frameRef.current) {
frameRef.current.src = `/project/${slug}/file/index.html?t=${Date.now()}`;
}
}, [slug]);
useEffect(() => {
previewStore.setState({ refresh });
refresh();
}, [refresh]);
const PanelComponent = !project.isCompact ? Panel : Fragment;
return (
<PanelComponent>
<iframe
id="web-preview"
ref={frameRef}
className="border-none w-full h-full bg-white"
sandbox="allow-scripts"
/>
</PanelComponent>
);
};
export default WebPreview;

View File

@ -1,50 +0,0 @@
import { getFileExt } from "@/lib/utils";
import db from "@/server/db";
import { file } from "@/server/db/schema/file";
import { and, eq, isNull } from "drizzle-orm";
import { NextRequest } from "next/server";
import { serveHtml } from "./serve-html";
import { Mime } from "mime/lite";
import standardTypes from "mime/types/standard.js";
import otherTypes from "mime/types/other.js";
import { serveJs } from "./serve-js";
const mime = new Mime(standardTypes, otherTypes);
mime.define({ "text/javascript": ["jsx", "tsx"] }, true);
// Opt out of caching for all data requests in the route segment
export const dynamic = "force-dynamic";
export const GET = async (req: NextRequest, { params }: any) => {
const path = params.path.join("/");
const { slug } = params;
const fileData = await db.query.file.findFirst({
where: and(eq(file.path, path), isNull(file.deletedAt)),
});
if (!fileData) {
return new Response("File not found!", { status: 404 });
}
const ext = getFileExt(fileData.filename);
let content = fileData.content || "";
if (["html", "htm"].includes(ext)) {
content = await serveHtml(fileData);
}
if (["js", "ts", "jsx", "tsx"].includes(ext)) {
content = await serveJs(fileData, slug);
}
return new Response(content, {
headers: {
"Content-Type":
mime.getType(fileData.filename) || "application/octet-stream",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
},
});
};

View File

@ -1,41 +0,0 @@
import { BASE_URL } from "@/lib/consts";
import { FileSchema } from "@/server/db/schema/file";
export const serveJs = async (file: FileSchema, slug: string) => {
let content = file.content || "";
const importRegex = /(?:import.+from.+)(?:"|')(.+)(?:"|')/g;
content = content.replace(
importRegex,
(match: string, importPath: string) => {
// local file
if (importPath.startsWith("./")) {
return match.replace(
importPath,
BASE_URL + `/project/${slug}/file` + importPath.substring(1)
);
}
// resolve to esm
return match.replace(importPath, "https://esm.sh/" + importPath);
}
);
try {
const res = await fetch("http://localhost:3001/file/" + file.path);
if (!res.ok) {
throw new Error(res.statusText);
}
const data = await res.text();
if (typeof data !== "string") {
throw new Error("Invalid response!");
}
return data;
} catch (err) {
console.error((err as any).message);
}
return content;
};

View File

@ -1,2 +0,0 @@
export const IS_DEV = process.env.NODE_ENV === "development";
export const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL || "";

View File

@ -1,10 +0,0 @@
import { headers as getHeaders, cookies as getCookies } from "next/headers";
export const createContext = async () => {
// const headers = getHeaders();
// const cookies = getCookies();
return {};
};
export type Context = Awaited<ReturnType<typeof createContext>>;

View File

@ -1,26 +0,0 @@
CREATE TABLE `files` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`parent_id` integer,
`user_id` integer NOT NULL,
`path` text NOT NULL,
`filename` text NOT NULL,
`is_directory` integer DEFAULT false NOT NULL,
`is_file` integer DEFAULT false NOT NULL,
`content` text,
`created_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL,
`deleted_at` text,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action,
FOREIGN KEY (`parent_id`) REFERENCES `files`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE `users` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`email` text NOT NULL,
`password` text NOT NULL,
`created_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL,
`deleted_at` text
);
--> statement-breakpoint
CREATE UNIQUE INDEX `files_path_unique` ON `files` (`path`);--> statement-breakpoint
CREATE UNIQUE INDEX `files_filename_unique` ON `files` (`filename`);--> statement-breakpoint
CREATE UNIQUE INDEX `users_email_unique` ON `users` (`email`);

View File

@ -1,191 +0,0 @@
{
"version": "5",
"dialect": "sqlite",
"id": "30eeec75-2990-42f2-a773-b55bfa0a4c0a",
"prevId": "00000000-0000-0000-0000-000000000000",
"tables": {
"files": {
"name": "files",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"parent_id": {
"name": "parent_id",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"path": {
"name": "path",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"filename": {
"name": "filename",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"is_directory": {
"name": "is_directory",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": false
},
"is_file": {
"name": "is_file",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": false
},
"content": {
"name": "content",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "CURRENT_TIMESTAMP"
},
"deleted_at": {
"name": "deleted_at",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {
"files_path_unique": {
"name": "files_path_unique",
"columns": [
"path"
],
"isUnique": true
},
"files_filename_unique": {
"name": "files_filename_unique",
"columns": [
"filename"
],
"isUnique": true
}
},
"foreignKeys": {
"files_user_id_users_id_fk": {
"name": "files_user_id_users_id_fk",
"tableFrom": "files",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"parent_id_fk": {
"name": "parent_id_fk",
"tableFrom": "files",
"tableTo": "files",
"columnsFrom": [
"parent_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"users": {
"name": "users",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"password": {
"name": "password",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "CURRENT_TIMESTAMP"
},
"deleted_at": {
"name": "deleted_at",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {
"users_email_unique": {
"name": "users_email_unique",
"columns": [
"email"
],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
}
},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
}
}

View File

@ -1,13 +0,0 @@
{
"version": "5",
"dialect": "sqlite",
"entries": [
{
"idx": 0,
"version": "5",
"when": 1708329026876,
"tag": "0000_loud_hex",
"breakpoints": true
}
]
}

View File

@ -1,5 +0,0 @@
import { createContext } from "./context";
import { appRouter } from "./routers/_app";
import { createCallerFactory } from "./trpc";
export const trpcServer = createCallerFactory(appRouter)(createContext);

View File

@ -1,46 +0,0 @@
import express from "express";
import db from "../db";
import { and, eq, isNull } from "drizzle-orm";
import { file } from "../db/schema/file";
import * as swc from "@swc/core";
const app = express();
const getFileByPath = async (path: string) => {
const fileData = await db.query.file.findFirst({
where: and(eq(file.path, path), isNull(file.deletedAt)),
});
return fileData;
};
app.get("/file/*", async (req, res) => {
const pathname = Object.values(req.params).join("/");
const fileData = await getFileByPath(pathname);
if (!fileData) {
return res.status(404).send("File not found!");
}
try {
let code = fileData.content || "";
const result = await swc.transform(code, {
jsc: {
parser: {
jsx: true,
syntax: "ecmascript",
},
target: "es5",
},
});
res.contentType("text/javascript").send(result.code);
} catch (err) {
console.log(err);
return res.status(400).send("Cannot transform file!");
}
});
app.listen(3001, () => {
console.log("App listening on http://localhost:3001");
});

Some files were not shown because too many files have changed in this diff Show More