├── .eslintrc.json
├── .gitignore
├── .prettierrc
├── .vscode
└── settings.json
├── README.md
├── next.config.js
├── package-lock.json
├── package.json
├── postcss.config.js
├── public
├── favicon.ico
├── favicons
│ ├── android-chrome-192x192.png
│ ├── android-chrome-512x512.png
│ ├── apple-touch-icon.png
│ ├── favicon-16x16.png
│ ├── favicon-32x32.png
│ ├── favicon.ico
│ └── site.webmanifest
├── frame.html
├── logo.svg
├── vendor
│ └── sass.worker.js
└── vercel.svg
├── src
├── components
│ ├── Editor
│ │ ├── EditorDesktop.js
│ │ ├── format.js
│ │ ├── index.js
│ │ └── setupTsxMode.js
│ ├── Preview
│ │ └── index.jsx
│ ├── codemirror
│ │ └── index.js
│ ├── header
│ │ ├── LayoutSwitch.js
│ │ ├── SaveBtn.js
│ │ └── index.js
│ ├── monaco
│ │ ├── index.js
│ │ └── markdown.js
│ ├── select
│ │ └── index.js
│ └── setting
│ │ └── Modal.js
├── hooks
│ ├── useDebouncedState.js
│ └── useIsomorphicLayoutEffect.js
├── pages
│ ├── _app.js
│ ├── api
│ │ ├── hello.js
│ │ └── thumbnail.js
│ ├── index.js
│ ├── pen
│ │ └── [...id].js
│ ├── preview
│ │ └── [...id].js
│ └── qrcode-generator.js
├── styles
│ ├── Home.module.css
│ ├── codemirror
│ │ ├── base.css
│ │ └── dark.css
│ ├── globals.css
│ └── tailwind.css
├── utils
│ ├── compile.js
│ ├── database.js
│ ├── theme.js
│ └── workers.js
└── workers
│ ├── compile.worker.js
│ └── prettier.worker.js
├── tailwind.config.js
└── yarn.lock
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 | .pnpm-debug.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "tabWidth": 2,
3 | "useTabs": false
4 | }
5 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "commentTranslate.hover.enabled": false
3 | }
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 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).
2 |
3 | ## Getting Started
4 |
5 | First, run the development server:
6 |
7 | ```bash
8 | npm run dev
9 | # or
10 | yarn dev
11 | ```
12 |
13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
14 |
15 | You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file.
16 |
17 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`.
18 |
19 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
20 |
21 | ## Learn More
22 |
23 | To learn more about Next.js, take a look at the following resources:
24 |
25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
27 |
28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
29 |
30 | ## Deploy on Vercel
31 |
32 | 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.
33 |
34 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
35 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | const MonacoWebpackPlugin = require("monaco-editor-webpack-plugin");
2 | const path = require("path");
3 |
4 | module.exports = {
5 | eslint: {
6 | ignoreDuringBuilds: true,
7 | },
8 | webpack: (config, { isServer, webpack, dev }) => {
9 | config.module.rules
10 | .filter((rule) => rule.oneOf)
11 | .forEach((rule) => {
12 | rule.oneOf.forEach((r) => {
13 | if (
14 | r.issuer &&
15 | r.issuer.and &&
16 | r.issuer.and.length === 1 &&
17 | r.issuer.and[0].source &&
18 | r.issuer.and[0].source.replace(/\\/g, "") ===
19 | path.resolve(process.cwd(), "src/pages/_app")
20 | ) {
21 | r.issuer.or = [
22 | ...r.issuer.and,
23 | /[\\/]node_modules[\\/]monaco-editor[\\/]/,
24 | ];
25 | delete r.issuer.and;
26 | }
27 | });
28 | });
29 |
30 | config.output.globalObject = "self";
31 | if (!isServer) {
32 | config.plugins.push(
33 | new MonacoWebpackPlugin({
34 | languages: [
35 | "json",
36 | "markdown",
37 | "css",
38 | "typescript",
39 | "javascript",
40 | "html",
41 | "scss",
42 | "less",
43 | ],
44 | filename: "static/[name].worker.js",
45 | })
46 | );
47 | }
48 | return config;
49 | },
50 | };
51 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "my-pen",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "chrome-aws-lambda": "^10.1.0",
13 | "next": "12.1.5",
14 | "node-fetch": "^3.2.4",
15 | "playwright-core": "^1.21.1",
16 | "react": "17.x",
17 | "react-dom": "17.x"
18 | },
19 | "devDependencies": {
20 | "@babel/standalone": "^7.17.9",
21 | "@headlessui/react": "^1.6.0",
22 | "autoprefixer": "^10.4.4",
23 | "clsx": "^1.1.1",
24 | "codemirror": "^5.57.0",
25 | "debounce": "^1.2.1",
26 | "dlv": "^1.1.3",
27 | "eslint": "8.13.0",
28 | "eslint-config-next": "12.1.5",
29 | "is-mobile": "^3.1.1",
30 | "less": "^4.1.2",
31 | "monaco-editor": "^0.33.0",
32 | "monaco-editor-webpack-plugin": "^7.0.1",
33 | "next-transpile-modules": "^9.0.0",
34 | "p-queue": "^7.2.0",
35 | "postcss": "^8.4.12",
36 | "prettier": "^2.6.2",
37 | "qrcode": "^1.5.0",
38 | "react-split-pane": "^0.1.92",
39 | "react-use": "^17.3.2",
40 | "sass.js": "^0.11.1",
41 | "tailwindcss": "^3.0.24",
42 | "worker-loader": "^3.0.8"
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maqi1520/next-code-pen/a779f00a148a64e2b54bd80c78ea5ce148163de0/public/favicon.ico
--------------------------------------------------------------------------------
/public/favicons/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maqi1520/next-code-pen/a779f00a148a64e2b54bd80c78ea5ce148163de0/public/favicons/android-chrome-192x192.png
--------------------------------------------------------------------------------
/public/favicons/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maqi1520/next-code-pen/a779f00a148a64e2b54bd80c78ea5ce148163de0/public/favicons/android-chrome-512x512.png
--------------------------------------------------------------------------------
/public/favicons/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maqi1520/next-code-pen/a779f00a148a64e2b54bd80c78ea5ce148163de0/public/favicons/apple-touch-icon.png
--------------------------------------------------------------------------------
/public/favicons/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maqi1520/next-code-pen/a779f00a148a64e2b54bd80c78ea5ce148163de0/public/favicons/favicon-16x16.png
--------------------------------------------------------------------------------
/public/favicons/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maqi1520/next-code-pen/a779f00a148a64e2b54bd80c78ea5ce148163de0/public/favicons/favicon-32x32.png
--------------------------------------------------------------------------------
/public/favicons/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maqi1520/next-code-pen/a779f00a148a64e2b54bd80c78ea5ce148163de0/public/favicons/favicon.ico
--------------------------------------------------------------------------------
/public/favicons/site.webmanifest:
--------------------------------------------------------------------------------
1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
--------------------------------------------------------------------------------
/public/frame.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
85 |
86 |
87 |
88 |
89 |
113 |
114 |
--------------------------------------------------------------------------------
/public/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/Editor/EditorDesktop.js:
--------------------------------------------------------------------------------
1 | import React, { useRef, useEffect } from "react";
2 | import * as monaco from "monaco-editor/esm/vs/editor/editor.api";
3 | import { CommandsRegistry } from "monaco-editor/esm/vs/platform/commands/common/commands";
4 | import { registerDocumentFormattingEditProviders } from "./format";
5 |
6 | function setupKeybindings(editor) {
7 | let formatCommandId = "editor.action.formatDocument";
8 | const { handler, when } = CommandsRegistry.getCommand(formatCommandId);
9 | editor._standaloneKeybindingService.addDynamicKeybinding(
10 | formatCommandId,
11 | monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS,
12 | handler,
13 | when
14 | );
15 | }
16 | registerDocumentFormattingEditProviders();
17 |
18 | const languageToMode = {
19 | html: "html",
20 | css: "css",
21 | less: "less",
22 | scss: "scss",
23 | javascript: "javascript",
24 | babel: "javascript",
25 | typescript: "typescript",
26 | };
27 |
28 | const Editor = ({ language, defaultValue, value, onChange }) => {
29 | const divEl = useRef(null);
30 | const editor = useRef(null);
31 |
32 | useEffect(() => {
33 | if (divEl.current) {
34 | editor.current = monaco.editor.create(divEl.current, {
35 | minimap: { enabled: false },
36 | theme: "vs-dark",
37 | });
38 | editor.current.onDidChangeModelContent(() => {
39 | onChange(editor.current.getValue());
40 | });
41 | }
42 |
43 | setupKeybindings(editor.current);
44 |
45 | return () => {
46 | editor.current.dispose();
47 | };
48 | }, []);
49 |
50 | useEffect(() => {
51 | const model = editor.current.getModel();
52 | monaco.editor.setModelLanguage(model, languageToMode[language]);
53 | }, [language]);
54 |
55 | useEffect(() => {
56 | if (defaultValue) {
57 | editor.current.setValue(defaultValue);
58 | }
59 | }, []);
60 |
61 | useEffect(() => {
62 | if (value) {
63 | editor.current.setValue(value);
64 | }
65 | }, [value]);
66 |
67 | useEffect(() => {
68 | const observer = new ResizeObserver(() => {
69 | window.setTimeout(() => editor.current.layout(), 0);
70 | });
71 | observer.observe(divEl.current);
72 | return () => {
73 | observer.disconnect();
74 | };
75 | }, []);
76 |
77 | return ;
78 | };
79 |
80 | export default Editor;
81 |
--------------------------------------------------------------------------------
/src/components/Editor/format.js:
--------------------------------------------------------------------------------
1 | import * as monaco from "monaco-editor/esm/vs/editor/editor.api";
2 | import PrettierWorker from "worker-loader!../../workers/prettier.worker.js";
3 | import { createWorkerQueue } from "../../utils/workers";
4 |
5 | export function registerDocumentFormattingEditProviders() {
6 | const disposables = [];
7 | let prettierWorker;
8 |
9 | const formattingEditProvider = {
10 | async provideDocumentFormattingEdits(model, _options, _token) {
11 | if (!prettierWorker) {
12 | prettierWorker = createWorkerQueue(PrettierWorker);
13 | }
14 | const { canceled, error, pretty } = await prettierWorker.emit({
15 | text: model.getValue(),
16 | language: model.getLanguageId(),
17 | });
18 | if (canceled || error) return [];
19 | return [
20 | {
21 | range: model.getFullModelRange(),
22 | text: pretty,
23 | },
24 | ];
25 | },
26 | };
27 |
28 | // // override the built-in HTML formatter
29 | // const _registerDocumentFormattingEditProvider =
30 | // monaco.languages.registerDocumentFormattingEditProvider;
31 | // monaco.languages.registerDocumentFormattingEditProvider = (id, provider) => {
32 | // if ((['css','less','scss','javascript','typescript','html'].includes(id))) {
33 | // return _registerDocumentFormattingEditProvider(
34 | // ,
35 | // formattingEditProvider
36 | // );
37 |
38 | // }else{
39 | // return _registerDocumentFormattingEditProvider(id, provider);
40 | // }
41 |
42 | // };
43 | ["css", "less", "scss", "javascript", "typescript", "html"].forEach((id) => {
44 | disposables.push(
45 | monaco.languages.registerDocumentFormattingEditProvider(
46 | id,
47 | formattingEditProvider
48 | )
49 | );
50 | });
51 |
52 | return {
53 | dispose() {
54 | disposables.forEach((disposable) => disposable.dispose());
55 | if (prettierWorker) {
56 | prettierWorker.terminate();
57 | }
58 | },
59 | };
60 | }
61 |
--------------------------------------------------------------------------------
/src/components/Editor/index.js:
--------------------------------------------------------------------------------
1 | import dynamic from "next/dynamic";
2 | import isMobile from "is-mobile";
3 | const EditorMobile = dynamic(() => import("../codemirror"), {
4 | ssr: false,
5 | });
6 |
7 | const EditorDesktop = dynamic(() => import("./EditorDesktop"), {
8 | ssr: false,
9 | });
10 |
11 | export const Editor = isMobile() ? EditorMobile : EditorDesktop;
12 |
--------------------------------------------------------------------------------
/src/components/Editor/setupTsxMode.js:
--------------------------------------------------------------------------------
1 | import * as monaco from "monaco-editor/esm/vs/editor/editor.api";
2 |
3 | export function setupTsxMode(content) {
4 | const modelUri = monaco.Uri.file("index.tsx");
5 | const codeModel = monaco.editor.createModel(
6 | content || "",
7 | "typescript",
8 | modelUri
9 | );
10 |
11 | // 设置typescript 使用jsx 的编译方式
12 | monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
13 | jsx: "react",
14 | });
15 |
16 | monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions({
17 | noSemanticValidation: false,
18 | noSyntaxValidation: false,
19 | });
20 | return codeModel;
21 | }
22 |
23 | export function setupHtmlMode(content) {
24 | const modelUri = monaco.Uri.file("index.html");
25 | const codeModel = monaco.editor.createModel(content || "", "html", modelUri);
26 | return codeModel;
27 | }
28 |
29 | export function setupJavascriptMode(content) {
30 | const modelUri = monaco.Uri.file("index.js");
31 | const codeModel = monaco.editor.createModel(
32 | content || "",
33 | "javascript",
34 | modelUri
35 | );
36 | return codeModel;
37 | }
38 |
39 | export function setupTypescriptMode(content) {
40 | const modelUri = monaco.Uri.file("index.ts");
41 | const codeModel = monaco.editor.createModel(
42 | content || "",
43 | "typescript",
44 | modelUri
45 | );
46 | return codeModel;
47 | }
48 |
49 | export function setupCssMode(content) {
50 | const modelUri = monaco.Uri.file("index.css");
51 | const codeModel = monaco.editor.createModel(content || "", "css", modelUri);
52 | return codeModel;
53 | }
54 |
--------------------------------------------------------------------------------
/src/components/Preview/index.jsx:
--------------------------------------------------------------------------------
1 | import { forwardRef, useMemo, useState } from "react";
2 | import clsx from "clsx";
3 | import { useIsomorphicLayoutEffect } from "../../hooks/useIsomorphicLayoutEffect";
4 |
5 | export default forwardRef(function Preview(
6 | { onLoad, iframeClassName = "", scripts, styles },
7 | ref
8 | ) {
9 | const [resizing, setResizing] = useState();
10 | useIsomorphicLayoutEffect(() => {
11 | function onMouseMove(e) {
12 | e.preventDefault();
13 | setResizing(true);
14 | }
15 | function onMouseUp(e) {
16 | e.preventDefault();
17 | setResizing();
18 | }
19 | window.addEventListener("mousemove", onMouseMove);
20 | window.addEventListener("mouseup", onMouseUp);
21 | window.addEventListener("touchmove", onMouseMove);
22 | window.addEventListener("touchend", onMouseUp);
23 | return () => {
24 | window.removeEventListener("mousemove", onMouseMove);
25 | window.removeEventListener("mouseup", onMouseUp);
26 | window.removeEventListener("touchmove", onMouseMove);
27 | window.removeEventListener("touchend", onMouseUp);
28 | };
29 | }, []);
30 |
31 | const scriptsdom = scripts
32 | .map((s) => {
33 | if (s.trim() !== "") {
34 | return ``;
35 | } else {
36 | return false;
37 | }
38 | })
39 | .filter(Boolean)
40 | .join("");
41 | const stylessdom = styles
42 | .map((s) => {
43 | if (s.trim() !== "") {
44 | return ``;
45 | } else {
46 | return false;
47 | }
48 | })
49 | .filter(Boolean)
50 | .join("");
51 |
52 | const srcDoc = `
53 |
54 |
55 |
56 | ${stylessdom}
57 |
113 |
114 | ${scriptsdom}
115 |
139 |
140 | `;
141 |
142 | return (
143 |
157 | );
158 | });
159 |
--------------------------------------------------------------------------------
/src/components/codemirror/index.js:
--------------------------------------------------------------------------------
1 | import { useRef, useEffect, useState } from "react";
2 | import { useIsomorphicLayoutEffect } from "../../hooks/useIsomorphicLayoutEffect";
3 | import CodeMirror from "codemirror";
4 | import { onDidChangeTheme, getTheme } from "../../utils/theme";
5 | require("codemirror/mode/htmlmixed/htmlmixed");
6 | require("codemirror/mode/javascript/javascript");
7 | require("codemirror/mode/css/css");
8 |
9 | const docToMode = {
10 | html: "htmlmixed",
11 | css: "css",
12 | javascript: "javascript",
13 | };
14 |
15 | const modeToDoc = {
16 | htmlmixed: "html",
17 | css: "css",
18 | javascript: "javascript",
19 | };
20 |
21 | export default function EditorMobile({
22 | language,
23 | value,
24 | onChange,
25 | editorRef: inRef,
26 | }) {
27 | const ref = useRef();
28 | const cmRef = useRef();
29 | const [i, setI] = useState(0);
30 | const skipNextOnChange = useRef(true);
31 |
32 | useEffect(() => {
33 | cmRef.current = CodeMirror(ref.current, {
34 | value,
35 | mode: docToMode[language],
36 | lineNumbers: true,
37 | viewportMargin: Infinity,
38 | tabSize: 2,
39 | theme: "dark",
40 | addModeClass: true,
41 | });
42 | typeof inRef === "function" &&
43 | inRef({
44 | getValue(doc) {
45 | return content.current[doc];
46 | },
47 | });
48 | }, []);
49 |
50 | useEffect(() => {
51 | function handleChange() {
52 | onChange(cmRef.current.getValue());
53 | }
54 | cmRef.current.on("change", handleChange);
55 | return () => {
56 | cmRef.current.off("change", handleChange);
57 | };
58 | }, [onChange]);
59 |
60 | useEffect(() => {
61 | cmRef.current.setOption("mode", docToMode[language]);
62 | }, [language]);
63 |
64 | useIsomorphicLayoutEffect(() => {
65 | if (!cmRef.current) return;
66 | cmRef.current.refresh();
67 | //cmRef.current.focus();
68 | }, [i]);
69 |
70 | useEffect(() => {
71 | function handleThemeChange(theme) {
72 | cmRef.current.setOption("theme", theme);
73 | }
74 | const dispose = onDidChangeTheme(handleThemeChange);
75 | return () => dispose();
76 | }, []);
77 |
78 | return (
79 |
82 | );
83 | }
84 |
--------------------------------------------------------------------------------
/src/components/header/LayoutSwitch.js:
--------------------------------------------------------------------------------
1 | import React, { Fragment } from "react";
2 | import clsx from "clsx";
3 | import { Popover, Transition } from "@headlessui/react";
4 |
5 | export default function LayoutSwitch({ value, onChange }) {
6 | return (
7 |
8 | {({ open }) => (
9 | <>
10 |
18 |
48 |
49 |
58 |
59 |
60 |
61 | - onChange("top")}
63 | className={clsx("p-2 cursor-pointer", {
64 | "text-sky-500": value === "top",
65 | })}
66 | >
67 |
97 |
98 | - onChange("left")}
100 | className={clsx("p-2 cursor-pointer", {
101 | "text-sky-500": value === "left",
102 | })}
103 | >
104 |
134 |
135 | - onChange("right")}
137 | className={clsx("p-2 cursor-pointer", {
138 | "text-sky-500": value === "right",
139 | })}
140 | >
141 |
173 |
174 |
175 |
176 |
177 |
178 | >
179 | )}
180 |
181 | );
182 | }
183 |
--------------------------------------------------------------------------------
/src/components/header/SaveBtn.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Router from "next/router";
3 | import { useAsyncFn } from "react-use";
4 |
5 | export default function SaveBtn({ data }) {
6 | const [state, handleSave] = useAsyncFn(async () => {
7 | const response = await window.fetch(
8 | process.env.NEXT_PUBLIC_API_URL + "/api/pen",
9 | {
10 | method: "POST",
11 | headers: {
12 | "content-type": "application/json",
13 | },
14 | body: JSON.stringify(data),
15 | }
16 | );
17 | const result = await response.json();
18 |
19 | Router.push(`/pen/${result.id}`);
20 | return result;
21 | }, [data]);
22 |
23 | return (
24 |
69 | );
70 | }
71 |
--------------------------------------------------------------------------------
/src/components/header/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Link from "next/link";
3 |
4 | export default function Header({ children }) {
5 | return (
6 |
44 | );
45 | }
46 |
--------------------------------------------------------------------------------
/src/components/monaco/index.js:
--------------------------------------------------------------------------------
1 | import * as monaco from "monaco-editor";
2 | import { CommandsRegistry } from "monaco-editor/esm/vs/platform/commands/common/commands";
3 | import PrettierWorker from "worker-loader!../../workers/prettier.worker.js";
4 | import { createWorkerQueue } from "../../utils/workers";
5 | import { getTheme } from "../../utils/theme";
6 | import colors from "tailwindcss/colors";
7 | import dlv from "dlv";
8 |
9 | function toHex(d) {
10 | return Number(d).toString(16).padStart(2, "0");
11 | }
12 |
13 | function getColor(path) {
14 | let [key, opacity = 1] = path.split("/");
15 | return (
16 | dlv(colors, key).replace("#", "") +
17 | toHex(Math.round(parseFloat(opacity) * 255))
18 | );
19 | }
20 |
21 | function makeTheme(themeColors) {
22 | return Object.entries(themeColors).map(([token, colorPath]) => ({
23 | token,
24 | foreground: getColor(colorPath),
25 | }));
26 | }
27 |
28 | window.MonacoEnvironment = {
29 | getWorkerUrl: (_moduleId, label) => {
30 | const v = `?v=${
31 | require("monaco-editor/package.json?fields=version").version
32 | }`;
33 | if (label === "css" || label === "tailwindcss")
34 | return `_next/static/chunks/css.worker.js${v}`;
35 | if (label === "html") return `_next/static/chunks/html.worker.js${v}`;
36 | if (label === "typescript" || label === "javascript")
37 | return `_next/static/chunks/ts.worker.js${v}`;
38 | return `_next/static/chunks/editor.worker.js${v}`;
39 | },
40 | };
41 |
42 | monaco.editor.defineTheme("tw-light", {
43 | base: "vs",
44 | inherit: true,
45 | rules: [
46 | { foreground: getColor("gray.800") },
47 | ...makeTheme({
48 | comment: "gray.400",
49 | string: "indigo.600",
50 | number: "gray.800",
51 | tag: "sky.600",
52 | delimiter: "gray.400",
53 | // HTML
54 | "attribute.name.html": "sky.500",
55 | "attribute.value.html": "indigo.600",
56 | "delimiter.html": "gray.400",
57 | // JS
58 | "keyword.js": "sky.600",
59 | "identifier.js": "gray.800",
60 | // CSS
61 | "attribute.name.css": "indigo.600",
62 | "attribute.value.unit.css": "teal.600",
63 | "attribute.value.number.css": "gray.800",
64 | "attribute.value.css": "gray.800",
65 | "attribute.value.hex.css": "gray.800",
66 | "keyword.css": "sky.600",
67 | "function.css": "teal.600",
68 | "pseudo.css": "sky.600",
69 | "variable.css": "gray.800",
70 | }),
71 | ],
72 | colors: {
73 | "editor.background": "#ffffff",
74 | "editor.selectionBackground": "#" + getColor("slate.200"),
75 | "editor.inactiveSelectionBackground": "#" + getColor("slate.200/0.4"),
76 | "editorLineNumber.foreground": "#" + getColor("gray.400"),
77 | "editor.lineHighlightBorder": "#" + getColor("slate.100"),
78 | "editorBracketMatch.background": "#00000000",
79 | "editorBracketMatch.border": "#" + getColor("slate.300"),
80 | "editorSuggestWidget.background": "#" + getColor("slate.50"),
81 | "editorSuggestWidget.selectedBackground": "#" + getColor("slate.400/0.1"),
82 | "editorSuggestWidget.selectedForeground": "#" + getColor("slate.700"),
83 | "editorSuggestWidget.foreground": "#" + getColor("slate.700"),
84 | "editorSuggestWidget.highlightForeground": "#" + getColor("indigo.500"),
85 | "editorSuggestWidget.focusHighlightForeground":
86 | "#" + getColor("indigo.500"),
87 | "editorHoverWidget.background": "#" + getColor("slate.50"),
88 | "editorError.foreground": "#" + getColor("red.500"),
89 | "editorWarning.foreground": "#" + getColor("yellow.500"),
90 | },
91 | });
92 |
93 | monaco.editor.defineTheme("tw-dark", {
94 | base: "vs-dark",
95 | inherit: true,
96 | rules: [
97 | { foreground: getColor("slate.50") },
98 | ...makeTheme({
99 | comment: "slate.400",
100 | string: "sky.300",
101 | number: "slate.50",
102 | tag: "pink.400",
103 | delimiter: "slate.500",
104 | // HTML
105 | "attribute.name.html": "slate.300",
106 | "attribute.value.html": "sky.300",
107 | "delimiter.html": "slate.500",
108 | // JS
109 | "keyword.js": "slate.300",
110 | "identifier.js": "slate.50",
111 | // CSS
112 | "attribute.name.css": "sky.300",
113 | "attribute.value.unit.css": "teal.200",
114 | "attribute.value.number.css": "slate.50",
115 | "attribute.value.css": "slate.50",
116 | "attribute.value.hex.css": "slate.50",
117 | "keyword.css": "slate.300",
118 | "function.css": "teal.200",
119 | "pseudo.css": "slate.300",
120 | "variable.css": "slate.50",
121 | }),
122 | ],
123 | colors: {
124 | "editor.background": "#" + getColor("slate.800"),
125 | "editor.selectionBackground": "#" + getColor("slate.700"),
126 | "editor.inactiveSelectionBackground": "#" + getColor("slate.700/0.6"),
127 | "editorLineNumber.foreground": "#" + getColor("slate.600"),
128 | "editor.lineHighlightBorder": "#" + getColor("slate.700"),
129 | "editorBracketMatch.background": "#00000000",
130 | "editorBracketMatch.border": "#" + getColor("slate.500"),
131 | "editorSuggestWidget.background": "#" + getColor("slate.700"),
132 | "editorSuggestWidget.selectedBackground": "#" + getColor("slate.400/0.12"),
133 | "editorSuggestWidget.foreground": "#" + getColor("slate.300"),
134 | "editorSuggestWidget.selectedForeground": "#" + getColor("slate.300"),
135 | "editorSuggestWidget.highlightForeground": "#" + getColor("sky.400"),
136 | "editorSuggestWidget.focusHighlightForeground": "#" + getColor("sky.400"),
137 | "editorHoverWidget.background": "#" + getColor("slate.700"),
138 | "editorError.foreground": "#" + getColor("red.400"),
139 | "editorWarning.foreground": "#" + getColor("yellow.400"),
140 | },
141 | });
142 |
143 | export function createMonacoEditor({ container, initialContent, onChange }) {
144 | let editor;
145 | let onChangeCallback = onChange;
146 | const disposables = [];
147 | let shouldTriggerOnChange = true;
148 |
149 | disposables.push(registerDocumentFormattingEditProviders());
150 |
151 | editor = monaco.editor.create(container, {
152 | fontFamily:
153 | 'Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
154 | fontSize: 14,
155 | lineHeight: 21,
156 | minimap: { enabled: false },
157 | theme: getTheme() === "dark" ? "tw-dark" : "tw-light",
158 | wordWrap: "on",
159 | fixedOverflowWidgets: true,
160 | unicodeHighlight: {
161 | ambiguousCharacters: false,
162 | },
163 | });
164 | disposables.push(editor);
165 |
166 | setupKeybindings(editor);
167 |
168 | function triggerOnChange(id, newContent) {
169 | if (onChangeCallback && shouldTriggerOnChange) {
170 | onChangeCallback(id, {
171 | html:
172 | id === "html" && typeof newContent !== "undefined"
173 | ? newContent
174 | : html.getModel()?.getValue() ?? initialContent.html,
175 | css:
176 | id === "css" && typeof newContent !== "undefined"
177 | ? newContent
178 | : css.getModel()?.getValue() ?? initialContent.css,
179 | config:
180 | id === "config" && typeof newContent !== "undefined"
181 | ? newContent
182 | : config.getModel()?.getValue() ?? initialContent.config,
183 | });
184 | }
185 | }
186 |
187 | let isInitialChange = true;
188 | editor.onDidChangeModel(() => {
189 | if (isInitialChange) {
190 | isInitialChange = false;
191 | return;
192 | }
193 | const currentModel = editor.getModel();
194 | if (currentModel === html.getModel()) {
195 | html.updateDecorations();
196 | } else if (currentModel === css.getModel()) {
197 | css.updateDecorations();
198 | }
199 | });
200 |
201 | return {
202 | editor,
203 | getValue(doc) {
204 | return documents[doc].getModel()?.getValue() ?? initialContent[doc];
205 | },
206 | reset(content) {
207 | shouldTriggerOnChange = false;
208 | initialContent = content;
209 | if (documents.html.getModel()) {
210 | documents.html.getModel().setValue(content.html);
211 | }
212 | if (documents.css.getModel()) {
213 | documents.css.getModel().setValue(content.css);
214 | }
215 | if (documents.config.getModel()) {
216 | documents.config.getModel().setValue(content.config);
217 | }
218 | window.setTimeout(() => {
219 | shouldTriggerOnChange = true;
220 | }, 0);
221 | },
222 | setTailwindVersion(tailwindVersion) {
223 | config.setTailwindVersion(tailwindVersion);
224 | },
225 | dispose() {
226 | disposables.forEach((disposable) => disposable.dispose());
227 | },
228 | setOnChange(fn) {
229 | onChangeCallback = fn;
230 | },
231 | };
232 | }
233 |
234 | function setupKeybindings(editor) {
235 | let formatCommandId = "editor.action.formatDocument";
236 | editor._standaloneKeybindingService.addDynamicKeybinding(
237 | `-${formatCommandId}`,
238 | null,
239 | () => {}
240 | );
241 | const { handler, when } = CommandsRegistry.getCommand(formatCommandId);
242 | editor._standaloneKeybindingService.addDynamicKeybinding(
243 | formatCommandId,
244 | monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS,
245 | handler,
246 | when
247 | );
248 | }
249 |
250 | function registerDocumentFormattingEditProviders() {
251 | const disposables = [];
252 | let prettierWorker;
253 |
254 | const formattingEditProvider = {
255 | async provideDocumentFormattingEdits(model, _options, _token) {
256 | if (!prettierWorker) {
257 | prettierWorker = createWorkerQueue(PrettierWorker);
258 | }
259 | const { canceled, error, pretty } = await prettierWorker.emit({
260 | text: model.getValue(),
261 | language: model.getLanguageId(),
262 | });
263 | if (canceled || error) return [];
264 | return [
265 | {
266 | range: model.getFullModelRange(),
267 | text: pretty,
268 | },
269 | ];
270 | },
271 | };
272 |
273 | // override the built-in HTML formatter
274 | const _registerDocumentFormattingEditProvider =
275 | monaco.languages.registerDocumentFormattingEditProvider;
276 | monaco.languages.registerDocumentFormattingEditProvider = (id, provider) => {
277 | if (id !== "html") {
278 | return _registerDocumentFormattingEditProvider(id, provider);
279 | }
280 | return _registerDocumentFormattingEditProvider(
281 | "html",
282 | formattingEditProvider
283 | );
284 | };
285 | disposables.push(
286 | monaco.languages.registerDocumentFormattingEditProvider(
287 | "markdown",
288 | formattingEditProvider
289 | )
290 | );
291 | disposables.push(
292 | monaco.languages.registerDocumentFormattingEditProvider(
293 | "css",
294 | formattingEditProvider
295 | )
296 | );
297 | disposables.push(
298 | monaco.languages.registerDocumentFormattingEditProvider(
299 | "javascript",
300 | formattingEditProvider
301 | )
302 | );
303 |
304 | return {
305 | dispose() {
306 | disposables.forEach((disposable) => disposable.dispose());
307 | if (prettierWorker) {
308 | prettierWorker.terminate();
309 | }
310 | },
311 | };
312 | }
313 |
--------------------------------------------------------------------------------
/src/components/monaco/markdown.js:
--------------------------------------------------------------------------------
1 | import * as monaco from 'monaco-editor'
2 | import { debounce } from 'debounce'
3 |
4 | export const HTML_URI = 'file:///index.md'
5 |
6 | export function setupMarkdownMode(content, onChange, getEditor) {
7 | const disposables = []
8 |
9 | const model = monaco.editor.createModel(
10 | content || '',
11 | 'markdown',
12 | monaco.Uri.parse(HTML_URI)
13 | )
14 | disposables.push(model)
15 |
16 | const updateDecorations = debounce(async () => {}, 100)
17 |
18 | disposables.push(
19 | model.onDidChangeContent(() => {
20 | onChange()
21 | })
22 | )
23 |
24 | return {
25 | getModel: () => model,
26 | updateDecorations,
27 | activate: () => {
28 | getEditor().setModel(model)
29 | },
30 | dispose() {
31 | disposables.forEach((disposable) => disposable.dispose())
32 | },
33 | }
34 | }
35 |
36 | export const CSS_URI = 'file:///index.css'
37 |
38 | export function setupCssMode(content, onChange, getEditor) {
39 | const disposables = []
40 |
41 | const model = monaco.editor.createModel(
42 | content || '',
43 | 'css',
44 | monaco.Uri.parse(CSS_URI)
45 | )
46 | disposables.push(model)
47 |
48 | const updateDecorations = debounce(async () => {}, 100)
49 |
50 | disposables.push(
51 | model.onDidChangeContent(() => {
52 | onChange()
53 | })
54 | )
55 |
56 | return {
57 | getModel: () => model,
58 | updateDecorations,
59 | activate: () => {
60 | getEditor().setModel(model)
61 | },
62 | dispose() {
63 | disposables.forEach((disposable) => disposable.dispose())
64 | },
65 | }
66 | }
67 |
68 | export const JS_URI = 'file:///index.js'
69 |
70 | export function setupJavaScriptMode(content, onChange, getEditor) {
71 | const disposables = []
72 |
73 | const model = monaco.editor.createModel(
74 | content || '',
75 | 'javascript',
76 | monaco.Uri.parse(JS_URI)
77 | )
78 | disposables.push(model)
79 |
80 | const updateDecorations = debounce(async () => {}, 100)
81 |
82 | disposables.push(
83 | model.onDidChangeContent(() => {
84 | onChange()
85 | })
86 | )
87 |
88 | return {
89 | getModel: () => model,
90 | updateDecorations,
91 | activate: () => {
92 | getEditor().setModel(model)
93 | },
94 | dispose() {
95 | disposables.forEach((disposable) => disposable.dispose())
96 | },
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/src/components/select/index.js:
--------------------------------------------------------------------------------
1 | import { Fragment } from "react";
2 | import { Listbox, Transition } from "@headlessui/react";
3 |
4 | export default function Select({ value, onChange, options }) {
5 | return (
6 |
7 |
8 |
9 |
10 | {value}
11 |
12 |
26 |
27 |
28 |
34 |
35 | {options.map((option, index) => (
36 |
39 | `cursor-pointer select-none relative py-1 pl-8 pr-3 ${
40 | active ? "text-gray-200 bg-sky-600" : "text-gray-50"
41 | }`
42 | }
43 | value={option}
44 | >
45 | {({ selected }) => (
46 | <>
47 |
52 | {option}
53 |
54 | {selected ? (
55 |
56 |
70 |
71 | ) : null}
72 | >
73 | )}
74 |
75 | ))}
76 |
77 |
78 |
79 |
80 |
81 | );
82 | }
83 |
--------------------------------------------------------------------------------
/src/components/setting/Modal.js:
--------------------------------------------------------------------------------
1 | import { Dialog, Transition } from "@headlessui/react";
2 | import { Fragment, useState } from "react";
3 | import SaveBtn from "../header/SaveBtn";
4 |
5 | export default function Modal(props) {
6 | const [name, setName] = useState("");
7 | const [scripts, setScripts] = useState([]);
8 | const [styles, setStyles] = useState([]);
9 | let [isOpen, setIsOpen] = useState(false);
10 |
11 | function handleOK() {
12 | props.onChange({ name, scripts, styles });
13 | setIsOpen(false);
14 | }
15 |
16 | function closeModal() {
17 | setIsOpen(false);
18 | }
19 |
20 | function openModal() {
21 | setName(props.name);
22 | setStyles(props.styles || []);
23 | setScripts(props.scripts || []);
24 | setIsOpen(true);
25 | }
26 |
27 | const handleChangeStyles = (index, value) => {
28 | const newStyles = [...styles];
29 | newStyles[index] = value;
30 | setScripts(newStyles);
31 | };
32 |
33 | const handleChangeScripts = (index, value) => {
34 | const newScripts = [...scripts];
35 | newScripts[index] = value;
36 | setScripts(newScripts);
37 | };
38 |
39 | const handleRemoveStyles = (index) => {
40 | const newStyles = [...styles];
41 | newStyles.splice(index, 1);
42 | setStyles(newStyles);
43 | };
44 |
45 | const handleRemoveScripts = (index) => {
46 | const newScripts = [...scripts];
47 | newScripts.splice(index, 1);
48 | setScripts(newScripts);
49 | };
50 |
51 | return (
52 | <>
53 |
79 |
80 |
81 |
248 |
249 | >
250 | );
251 | }
252 |
--------------------------------------------------------------------------------
/src/hooks/useDebouncedState.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect, useRef } from 'react'
2 |
3 | export function useDebouncedState(initialValue, timeout = 100) {
4 | const [value, setValue] = useState({ value: initialValue })
5 | const [debouncedValue, setDebouncedValue] = useState(initialValue)
6 | const handler = useRef()
7 |
8 | useEffect(() => {
9 | handler.current = window.setTimeout(() => {
10 | setDebouncedValue(value.value)
11 | }, timeout)
12 | return () => {
13 | window.clearTimeout(handler.current)
14 | }
15 | }, [value, timeout])
16 |
17 | return [
18 | // X
19 | debouncedValue,
20 | // setX
21 | (newValue) => {
22 | setValue({ value: newValue })
23 | },
24 | // setXImmediate
25 | (newValue) => {
26 | window.clearTimeout(handler.current)
27 | setDebouncedValue(newValue)
28 | },
29 | // cancelSetX
30 | () => window.clearTimeout(handler.current),
31 | ]
32 | }
33 |
--------------------------------------------------------------------------------
/src/hooks/useIsomorphicLayoutEffect.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useLayoutEffect } from 'react'
2 |
3 | export const useIsomorphicLayoutEffect =
4 | typeof window !== 'undefined' ? useLayoutEffect : useEffect
5 |
--------------------------------------------------------------------------------
/src/pages/_app.js:
--------------------------------------------------------------------------------
1 | import "../styles/globals.css";
2 | import Head from "next/head";
3 |
4 | const TITLE = "Code Editor | 一个纯前端在线代码实时预览工具";
5 | const DESCRIPTION =
6 | "一个纯前端的在线代码实时预览工具,支持 Less Scss JavaScript Typescript。";
7 | const FAVICON_VERSION = 1;
8 |
9 | function v(href) {
10 | return `${href}?v=${FAVICON_VERSION}`;
11 | }
12 |
13 | function MyApp({ Component, pageProps }) {
14 | return (
15 | <>
16 |
17 |
22 |
28 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | {TITLE}
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 | >
53 | );
54 | }
55 |
56 | export default MyApp;
57 |
--------------------------------------------------------------------------------
/src/pages/api/hello.js:
--------------------------------------------------------------------------------
1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction
2 |
3 | export default function handler(req, res) {
4 | res.status(200).json({ name: 'John Doe' })
5 | }
6 |
--------------------------------------------------------------------------------
/src/pages/api/thumbnail.js:
--------------------------------------------------------------------------------
1 | import chromium from "chrome-aws-lambda";
2 | import playwright from "playwright-core";
3 |
4 | const getAbsoluteURL = (path) => {
5 | const baseURL = process.env.VERCEL_URL
6 | ? `https://${process.env.VERCEL_URL}`
7 | : "http://localhost:3000";
8 | return baseURL + path;
9 | };
10 |
11 | export default async function handler(req, res) {
12 | // Start the browser with the AWS Lambda wrapper (chrome-aws-lambda)
13 | const browser = await playwright.chromium.launch({
14 | args: chromium.args,
15 | executablePath: await chromium.executablePath,
16 | headless: chromium.headless,
17 | });
18 | // 创建一个页面,并设置视窗大小
19 | const page = await browser.newPage({
20 | viewport: {
21 | width: 1200,
22 | height: 630,
23 | },
24 | });
25 | // 从url path 拼接成完成路径
26 | const url = getAbsoluteURL(req.query["path"] || "");
27 | await page.goto(url, {
28 | timeout: 15 * 1000,
29 | waitUntil: "networkidle",
30 | });
31 | await page.waitForTimeout(1000);
32 | // 生成png 的缩略图
33 | const data = await page.screenshot({
34 | type: "png",
35 | });
36 | await browser.close();
37 | // 设置图片强缓存
38 | res.setHeader("Cache-Control", "s-maxage=31536000, stale-while-revalidate");
39 | res.setHeader("Content-Type", "image/png");
40 | // 设置返回 Content-Type 图片格式
41 | res.end(data);
42 | }
43 |
--------------------------------------------------------------------------------
/src/pages/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable @next/next/no-img-element */
2 | import Link from "next/link";
3 | import Head from "next/head";
4 | import Header from "../components/header";
5 | import { list } from "../utils/database";
6 |
7 | export default function Home({ data }) {
8 | return (
9 | <>
10 |
11 |
12 |
13 |
17 |
21 |
22 |
23 |
70 |
88 | >
89 | );
90 | }
91 |
92 | export async function getServerSideProps({ params, res, query }) {
93 | try {
94 | const result = await list();
95 | return {
96 | props: {
97 | data: result.data,
98 | },
99 | };
100 | } catch (error) {
101 | return {
102 | props: {
103 | errorCode: error.status || 500,
104 | },
105 | };
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/src/pages/pen/[...id].js:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useEffect, useRef, useState } from "react";
2 | import Error from "next/error";
3 | import Head from "next/head";
4 | import { Editor } from "../../components/Editor";
5 | import Preview from "../../components/Preview";
6 | import Header from "../../components/header";
7 | import LayoutSwitch from "../../components/header/LayoutSwitch";
8 | import Select from "../../components/select";
9 | import SplitPane from "react-split-pane";
10 | import { useDebounce } from "react-use";
11 | import Worker from "worker-loader!../../workers/compile.worker.js";
12 | import { requestResponse } from "../../utils/workers";
13 | import { compileScss } from "../../utils/compile";
14 | import { get } from "../../utils/database";
15 | import Modal from "../../components/setting/Modal";
16 | import SaveBtn from "../../components/header/SaveBtn";
17 |
18 | function Pen({
19 | id = "",
20 | initialContent = {
21 | html: "",
22 | css: "",
23 | js: "",
24 | scripts: [],
25 | styles: [],
26 | cssLang: "css",
27 | jsLang: "babel",
28 | htmlLang: "html",
29 | },
30 | }) {
31 | const [state, setState] = useState(initialContent);
32 | const { html, htmlLang, css, cssLang, js, jsLang, name, styles, scripts } =
33 | state;
34 | const setCssLang = (cssLang) => setState((prev) => ({ ...prev, cssLang }));
35 | const setJsLang = (jsLang) => setState((prev) => ({ ...prev, jsLang }));
36 | const setHtmlLang = (htmlLang) => setState((prev) => ({ ...prev, htmlLang }));
37 | const setBase = (base) => setState((prev) => ({ ...prev, ...base }));
38 |
39 | const previewRef = useRef();
40 | const [layout, setLayout] = useState("left");
41 | const worker = useRef();
42 |
43 | useEffect(() => {
44 | worker.current = new Worker();
45 | return () => {
46 | worker.current.terminate();
47 | };
48 | }, []);
49 |
50 | const inject = async (content) => {
51 | previewRef.current.contentWindow.postMessage(content, "*");
52 | };
53 |
54 | const compileNow = async (content) => {
55 | let { canceled, error, ...other } = await requestResponse(
56 | worker.current,
57 | content
58 | );
59 | if (content.cssLang === "scss") {
60 | try {
61 | other.css = await compileScss(content.css);
62 | } catch (error) {
63 | other.css = undefined;
64 | }
65 | }
66 |
67 | if (canceled) {
68 | return;
69 | }
70 |
71 | inject(other);
72 | };
73 |
74 | const [, cancel] = useDebounce(
75 | () => {
76 | compileNow(state);
77 | },
78 | 1000,
79 | [state]
80 | );
81 |
82 | const handleChangeHtml = (value) => {
83 | setState((prev) => ({ ...prev, html: value }));
84 | };
85 |
86 | const handleChangeCss = (value) => {
87 | setState((prev) => ({ ...prev, css: value }));
88 | };
89 | const handleChangeJs = (value) => {
90 | setState((prev) => ({ ...prev, js: value }));
91 | };
92 |
93 | const panes = [
94 |
99 |
100 |
101 |
108 | HTML
109 |
110 |
115 |
116 |
120 |
121 |
122 |
129 | CSS
130 |
135 |
136 |
141 |
142 |
143 |
144 |
151 | JS
152 |
157 |
158 |
163 |
164 |
165 | ,
166 |
167 |
168 |
{
174 | inject({
175 | html,
176 | });
177 | }}
178 | />
179 | ,
180 | ];
181 |
182 | return (
183 |
184 |
194 |
195 |
196 |
201 | {layout === "left"
202 | ? panes.map((p) => p)
203 | : panes.reverse().map((p) => p)}
204 |
205 |
206 |
207 | );
208 | }
209 |
210 | export default function Page({ errorCode, ...props }) {
211 | const [visible, setVisible] = useState(false);
212 | useEffect(() => {
213 | setVisible(true);
214 | }, []);
215 |
216 | if (errorCode) {
217 | return ;
218 | }
219 | return (
220 | <>
221 |
222 |
223 |
224 |
228 |
232 |
233 | {visible ? : null}
234 | >
235 | );
236 | }
237 |
238 | export async function getServerSideProps({ params, res, query }) {
239 | if (params.id.length !== 1) {
240 | return {
241 | props: {
242 | errorCode: 404,
243 | },
244 | };
245 | }
246 | if (params.id && params.id[0] === "create") {
247 | res.setHeader(
248 | "cache-control",
249 | "public, max-age=0, must-revalidate, s-maxage=31536000"
250 | );
251 | return {
252 | props: {},
253 | };
254 | } else {
255 | try {
256 | const initialContent = await get({
257 | id: params.id[0],
258 | });
259 |
260 | res.setHeader(
261 | "cache-control",
262 | "public, max-age=0, must-revalidate, s-maxage=31536000"
263 | );
264 |
265 | return {
266 | props: {
267 | id: params.id[0],
268 | initialContent,
269 | },
270 | };
271 | } catch (error) {
272 | return {
273 | props: {
274 | errorCode: error.status || 500,
275 | },
276 | };
277 | }
278 | }
279 | }
280 |
--------------------------------------------------------------------------------
/src/pages/preview/[...id].js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef } from "react";
2 | import Head from "next/head";
3 | import Error from "next/error";
4 | import Preview from "../../components/Preview";
5 | import Worker from "worker-loader!../../workers/compile.worker.js";
6 | import { requestResponse } from "../../utils/workers";
7 | import { compileScss } from "../../utils/compile";
8 | import { get } from "../../utils/database";
9 |
10 | export default function Page({
11 | id = "",
12 | errorCode,
13 | initialContent = {
14 | html: "",
15 | css: "",
16 | js: "",
17 | scripts: [],
18 | styles: [],
19 | cssLang: "css",
20 | jsLang: "babel",
21 | htmlLang: "html",
22 | },
23 | }) {
24 | const previewRef = useRef();
25 | const worker = useRef();
26 |
27 | useEffect(() => {
28 | worker.current = new Worker();
29 | return () => {
30 | worker.current.terminate();
31 | };
32 | }, []);
33 |
34 | const inject = async (content) => {
35 | previewRef.current.contentWindow.postMessage(content, "*");
36 | };
37 |
38 | const compileNow = async (content) => {
39 | let { canceled, error, ...other } = await requestResponse(
40 | worker.current,
41 | content
42 | );
43 | if (content.cssLang === "scss") {
44 | try {
45 | other.css = await compileScss(content.css);
46 | } catch (error) {
47 | other.css = undefined;
48 | }
49 | }
50 |
51 | if (canceled) {
52 | return;
53 | }
54 |
55 | inject(other);
56 | };
57 |
58 | useEffect(() => {
59 | compileNow(initialContent);
60 | }, [initialContent]);
61 |
62 | if (errorCode) {
63 | return ;
64 | }
65 | return (
66 |
67 |
68 |
69 |
70 |
74 |
78 |
79 |
{
85 | inject({
86 | html: initialContent.html,
87 | });
88 | }}
89 | />
90 |
91 | );
92 | }
93 |
94 | export async function getServerSideProps({ params, res, query }) {
95 | if (params.id.length !== 1) {
96 | return {
97 | props: {
98 | errorCode: 404,
99 | },
100 | };
101 | }
102 |
103 | try {
104 | const initialContent = await get({
105 | id: params.id[0],
106 | });
107 |
108 | res.setHeader(
109 | "cache-control",
110 | "public, max-age=0, must-revalidate, s-maxage=31536000"
111 | );
112 |
113 | return {
114 | props: {
115 | id: params.id[0],
116 | initialContent,
117 | },
118 | };
119 | } catch (error) {
120 | return {
121 | props: {
122 | errorCode: error.status || 500,
123 | },
124 | };
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/src/pages/qrcode-generator.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable @next/next/no-img-element */
2 | import React from "react";
3 | import QRCode from "qrcode";
4 |
5 | export default function QrcodePage() {
6 | const [input, setInput] = React.useState("");
7 | const [error, setError] = React.useState("");
8 | const [colorDark, setColorDark] = React.useState("#000000");
9 | const [colorLight, setColorLight] = React.useState("#ffffff");
10 | const [qrcode, setQrcode] = React.useState(
11 | "https://mp.weixin.qq.com/mp/qrcode?scene=10000004&size=302&__biz=Mzg4MTcyNDY4OQ==&mid=2247487204&idx=1&sn=b8b9b38fe6f172fdc4b68bac08d3cfd4&send_time="
12 | );
13 |
14 | const handleInputChange = async (e) => {
15 | setInput(e.target.value);
16 | console.log(colorDark);
17 | try {
18 | const dataURL = await QRCode.toDataURL(e.target.value, {
19 | width: 300,
20 | margin: 3,
21 | color: {
22 | dark: colorDark,
23 | light: colorLight,
24 | },
25 | });
26 | setQrcode(dataURL);
27 | setError("");
28 | } catch (err) {
29 | setError(err.message);
30 | console.error(err);
31 | }
32 | };
33 |
34 | return (
35 |
36 |
二维码生成器
37 |
38 |
39 |
46 | {error ? (
47 |
51 |
63 |
{error}
64 |
65 | ) : null}
66 |
67 |
68 |
87 | {qrcode && (
88 |

93 | )}
94 |
95 |
96 |
97 | );
98 | }
99 |
--------------------------------------------------------------------------------
/src/styles/Home.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | padding: 0 2rem;
3 | }
4 |
5 | .main {
6 | min-height: 100vh;
7 | padding: 4rem 0;
8 | flex: 1;
9 | display: flex;
10 | flex-direction: column;
11 | justify-content: center;
12 | align-items: center;
13 | }
14 |
15 | .footer {
16 | display: flex;
17 | flex: 1;
18 | padding: 2rem 0;
19 | border-top: 1px solid #eaeaea;
20 | justify-content: center;
21 | align-items: center;
22 | }
23 |
24 | .footer a {
25 | display: flex;
26 | justify-content: center;
27 | align-items: center;
28 | flex-grow: 1;
29 | }
30 |
31 | .title a {
32 | color: #0070f3;
33 | text-decoration: none;
34 | }
35 |
36 | .title a:hover,
37 | .title a:focus,
38 | .title a:active {
39 | text-decoration: underline;
40 | }
41 |
42 | .title {
43 | margin: 0;
44 | line-height: 1.15;
45 | font-size: 4rem;
46 | }
47 |
48 | .title,
49 | .description {
50 | text-align: center;
51 | }
52 |
53 | .description {
54 | margin: 4rem 0;
55 | line-height: 1.5;
56 | font-size: 1.5rem;
57 | }
58 |
59 | .code {
60 | background: #fafafa;
61 | border-radius: 5px;
62 | padding: 0.75rem;
63 | font-size: 1.1rem;
64 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
65 | Bitstream Vera Sans Mono, Courier New, monospace;
66 | }
67 |
68 | .grid {
69 | display: flex;
70 | align-items: center;
71 | justify-content: center;
72 | flex-wrap: wrap;
73 | max-width: 800px;
74 | }
75 |
76 | .card {
77 | margin: 1rem;
78 | padding: 1.5rem;
79 | text-align: left;
80 | color: inherit;
81 | text-decoration: none;
82 | border: 1px solid #eaeaea;
83 | border-radius: 10px;
84 | transition: color 0.15s ease, border-color 0.15s ease;
85 | max-width: 300px;
86 | }
87 |
88 | .card:hover,
89 | .card:focus,
90 | .card:active {
91 | color: #0070f3;
92 | border-color: #0070f3;
93 | }
94 |
95 | .card h2 {
96 | margin: 0 0 1rem 0;
97 | font-size: 1.5rem;
98 | }
99 |
100 | .card p {
101 | margin: 0;
102 | font-size: 1.25rem;
103 | line-height: 1.5;
104 | }
105 |
106 | .logo {
107 | height: 1em;
108 | margin-left: 0.5rem;
109 | }
110 |
111 | @media (max-width: 600px) {
112 | .grid {
113 | width: 100%;
114 | flex-direction: column;
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/src/styles/codemirror/base.css:
--------------------------------------------------------------------------------
1 | /* BASICS */
2 |
3 | .CodeMirror {
4 | /* Set height, width, borders, and global font properties here */
5 | font-family: Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
6 | monospace;
7 | font-size: 16px;
8 | line-height: 24px;
9 | height: 100%;
10 | color: theme('colors.gray.800');
11 | direction: ltr;
12 | }
13 |
14 | /* PADDING */
15 |
16 | .CodeMirror-lines {
17 | padding: 4px 0; /* Vertical padding around content */
18 | }
19 | .CodeMirror pre.CodeMirror-line,
20 | .CodeMirror pre.CodeMirror-line-like {
21 | padding: 0 4px; /* Horizontal padding of content */
22 | }
23 |
24 | .CodeMirror-scrollbar-filler,
25 | .CodeMirror-gutter-filler {
26 | background-color: white; /* The little square between H and V scrollbars */
27 | }
28 |
29 | /* GUTTER */
30 |
31 | .CodeMirror-gutters {
32 | background: #fff;
33 | white-space: nowrap;
34 | }
35 | .CodeMirror-linenumbers {
36 | }
37 | .CodeMirror-linenumber {
38 | padding: 0 3px 0 5px;
39 | min-width: 20px;
40 | text-align: right;
41 | color: theme('colors.gray.200');
42 | white-space: nowrap;
43 | }
44 |
45 | .CodeMirror-guttermarker {
46 | color: black;
47 | }
48 | .CodeMirror-guttermarker-subtle {
49 | color: #999;
50 | }
51 |
52 | /* CURSOR */
53 |
54 | .CodeMirror-cursor {
55 | border-left: 2px solid black;
56 | border-right: none;
57 | width: 0;
58 | }
59 | /* Shown when moving in bi-directional text */
60 | .CodeMirror div.CodeMirror-secondarycursor {
61 | border-left: 1px solid silver;
62 | }
63 | .cm-fat-cursor .CodeMirror-cursor {
64 | width: auto;
65 | border: 0 !important;
66 | background: #7e7;
67 | }
68 | .cm-fat-cursor div.CodeMirror-cursors {
69 | z-index: 1;
70 | }
71 | .cm-fat-cursor-mark {
72 | background-color: rgba(20, 255, 20, 0.5);
73 | -webkit-animation: blink 1.06s steps(1) infinite;
74 | -moz-animation: blink 1.06s steps(1) infinite;
75 | animation: blink 1.06s steps(1) infinite;
76 | }
77 | .cm-animate-fat-cursor {
78 | width: auto;
79 | border: 0;
80 | -webkit-animation: blink 1.06s steps(1) infinite;
81 | -moz-animation: blink 1.06s steps(1) infinite;
82 | animation: blink 1.06s steps(1) infinite;
83 | background-color: #7e7;
84 | }
85 | @-moz-keyframes blink {
86 | 0% {
87 | }
88 | 50% {
89 | background-color: transparent;
90 | }
91 | 100% {
92 | }
93 | }
94 | @-webkit-keyframes blink {
95 | 0% {
96 | }
97 | 50% {
98 | background-color: transparent;
99 | }
100 | 100% {
101 | }
102 | }
103 | @keyframes blink {
104 | 0% {
105 | }
106 | 50% {
107 | background-color: transparent;
108 | }
109 | 100% {
110 | }
111 | }
112 |
113 | /* Can style cursor different in overwrite (non-insert) mode */
114 | .CodeMirror-overwrite .CodeMirror-cursor {
115 | }
116 |
117 | .cm-tab {
118 | display: inline-block;
119 | text-decoration: inherit;
120 | }
121 |
122 | .CodeMirror-rulers {
123 | position: absolute;
124 | left: 0;
125 | right: 0;
126 | top: -50px;
127 | bottom: 0;
128 | overflow: hidden;
129 | }
130 | .CodeMirror-ruler {
131 | border-left: 1px solid #ccc;
132 | top: 0;
133 | bottom: 0;
134 | position: absolute;
135 | }
136 |
137 | /* DEFAULT THEME */
138 |
139 | .cm-s-light .cm-header {
140 | color: blue;
141 | }
142 | .cm-s-light .cm-quote {
143 | color: #090;
144 | }
145 | .cm-negative {
146 | color: #d44;
147 | }
148 | .cm-positive {
149 | color: #292;
150 | }
151 | .cm-header,
152 | .cm-strong {
153 | font-weight: bold;
154 | }
155 | .cm-em {
156 | font-style: italic;
157 | }
158 | .cm-link {
159 | text-decoration: underline;
160 | }
161 | .cm-strikethrough {
162 | text-decoration: line-through;
163 | }
164 |
165 | .cm-s-light .cm-keyword {
166 | color: theme('colors.sky.600');
167 | }
168 | .cm-s-light .cm-def {
169 | color: theme('colors.sky.600');
170 | }
171 | .cm-s-light .cm-def.cm-m-javascript {
172 | color: inherit;
173 | }
174 | .cm-s-light .cm-property {
175 | color: theme('colors.indigo.600');
176 | }
177 | .cm-s-light .cm-comment {
178 | color: theme('colors.gray.500');
179 | }
180 | .cm-s-light .cm-string,
181 | .cm-s-light .cm-string-2 {
182 | color: theme('colors.indigo.600');
183 | }
184 | .cm-s-light .cm-qualifier {
185 | color: theme('colors.sky.600');
186 | }
187 | .cm-s-light .cm-tag {
188 | color: theme('colors.sky.600');
189 | }
190 | .cm-s-light .cm-bracket {
191 | color: theme('colors.gray.300');
192 | }
193 | /* HTML attribute `=` */
194 | .cm-s-light .cm-bracket + .cm-tag ~ .cm-attribute + [class='cm-m-xml'] {
195 | color: theme('colors.gray.300');
196 | }
197 | .cm-s-light .cm-tag.cm-m-tailwindcss {
198 | color: theme('colors.gray.800');
199 | }
200 | .cm-s-light .cm-attribute {
201 | color: theme('colors.sky.400');
202 | }
203 | .cm-s-light .cm-hr {
204 | color: #999;
205 | }
206 | .cm-s-light .cm-link {
207 | color: #00c;
208 | }
209 | .cm-s-light .cm-callee {
210 | color: theme('colors.teal.600');
211 | }
212 | .cm-s-light .cm-variable.cm-m-javascript,
213 | .cm-s-light .cm-operator.cm-m-javascript,
214 | .cm-s-light .cm-property.cm-m-javascript {
215 | color: inherit;
216 | }
217 |
218 | .cm-s-light .cm-error {
219 | color: #f00;
220 | }
221 | .cm-invalidchar {
222 | color: #f00;
223 | }
224 |
225 | .CodeMirror-composing {
226 | border-bottom: 2px solid;
227 | }
228 |
229 | /* Default styles for common addons */
230 |
231 | div.CodeMirror span.CodeMirror-matchingbracket {
232 | color: #0b0;
233 | }
234 | div.CodeMirror span.CodeMirror-nonmatchingbracket {
235 | color: #a22;
236 | }
237 | .CodeMirror-matchingtag {
238 | background: rgba(255, 150, 0, 0.3);
239 | }
240 | .CodeMirror-activeline-background {
241 | background: #e8f2ff;
242 | }
243 |
244 | /* STOP */
245 |
246 | /* The rest of this file contains styles related to the mechanics of
247 | the editor. You probably shouldn't touch them. */
248 |
249 | .CodeMirror {
250 | position: relative;
251 | overflow: hidden;
252 | background: white;
253 | }
254 |
255 | .CodeMirror-scroll {
256 | overflow: scroll !important; /* Things will break if this is overridden */
257 | /* 50px is the magic margin used to hide the element's real scrollbars */
258 | /* See overflow: hidden in .CodeMirror */
259 | margin-bottom: -50px;
260 | margin-right: -50px;
261 | padding-bottom: 50px;
262 | height: 100%;
263 | outline: none; /* Prevent dragging from highlighting the element */
264 | position: relative;
265 | }
266 | .CodeMirror-sizer {
267 | position: relative;
268 | border-right: 50px solid transparent;
269 | }
270 |
271 | /* The fake, visible scrollbars. Used to force redraw during scrolling
272 | before actual scrolling happens, thus preventing shaking and
273 | flickering artifacts. */
274 | .CodeMirror-vscrollbar,
275 | .CodeMirror-hscrollbar,
276 | .CodeMirror-scrollbar-filler,
277 | .CodeMirror-gutter-filler {
278 | position: absolute;
279 | z-index: 6;
280 | display: none;
281 | }
282 | .CodeMirror-vscrollbar {
283 | right: 0;
284 | top: 0;
285 | overflow-x: hidden;
286 | overflow-y: scroll;
287 | }
288 | .CodeMirror-hscrollbar {
289 | bottom: 0;
290 | left: 0;
291 | overflow-y: hidden;
292 | overflow-x: scroll;
293 | }
294 | .CodeMirror-scrollbar-filler {
295 | right: 0;
296 | bottom: 0;
297 | }
298 | .CodeMirror-gutter-filler {
299 | left: 0;
300 | bottom: 0;
301 | }
302 |
303 | .CodeMirror-gutters {
304 | position: absolute;
305 | left: 0;
306 | top: 0;
307 | min-height: 100%;
308 | z-index: 3;
309 | }
310 | .CodeMirror-gutter {
311 | white-space: normal;
312 | height: 100%;
313 | display: inline-block;
314 | vertical-align: top;
315 | margin-bottom: -50px;
316 | }
317 | .CodeMirror-gutter-wrapper {
318 | position: absolute;
319 | z-index: 4;
320 | background: none !important;
321 | border: none !important;
322 | }
323 | .CodeMirror-gutter-background {
324 | position: absolute;
325 | top: 0;
326 | bottom: 0;
327 | z-index: 4;
328 | }
329 | .CodeMirror-gutter-elt {
330 | position: absolute;
331 | cursor: default;
332 | z-index: 4;
333 | }
334 | .CodeMirror-gutter-wrapper ::selection {
335 | background-color: transparent;
336 | }
337 | .CodeMirror-gutter-wrapper ::-moz-selection {
338 | background-color: transparent;
339 | }
340 |
341 | .CodeMirror-lines {
342 | cursor: text;
343 | min-height: 1px; /* prevents collapsing before first draw */
344 | }
345 | .CodeMirror pre.CodeMirror-line,
346 | .CodeMirror pre.CodeMirror-line-like {
347 | /* Reset some styles that the rest of the page might have set */
348 | -moz-border-radius: 0;
349 | -webkit-border-radius: 0;
350 | border-radius: 0;
351 | border-width: 0;
352 | background: transparent;
353 | font-family: inherit;
354 | font-size: inherit;
355 | margin: 0;
356 | white-space: pre;
357 | word-wrap: normal;
358 | line-height: inherit;
359 | color: inherit;
360 | z-index: 2;
361 | position: relative;
362 | overflow: visible;
363 | -webkit-tap-highlight-color: transparent;
364 | -webkit-font-variant-ligatures: contextual;
365 | font-variant-ligatures: contextual;
366 | }
367 | .CodeMirror-wrap pre.CodeMirror-line,
368 | .CodeMirror-wrap pre.CodeMirror-line-like {
369 | word-wrap: break-word;
370 | white-space: pre-wrap;
371 | word-break: normal;
372 | }
373 |
374 | .CodeMirror-linebackground {
375 | position: absolute;
376 | left: 0;
377 | right: 0;
378 | top: 0;
379 | bottom: 0;
380 | z-index: 0;
381 | }
382 |
383 | .CodeMirror-linewidget {
384 | position: relative;
385 | z-index: 2;
386 | padding: 0.1px; /* Force widget margins to stay inside of the container */
387 | }
388 |
389 | .CodeMirror-widget {
390 | }
391 |
392 | .CodeMirror-rtl pre {
393 | direction: rtl;
394 | }
395 |
396 | .CodeMirror-code {
397 | outline: none;
398 | }
399 |
400 | /* Force content-box sizing for the elements where we expect it */
401 | .CodeMirror-scroll,
402 | .CodeMirror-sizer,
403 | .CodeMirror-gutter,
404 | .CodeMirror-gutters,
405 | .CodeMirror-linenumber {
406 | -moz-box-sizing: content-box;
407 | box-sizing: content-box;
408 | }
409 |
410 | .CodeMirror-measure {
411 | position: absolute;
412 | width: 100%;
413 | height: 0;
414 | overflow: hidden;
415 | visibility: hidden;
416 | }
417 |
418 | .CodeMirror-cursor {
419 | position: absolute;
420 | pointer-events: none;
421 | }
422 | .CodeMirror-measure pre {
423 | position: static;
424 | }
425 |
426 | div.CodeMirror-cursors {
427 | visibility: hidden;
428 | position: relative;
429 | z-index: 3;
430 | }
431 | div.CodeMirror-dragcursors {
432 | visibility: visible;
433 | }
434 |
435 | .CodeMirror-focused div.CodeMirror-cursors {
436 | visibility: visible;
437 | }
438 |
439 | .CodeMirror-selected {
440 | background: #e5ebf1;
441 | }
442 | .CodeMirror-focused .CodeMirror-selected {
443 | background: #add6ff;
444 | }
445 | .CodeMirror-crosshair {
446 | cursor: crosshair;
447 | }
448 | .CodeMirror-line::selection,
449 | .CodeMirror-line > span::selection,
450 | .CodeMirror-line > span > span::selection {
451 | background: #add6ff;
452 | }
453 | .CodeMirror-line::-moz-selection,
454 | .CodeMirror-line > span::-moz-selection,
455 | .CodeMirror-line > span > span::-moz-selection {
456 | background: #add6ff;
457 | }
458 |
459 | .cm-searching {
460 | background-color: #ffa;
461 | background-color: rgba(255, 255, 0, 0.4);
462 | }
463 |
464 | /* Used to force a border model for a node */
465 | .cm-force-border {
466 | padding-right: 0.1px;
467 | }
468 |
469 | @media print {
470 | /* Hide the cursor when printing */
471 | .CodeMirror div.CodeMirror-cursors {
472 | visibility: hidden;
473 | }
474 | }
475 |
476 | /* See issue #2901 */
477 | .cm-tab-wrap-hack:after {
478 | content: '';
479 | }
480 |
481 | /* Help users use markselection to safely style text background */
482 | span.CodeMirror-selectedtext {
483 | background: none;
484 | }
485 |
--------------------------------------------------------------------------------
/src/styles/codemirror/dark.css:
--------------------------------------------------------------------------------
1 | /*
2 | Name: material
3 | Author: Mattia Astorino (http://github.com/equinusocio)
4 | Website: https://material-theme.site/
5 | */
6 |
7 | .cm-s-dark.CodeMirror {
8 | background-color: theme('colors.gray.800');
9 | color: theme('colors.gray.50');
10 | }
11 |
12 | .cm-s-dark .CodeMirror-gutters {
13 | background: theme('colors.gray.800');
14 | color: theme('colors.gray.600');
15 | border: none;
16 | }
17 |
18 | .cm-s-dark .CodeMirror-guttermarker,
19 | .cm-s-dark .CodeMirror-guttermarker-subtle,
20 | .cm-s-dark .CodeMirror-linenumber {
21 | color: theme('colors.gray.600');
22 | }
23 |
24 | .cm-s-dark .CodeMirror-cursor {
25 | border-left: 2px solid #aeafad;
26 | }
27 |
28 | .cm-s-dark div.CodeMirror-selected {
29 | background: #3a3d41;
30 | }
31 |
32 | .cm-s-dark.CodeMirror-focused div.CodeMirror-selected {
33 | background: #264f78;
34 | }
35 |
36 | .cm-s-dark .CodeMirror-line::selection,
37 | .cm-s-dark .CodeMirror-line > span::selection,
38 | .cm-s-dark .CodeMirror-line > span > span::selection {
39 | background: rgba(128, 203, 196, 0.2);
40 | }
41 |
42 | .cm-s-dark .CodeMirror-line::-moz-selection,
43 | .cm-s-dark .CodeMirror-line > span::-moz-selection,
44 | .cm-s-dark .CodeMirror-line > span > span::-moz-selection {
45 | background: rgba(128, 203, 196, 0.2);
46 | }
47 |
48 | .cm-s-dark .CodeMirror-activeline-background {
49 | background: rgba(0, 0, 0, 0.5);
50 | }
51 |
52 | .cm-s-dark .cm-keyword {
53 | color: theme('colors.gray.300');
54 | }
55 |
56 | .cm-s-dark .cm-number {
57 | color: theme('colors.gray.50');
58 | }
59 |
60 | .cm-s-dark .cm-def {
61 | color: theme('colors.gray.300');
62 | }
63 |
64 | .cm-s-dark .cm-string,
65 | .cm-s-dark .cm-string-2 {
66 | color: theme('colors.sky.300');
67 | }
68 |
69 | .cm-s-dark .cm-comment {
70 | color: theme('colors.gray.400');
71 | }
72 |
73 | .cm-s-dark .cm-tag {
74 | color: theme('colors.pink.400');
75 | }
76 |
77 | /* HTML attribute `=` */
78 | .cm-s-dark .cm-bracket + .cm-tag ~ .cm-attribute + [class='cm-m-xml'] {
79 | color: theme('colors.gray.500');
80 | }
81 |
82 | .cm-s-dark .cm-attribute {
83 | color: theme('colors.gray.300');
84 | }
85 |
86 | .cm-s-dark .cm-property {
87 | color: theme('colors.sky.300');
88 | }
89 |
90 | .cm-s-dark .cm-qualifier {
91 | color: theme('colors.pink.400');
92 | }
93 |
94 | .cm-s-dark .cm-bracket {
95 | color: theme('colors.gray.500');
96 | }
97 |
98 | .cm-s-dark .cm-callee {
99 | color: theme('colors.teal.200');
100 | }
101 |
102 | .cm-s-dark .cm-error {
103 | color: rgba(255, 255, 255, 1);
104 | background-color: #ff5370;
105 | }
106 |
107 | .cm-s-dark .CodeMirror-matchingbracket {
108 | text-decoration: underline;
109 | color: white !important;
110 | }
111 |
112 | .cm-s-dark .cm-tag.cm-m-tailwindcss {
113 | color: theme('colors.gray.50');
114 | }
115 |
116 | .cm-s-dark .cm-variable.cm-m-javascript,
117 | .cm-s-dark .cm-def.cm-m-javascript,
118 | .cm-s-dark .cm-property.cm-m-javascript {
119 | color: inherit;
120 | }
121 |
--------------------------------------------------------------------------------
/src/styles/globals.css:
--------------------------------------------------------------------------------
1 | @import "./tailwind.css";
2 | @import "./codemirror/base.css";
3 | @import "./codemirror/dark.css";
4 |
5 | .Resizer {
6 | background: #000;
7 | opacity: 0.6;
8 | z-index: 1;
9 | -moz-box-sizing: border-box;
10 | -webkit-box-sizing: border-box;
11 | box-sizing: border-box;
12 | -moz-background-clip: padding;
13 | -webkit-background-clip: padding;
14 | background-clip: padding-box;
15 | }
16 |
17 | .Resizer:hover {
18 | -webkit-transition: all 2s ease;
19 | transition: all 2s ease;
20 | }
21 |
22 | .Resizer.horizontal {
23 | height: 11px;
24 | margin: -5px 0;
25 | border-top: 5px solid rgba(255, 255, 255, 0);
26 | border-bottom: 5px solid rgba(255, 255, 255, 0);
27 | cursor: row-resize;
28 | width: 100%;
29 | }
30 |
31 | .Resizer.horizontal:hover {
32 | border-top: 5px solid rgba(0, 0, 0, 0.5);
33 | border-bottom: 5px solid rgba(0, 0, 0, 0.5);
34 | }
35 |
36 | .Resizer.vertical {
37 | width: 11px;
38 | margin: 0 -5px;
39 | border-left: 5px solid rgba(255, 255, 255, 0);
40 | border-right: 5px solid rgba(255, 255, 255, 0);
41 | cursor: col-resize;
42 | }
43 |
44 | .Resizer.vertical:hover {
45 | border-left: 5px solid rgba(0, 0, 0, 0.5);
46 | border-right: 5px solid rgba(0, 0, 0, 0.5);
47 | }
48 | .Resizer.disabled {
49 | cursor: not-allowed;
50 | }
51 | .Resizer.disabled:hover {
52 | border-color: transparent;
53 | }
54 |
--------------------------------------------------------------------------------
/src/styles/tailwind.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | html {
6 | font-size: 16px;
7 | }
8 | body {
9 | background: #1d1e22;
10 | }
11 |
--------------------------------------------------------------------------------
/src/utils/compile.js:
--------------------------------------------------------------------------------
1 | import Sass from "sass.js/dist/sass";
2 | Sass.setWorkerUrl("/vendor/sass.worker.js");
3 |
4 | export function compileScss(code) {
5 | const sass = new Sass();
6 | return new Promise((resolve, reject) => {
7 | sass.compile(code, (result) => {
8 | if (result.status === 0) return resolve(result.text);
9 | reject(new Error(result.formatted));
10 | });
11 | });
12 | }
13 |
--------------------------------------------------------------------------------
/src/utils/database.js:
--------------------------------------------------------------------------------
1 | import fetch from "node-fetch";
2 |
3 | export function get({ id }) {
4 | return fetch(process.env.NEXT_PUBLIC_API_URL + "/api/pen?id=" + id, {
5 | headers: {
6 | Accept: "application/json",
7 | },
8 | }).then((response) => {
9 | return response.json();
10 | });
11 | }
12 |
13 | export function list() {
14 | return fetch(process.env.NEXT_PUBLIC_API_URL + `/api/pen`, {
15 | headers: {
16 | Accept: "application/json",
17 | },
18 | }).then((response) => {
19 | return response.json();
20 | });
21 | }
22 |
--------------------------------------------------------------------------------
/src/utils/theme.js:
--------------------------------------------------------------------------------
1 | export function getTheme() {
2 | return document.querySelector('html').classList.contains('dark')
3 | ? 'dark'
4 | : 'light'
5 | }
6 |
7 | export function onDidChangeTheme(callback) {
8 | const root = document.querySelector('html')
9 |
10 | const observer = new MutationObserver((mutationsList) => {
11 | for (let mutation of mutationsList) {
12 | if (
13 | mutation.type === 'attributes' &&
14 | mutation.attributeName === 'class'
15 | ) {
16 | if (root.classList.contains('dark')) {
17 | callback('dark')
18 | } else {
19 | callback('light')
20 | }
21 | }
22 | }
23 | })
24 |
25 | observer.observe(root, { attributes: true })
26 |
27 | return () => {
28 | observer.disconnect()
29 | }
30 | }
31 |
32 | export function toggleTheme() {
33 | const root = document.querySelector('html')
34 | root.classList.add('disable-transitions')
35 | if (root.classList.contains('dark')) {
36 | root.classList.remove('dark')
37 | try {
38 | window.localStorage.setItem('theme', 'light')
39 | } catch (_) {}
40 | } else {
41 | root.classList.add('dark')
42 | try {
43 | window.localStorage.setItem('theme', 'dark')
44 | } catch (_) {}
45 | }
46 | window.setTimeout(() => {
47 | root.classList.remove('disable-transitions')
48 | }, 0)
49 | }
50 |
--------------------------------------------------------------------------------
/src/utils/workers.js:
--------------------------------------------------------------------------------
1 | import PQueue from 'p-queue'
2 |
3 | export function createWorkerQueue(Worker) {
4 | const worker = new Worker()
5 | const queue = new PQueue({ concurrency: 1 })
6 | return {
7 | worker,
8 | emit(data) {
9 | queue.clear()
10 | const _id = Math.random().toString(36).substr(2, 5)
11 | worker.postMessage({ _current: _id })
12 | return queue.add(
13 | () =>
14 | new Promise((resolve) => {
15 | function onMessage(event) {
16 | if (event.data._id !== _id) return
17 | worker.removeEventListener('message', onMessage)
18 | resolve(event.data)
19 | }
20 | worker.addEventListener('message', onMessage)
21 | worker.postMessage({ ...data, _id })
22 | })
23 | )
24 | },
25 | terminate() {
26 | worker.terminate()
27 | },
28 | }
29 | }
30 |
31 | export function requestResponse(worker, data) {
32 | return new Promise((resolve) => {
33 | const _id = Math.random().toString(36).substr(2, 5)
34 | function onMessage(event) {
35 | if (event.data._id !== _id) return
36 | worker.removeEventListener('message', onMessage)
37 | resolve(event.data)
38 | }
39 | worker.addEventListener('message', onMessage)
40 | worker.postMessage({ ...data, _id })
41 | })
42 | }
43 |
--------------------------------------------------------------------------------
/src/workers/compile.worker.js:
--------------------------------------------------------------------------------
1 | import * as Babel from "@babel/standalone";
2 | import Less from "less/lib/less";
3 | const less = Less();
4 | less.PluginLoader = function () {};
5 |
6 | let current;
7 |
8 | // eslint-disable-next-line no-restricted-globals
9 | addEventListener("message", async (event) => {
10 | current = event.data._id;
11 |
12 | function respond(data) {
13 | setTimeout(() => {
14 | if (event.data._id === current) {
15 | postMessage({ _id: event.data._id, ...data });
16 | } else {
17 | postMessage({ _id: event.data._id, canceled: true });
18 | }
19 | }, 0);
20 | }
21 |
22 | let js;
23 | let css;
24 | let html;
25 |
26 | if (event.data.js) {
27 | if (event.data.jsLang === "javascript") {
28 | js = event.data.js;
29 | }
30 | if (event.data.jsLang === "babel") {
31 | const res = Babel.transform(event.data.js, {
32 | presets: ["react"],
33 | });
34 | js = res.code;
35 | }
36 | if (event.data.jsLang === "typescript") {
37 | const res = Babel.transform(event.data.js, {
38 | filename: "index.ts",
39 | presets: ["typescript"],
40 | });
41 | js = res.code;
42 | }
43 | }
44 | if (event.data.css) {
45 | if (event.data.cssLang === "css") {
46 | css = event.data.css;
47 | }
48 | if (event.data.cssLang === "less") {
49 | css = await less.render(event.data.css).then((res) => res.css);
50 | }
51 | }
52 | if (event.data.html) {
53 | html = event.data.html;
54 | }
55 |
56 | respond({
57 | css,
58 | html,
59 | js,
60 | });
61 | });
62 |
--------------------------------------------------------------------------------
/src/workers/prettier.worker.js:
--------------------------------------------------------------------------------
1 | import prettier from "prettier/standalone";
2 |
3 | const options = {
4 | html: async () => ({
5 | parser: "html",
6 | plugins: [await import("prettier/parser-html")],
7 | printWidth: 100,
8 | }),
9 | typescript: async () => ({
10 | parser: "typescript",
11 | plugins: [await import("prettier/parser-typescript")],
12 | printWidth: 100,
13 | }),
14 | css: async () => ({
15 | parser: "css",
16 | plugins: [await import("prettier/parser-postcss")],
17 | printWidth: 100,
18 | }),
19 | less: async () => ({
20 | parser: "less",
21 | plugins: [await import("prettier/parser-postcss")],
22 | printWidth: 100,
23 | }),
24 | scss: async () => ({
25 | parser: "scss",
26 | plugins: [await import("prettier/parser-postcss")],
27 | printWidth: 100,
28 | }),
29 | javascript: async () => ({
30 | parser: "babel",
31 | plugins: [await import("prettier/parser-babel")],
32 | printWidth: 100,
33 | semi: false,
34 | singleQuote: true,
35 | }),
36 | };
37 |
38 | let current;
39 |
40 | addEventListener("message", async (event) => {
41 | if (event.data._current) {
42 | current = event.data._current;
43 | return;
44 | }
45 |
46 | function respond(data) {
47 | setTimeout(() => {
48 | if (event.data._id === current) {
49 | postMessage({ _id: event.data._id, ...data });
50 | } else {
51 | postMessage({ _id: event.data._id, canceled: true });
52 | }
53 | }, 0);
54 | }
55 |
56 | const opts = await options[event.data.language]();
57 |
58 | try {
59 | respond({
60 | pretty: prettier.format(event.data.text, opts),
61 | });
62 | } catch (error) {
63 | respond({ error });
64 | }
65 | });
66 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | content: [
3 | "./src/**/*.{js,ts,jsx,tsx}"
4 | ],
5 | theme: {
6 | extend: {},
7 | },
8 | plugins: [],
9 | }
10 |
--------------------------------------------------------------------------------