Sfoglia il codice sorgente

Initial layout & basic functions

jerryliao 1 anno fa
parent
commit
ed0834e40f
12 ha cambiato i file con 338 aggiunte e 0 eliminazioni
  1. 1 0
      README.md
  2. 6 0
      deno.json
  3. 5 0
      dev.ts
  4. 24 0
      fresh.gen.ts
  5. 9 0
      import_map.json
  6. 78 0
      islands/Editor.tsx
  7. 69 0
      islands/TopBar.tsx
  8. 9 0
      main.ts
  9. 16 0
      routes/_app.tsx
  10. 16 0
      routes/index.tsx
  11. BIN
      static/favicon.ico
  12. 105 0
      static/global.css

+ 1 - 0
README.md

@@ -1,2 +1,3 @@
 # postdown
+
 A web-based, shareable, self-hosted Markdown editor built with deno

+ 6 - 0
deno.json

@@ -0,0 +1,6 @@
+{
+  "tasks": {
+    "start": "deno run -A --watch=static/,routes/ dev.ts"
+  },
+  "importMap": "./import_map.json"
+}

+ 5 - 0
dev.ts

@@ -0,0 +1,5 @@
+#!/usr/bin/env -S deno run -A --watch=static/,routes/
+
+import dev from "$fresh/dev.ts";
+
+await dev(import.meta.url, "./main.ts");

+ 24 - 0
fresh.gen.ts

@@ -0,0 +1,24 @@
+// DO NOT EDIT. This file is generated by fresh.
+// This file SHOULD be checked into source version control.
+// This file is automatically updated during development when running `dev.ts`.
+
+import * as $0 from "./routes/[name].tsx";
+import * as $1 from "./routes/_app.tsx";
+import * as $2 from "./routes/index.tsx";
+import * as $$0 from "./islands/Editor.tsx";
+import * as $$1 from "./islands/TopBar.tsx";
+
+const manifest = {
+  routes: {
+    "./routes/[name].tsx": $0,
+    "./routes/_app.tsx": $1,
+    "./routes/index.tsx": $2,
+  },
+  islands: {
+    "./islands/Editor.tsx": $$0,
+    "./islands/TopBar.tsx": $$1,
+  },
+  baseUrl: import.meta.url,
+};
+
+export default manifest;

+ 9 - 0
import_map.json

@@ -0,0 +1,9 @@
+{
+  "imports": {
+    "$fresh/": "https://deno.land/x/fresh@1.0.1/",
+    "preact": "https://esm.sh/preact@10.8.2",
+    "preact/": "https://esm.sh/preact@10.8.2/",
+    "preact-render-to-string": "https://esm.sh/preact-render-to-string@5.2.0?deps=preact@10.8.2",
+    "showdown": "https://esm.sh/showdown@2.1.0"
+  }
+}

+ 78 - 0
islands/Editor.tsx

@@ -0,0 +1,78 @@
+/** @jsx h */
+import { h } from "preact";
+import { useEffect, useState } from "preact/hooks";
+import showdown, { Converter } from "showdown";
+
+interface EditorProps {
+  content: string;
+  allowMode: "edit" | "read" | "both";
+}
+
+let converter: Converter | null = null;
+export default function Editor(props: EditorProps) {
+  const [mode, setMode] = useState(props.allowMode);
+  const [displayContent, setDisplayContent] = useState("");
+  const [convertedContent, setConvertedContent] = useState("");
+
+  // Event listener
+  const modeChangeListener = (e: CustomEvent) => {
+    if (e.detail && props.allowMode === "both") {
+      setMode(e.detail);
+    }
+  };
+
+  // Init event listeners
+  useEffect(() => {
+    window.addEventListener("ModeChange", modeChangeListener);
+
+    return () => {
+      window.removeEventListener("ModeChange", modeChangeListener);
+    };
+  }, []);
+
+  // Init conversion
+  useEffect(() => {
+    if (props.content) {
+      convertText(props.content);
+    }
+  }, [props.content]);
+
+  const convertText = (text: string) => {
+    // Init converter
+    if (!converter) {
+      converter = new showdown.Converter();
+    }
+
+    // Save display text
+    setDisplayContent(text);
+
+    // Convert text and save
+    setConvertedContent(converter.makeHtml(text));
+  };
+
+  return (
+    <div className={`pd-editor pd-mode-${mode}`}>
+      {mode !== "read"
+        ? (
+          <div className="pd-edit-view">
+            <textarea
+              placeholder="Some Markdown here"
+              onInput={(e) => {
+                convertText((e.target as HTMLInputElement).value);
+              }}
+              value={displayContent}
+            />
+          </div>
+        )
+        : null}
+      {mode !== "edit"
+        ? (
+          <div
+            className="pd-read-view"
+            dangerouslySetInnerHTML={{ __html: convertedContent }}
+          />
+        )
+        : null}
+    </div>
+  );
+}

+ 69 - 0
islands/TopBar.tsx

@@ -0,0 +1,69 @@
+/** @jsx h */
+import { h } from "preact";
+import { useEffect, useState } from "preact/hooks";
+
+interface TopBarProps {
+  allowMode: "edit" | "read" | "both";
+}
+
+export default function TopBar(props: TopBarProps) {
+  const [mode, setMode] = useState(props.allowMode);
+
+  // Event listener
+  const modeChangeListener = (e: CustomEvent) => {
+    if (e.detail && props.allowMode === "both") {
+      setMode(e.detail);
+    }
+  };
+
+  // Event dispatche
+  const modeChangeDispatcher = (mode: string) => {
+    window.dispatchEvent(new CustomEvent("ModeChange", { detail: mode }));
+  };
+
+  // Init event listeners
+  useEffect(() => {
+    window.addEventListener("ModeChange", modeChangeListener);
+
+    return () => {
+      window.removeEventListener("ModeChange", modeChangeListener);
+    };
+  }, []);
+
+  return (
+    <div className="pd-top-bar">
+      <div className="pd-top-bar-mode-switcher">
+        <button
+          className={`pd-top-bar-btn ${mode === "edit" ? "active" : ""}`}
+          id="edit"
+          type="button"
+          onClick={() => {
+            modeChangeDispatcher("edit");
+          }}
+        >
+          Edit
+        </button>
+        <button
+          className={`pd-top-bar-btn ${mode === "read" ? "active" : ""}`}
+          id="read"
+          type="button"
+          onClick={() => {
+            modeChangeDispatcher("read");
+          }}
+        >
+          Read
+        </button>
+        <button
+          className={`pd-top-bar-btn ${mode === "both" ? "active" : ""}`}
+          id="both"
+          type="button"
+          onClick={() => {
+            modeChangeDispatcher("both");
+          }}
+        >
+          Both
+        </button>
+      </div>
+    </div>
+  );
+}

+ 9 - 0
main.ts

@@ -0,0 +1,9 @@
+/// <reference no-default-lib="true" />
+/// <reference lib="dom" />
+/// <reference lib="dom.asynciterable" />
+/// <reference lib="deno.ns" />
+/// <reference lib="deno.unstable" />
+
+import { start } from "$fresh/server.ts";
+import manifest from "./fresh.gen.ts";
+await start(manifest);

+ 16 - 0
routes/_app.tsx

@@ -0,0 +1,16 @@
+/** @jsx h */
+/** @jsxFrag Fragment */
+import { Fragment, h } from "preact";
+import { asset, Head } from "$fresh/runtime.ts";
+import { AppProps } from "$fresh/server.ts";
+
+export default function App(props: AppProps) {
+  return (
+    <>
+      <Head>
+        <link href={asset("/global.css")} rel="stylesheet" />
+      </Head>
+      <props.Component />
+    </>
+  );
+}

+ 16 - 0
routes/index.tsx

@@ -0,0 +1,16 @@
+/** @jsx h */
+import { h } from "preact";
+import { useState } from "preact/hooks";
+import TopBar from "../islands/TopBar.tsx";
+import Editor from "../islands/Editor.tsx";
+
+export default function Home() {
+  const [content, setContent] = useState("##Title");
+
+  return (
+    <div className="pd-page">
+      <TopBar allowMode="both" />
+      <Editor content={content} allowMode="both" />
+    </div>
+  );
+}

BIN
static/favicon.ico


+ 105 - 0
static/global.css

@@ -0,0 +1,105 @@
+* {
+  margin: 0;
+  padding: 0;
+}
+
+.pd-page {
+  width: 100vw;
+  height: 100vh;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+}
+
+/* TopBar styles start */
+.pd-top-bar {
+  width: 100vw;
+  padding: 0.75rem 0.75rem 0 0.75rem;
+  display: flex;
+  justify-content: space-between;
+  box-sizing: border-box;
+  flex-shrink: 0;
+}
+
+.pd-top-bar .pd-top-bar-mode-switcher {
+  border: 1px solid #ced4da;
+  border-radius: 0.375rem;
+  box-sizing: border-box;
+}
+
+.pd-top-bar .pd-top-bar-mode-switcher .pd-top-bar-btn {
+  box-sizing: border-box;
+  padding: 6px 12px;
+  background-color: #fff;
+  color: #212529;
+  border: none;
+  cursor: pointer;
+}
+
+.pd-top-bar .pd-top-bar-mode-switcher .pd-top-bar-btn.active {
+  background-color: #0d6efd;
+  color: #fff;
+}
+
+.pd-top-bar .pd-top-bar-mode-switcher .pd-top-bar-btn:first-child {
+  border-top-left-radius: 0.375rem;
+  border-bottom-left-radius: 0.375rem;
+}
+
+.pd-top-bar .pd-top-bar-mode-switcher .pd-top-bar-btn:last-child {
+  border-top-right-radius: 0.375rem;
+  border-bottom-right-radius: 0.375rem;
+}
+
+.pd-top-bar .pd-top-bar-mode-switcher .pd-top-bar-btn:nth-child(2) {
+  padding: 6px 10px;
+  border-left: 1px solid #ced4da;
+  border-right: 1px solid #ced4da;
+}
+/* TopBar styles end */
+
+/* Editor styles start */
+.pd-editor {
+  width: 100vw;
+  padding: 0.75rem;
+  display: flex;
+  justify-content: space-between;
+  box-sizing: border-box;
+  overflow: hidden;
+  flex-shrink: 0;
+  flex-grow: 1;
+}
+
+.pd-editor .pd-edit-view,
+.pd-editor .pd-read-view {
+  height: 100%;
+  border: 1px solid #ced4da;
+  border-radius: 0.375rem;
+  box-sizing: border-box;
+  padding: 0.75rem;
+  color: #212529;
+  overflow: auto;
+  flex-shrink: 0;
+  flex-basis: 0;
+  flex-grow: 1;
+}
+
+.pd-editor .pd-edit-view textarea {
+  width: 100%;
+  min-height: 100%;
+  display: block;
+  box-sizing: border-box;
+  border-radius: 0.375rem;
+  border: none;
+  resize: none;
+  outline: none;
+}
+
+.pd-editor.pd-mode-both .pd-edit-view {
+  margin-right: 0.375rem;
+}
+
+.pd-editor.pd-mode-both .pd-read-view {
+  margin-left: 0.375rem;
+}
+/* Editor styles end */