Pārlūkot izejas kodu

Fix type errors & optimize UX

jerryliao 4 dienas atpakaļ
vecāks
revīzija
7d00d29101
11 mainītis faili ar 301 papildinājumiem un 275 dzēšanām
  1. 1 0
      assets/global.css
  2. 2 1
      deno.json
  3. 5 0
      deno.lock
  4. 7 9
      islands/Editor.tsx
  5. 163 150
      islands/LoginFrame.tsx
  6. 1 3
      islands/Modal.tsx
  7. 5 4
      islands/TopBar.tsx
  8. 17 17
      routes/login.tsx
  9. 17 17
      routes/register.tsx
  10. 12 3
      static/markdown.css
  11. 71 71
      utils/server.ts

+ 1 - 0
assets/global.css

@@ -228,6 +228,7 @@ button {
 .pd-login-frame .pd-login-btn {
   background-color: #0d6efd;
   margin-top: 8px;
+  margin-bottom: 8px;
   height: 38px;
   color: #fff;
 }

+ 2 - 1
deno.json

@@ -24,6 +24,7 @@
     "@std/dotenv": "jsr:@std/dotenv@^0.225.6",
     "@std/encoding": "jsr:@std/encoding@^1.0.10",
     "@std/http": "jsr:@std/http@^1.0.25",
+    "@types/showdown": "npm:@types/showdown@^2.0.6",
     "fresh": "jsr:@fresh/core@^2.2.2",
     "preact": "npm:preact@^10.27.2",
     "@preact/signals": "npm:@preact/signals@^2.5.0",
@@ -62,4 +63,4 @@
       "vite/client"
     ]
   }
-}
+}

+ 5 - 0
deno.lock

@@ -43,6 +43,7 @@
     "npm:@preact/signals@^2.5.0": "2.9.0_preact@10.29.0",
     "npm:@preact/signals@^2.5.1": "2.9.0_preact@10.29.0",
     "npm:@prefresh/vite@^2.4.8": "2.4.12_preact@10.29.0_vite@7.3.1",
+    "npm:@types/showdown@^2.0.6": "2.0.6",
     "npm:esbuild-wasm@~0.25.11": "0.25.12",
     "npm:esbuild@0.25.7": "0.25.7",
     "npm:esbuild@~0.25.5": "0.25.12",
@@ -970,6 +971,9 @@
     "@types/estree@1.0.8": {
       "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="
     },
+    "@types/showdown@2.0.6": {
+      "integrity": "sha512-pTvD/0CIeqe4x23+YJWlX2gArHa8G0J0Oh6GKaVXV7TAeickpkkZiNOgFcFcmLQ5lB/K0qBJL1FtRYltBfbGCQ=="
+    },
     "baseline-browser-mapping@2.10.12": {
       "integrity": "sha512-qyq26DxfY4awP2gIRXhhLWfwzwI+N5Nxk6iQi8EFizIaWIjqicQTE4sLnZZVdeKPRcVNoJOkkpfzoIYuvCKaIQ==",
       "bin": true
@@ -1292,6 +1296,7 @@
       "jsr:@std/encoding@^1.0.10",
       "jsr:@std/http@^1.0.25",
       "npm:@preact/signals@^2.5.0",
+      "npm:@types/showdown@^2.0.6",
       "npm:preact@^10.27.2",
       "npm:showdown@^2.1.0",
       "npm:usid@2",

+ 7 - 9
islands/Editor.tsx

@@ -1,4 +1,3 @@
-import { render } from "preact";
 import { useEffect, useRef, useState } from "preact/hooks";
 import showdown, { Converter } from "showdown";
 import { asset } from "fresh/runtime";
@@ -114,10 +113,7 @@ export default function Editor(props: EditorProps) {
 
         renderStyleToShadow();
       }
-      render(
-        <div dangerouslySetInnerHTML={{ __html: convertedContent }} />,
-        shadowRoot,
-      );
+      shadowRoot.innerHTML = convertedContent;
     }
   };
 
@@ -130,12 +126,13 @@ export default function Editor(props: EditorProps) {
   };
 
   // Event listener
-  const modeChangeListener = (e: CustomEvent) => {
+  const modeChangeListener = (e: Event) => {
+    const { detail } = e as CustomEvent;
     if (
-      e.detail &&
-      (props.allowMode === e.detail || props.allowMode === EditorMode.Both)
+      detail &&
+      (props.allowMode === detail || props.allowMode === EditorMode.Both)
     ) {
-      setMode(e.detail);
+      setMode(detail);
     }
   };
 
@@ -216,6 +213,7 @@ export default function Editor(props: EditorProps) {
         ? (
           <div className="pd-edit-view" ref={editViewRef}>
             <textarea
+              spellcheck={false}
               placeholder="Some Markdown here"
               onScroll={() => {
                 onScroll(EditorMode.Edit);

+ 163 - 150
islands/LoginFrame.tsx

@@ -1,150 +1,163 @@
-import { useEffect, useState } from "preact/hooks";
-import { hideLoading, showLoading } from "utils/ui.ts";
-
-interface LoginFrameProps {
-  mode: "login" | "register";
-}
-
-export default function LoginFrame(props: LoginFrameProps) {
-  const [email, setEmail] = useState("");
-  const [password, setPassword] = useState("");
-  const [confirmPassword, setConfirmPassword] = useState("");
-  const [emailError, setEmailError] = useState(false);
-  const [passwordError, setPasswordError] = useState(false);
-  const [confirmPasswordError, setConfirmPasswordError] = useState(false);
-
-  const checkUserLogin = async () => {
-    const resp = await fetch("/api/user/login");
-    const respJson = await resp.json();
-    if (respJson.success) {
-      // Redirect to main page if valid
-      location.href = "/";
-      return true;
-    }
-    return false;
-  };
-
-  const doUserLogin = async () => {
-    const resp = await fetch("/api/user/login", {
-      method: "POST",
-      headers: { "Content-Type": "application/json" },
-      body: JSON.stringify({
-        email,
-        password,
-      }),
-    });
-    const respJson = await resp.json();
-    if (respJson.success) {
-      location.href = "/";
-      return true;
-    }
-    return false;
-  };
-
-  const doUserRegister = async () => {
-    const resp = await fetch("/api/user/register", {
-      method: "POST",
-      headers: { "Content-Type": "application/json" },
-      body: JSON.stringify({
-        email,
-        password,
-      }),
-    });
-    const respJson = await resp.json();
-    if (respJson.success) {
-      location.href = "/login";
-      return true;
-    }
-    return false;
-  };
-
-  useEffect(() => {
-    props.mode === "login" && checkUserLogin();
-  }, []);
-
-  const onSubmit = async () => {
-    showLoading();
-
-    // Do request
-    if (email && password && props.mode === "login") {
-      await doUserLogin();
-    } else if (
-      email &&
-      password &&
-      confirmPassword &&
-      props.mode === "register"
-    ) {
-      if (password !== confirmPassword) {
-        setConfirmPasswordError(true);
-      } else {
-        await doUserRegister();
-      }
-    }
-
-    // Set error
-    if (!email) {
-      setEmailError(true);
-    }
-    if (!password) {
-      setPasswordError(true);
-    }
-    if (!confirmPassword && props.mode === "register") {
-      setConfirmPasswordError(true);
-    }
-    hideLoading();
-  };
-
-  return (
-    <div className="pd-login-frame">
-      <span className="pd-login-input-label">Email</span>
-      <input
-        className={`pd-login-input${emailError ? " error" : ""}`}
-        type="text"
-        placeholder="Your email"
-        value={email}
-        onInput={(e) => {
-          setEmailError(false);
-          setEmail((e.target as HTMLInputElement).value);
-        }}
-      />
-      <span className="pd-login-input-label">Password</span>
-      <input
-        className={`pd-login-input${passwordError ? " error" : ""}`}
-        type="password"
-        placeholder="Your password"
-        value={password}
-        onInput={(e) => {
-          setPasswordError(false);
-          setPassword((e.target as HTMLInputElement).value);
-        }}
-        onKeyDown={(e) => {
-          if (e.key === "Enter") {
-            onSubmit();
-          }
-        }}
-      />
-      {props.mode === "register"
-        ? (
-          <>
-            <span className="pd-login-input-label">Confirm Password</span>
-            <input
-              className={`pd-login-input${
-                confirmPasswordError ? " error" : ""
-              }`}
-              type="password"
-              placeholder="Confirm your password"
-              value={confirmPassword}
-              onInput={(e) => {
-                setConfirmPasswordError(false);
-                setConfirmPassword((e.target as HTMLInputElement).value);
-              }}
-            />
-          </>
-        )
-        : null}
-      <button className="pd-login-btn" type="button" onClick={onSubmit}>
-        {props.mode === "register" ? "Register" : "Sign in"}
-      </button>
-    </div>
-  );
-}
+import { useEffect, useState } from "preact/hooks";
+import { hideLoading, showLoading } from "utils/ui.ts";
+
+interface LoginFrameProps {
+  mode: "login" | "register";
+}
+
+export default function LoginFrame(props: LoginFrameProps) {
+  const [email, setEmail] = useState("");
+  const [password, setPassword] = useState("");
+  const [confirmPassword, setConfirmPassword] = useState("");
+  const [emailError, setEmailError] = useState(false);
+  const [passwordError, setPasswordError] = useState(false);
+  const [confirmPasswordError, setConfirmPasswordError] = useState(false);
+
+  const checkUserLogin = async () => {
+    const resp = await fetch("/api/user/login");
+    const respJson = await resp.json();
+    if (respJson.success) {
+      // Redirect to main page if valid
+      location.href = "/";
+      return true;
+    }
+    return false;
+  };
+
+  const doUserLogin = async () => {
+    const resp = await fetch("/api/user/login", {
+      method: "POST",
+      headers: { "Content-Type": "application/json" },
+      body: JSON.stringify({
+        email,
+        password,
+      }),
+    });
+    const respJson = await resp.json();
+    if (respJson.success) {
+      location.href = "/";
+      return true;
+    }
+    return false;
+  };
+
+  const doUserRegister = async () => {
+    const resp = await fetch("/api/user/register", {
+      method: "POST",
+      headers: { "Content-Type": "application/json" },
+      body: JSON.stringify({
+        email,
+        password,
+      }),
+    });
+    const respJson = await resp.json();
+    if (respJson.success) {
+      location.href = "/login";
+      return true;
+    }
+    return false;
+  };
+
+  useEffect(() => {
+    props.mode === "login" && checkUserLogin();
+  }, []);
+
+  const onSubmit = async () => {
+    showLoading();
+
+    // Do request
+    if (email && password && props.mode === "login") {
+      await doUserLogin();
+    } else if (
+      email &&
+      password &&
+      confirmPassword &&
+      props.mode === "register"
+    ) {
+      if (password !== confirmPassword) {
+        setConfirmPasswordError(true);
+      } else {
+        await doUserRegister();
+      }
+    }
+
+    // Set error
+    if (!email) {
+      setEmailError(true);
+    }
+    if (!password) {
+      setPasswordError(true);
+    }
+    if (!confirmPassword && props.mode === "register") {
+      setConfirmPasswordError(true);
+    }
+    hideLoading();
+  };
+
+  return (
+    <div className="pd-login-frame">
+      <span className="pd-login-input-label">Email</span>
+      <input
+        className={`pd-login-input${emailError ? " error" : ""}`}
+        type="text"
+        placeholder="Your email"
+        value={email}
+        onInput={(e) => {
+          setEmailError(false);
+          setEmail((e.target as HTMLInputElement).value);
+        }}
+      />
+      <span className="pd-login-input-label">Password</span>
+      <input
+        className={`pd-login-input${passwordError ? " error" : ""}`}
+        type="password"
+        placeholder="Your password"
+        value={password}
+        onInput={(e) => {
+          setPasswordError(false);
+          setPassword((e.target as HTMLInputElement).value);
+        }}
+        onKeyDown={(e) => {
+          if (e.key === "Enter") {
+            onSubmit();
+          }
+        }}
+      />
+      {props.mode === "register"
+        ? (
+          <>
+            <span className="pd-login-input-label">Confirm Password</span>
+            <input
+              className={`pd-login-input${
+                confirmPasswordError ? " error" : ""
+              }`}
+              type="password"
+              placeholder="Confirm your password"
+              value={confirmPassword}
+              onInput={(e) => {
+                setConfirmPasswordError(false);
+                setConfirmPassword((e.target as HTMLInputElement).value);
+              }}
+              onKeyDown={(e) => {
+                if (e.key === "Enter") {
+                  onSubmit();
+                }
+              }}
+            />
+          </>
+        )
+        : null}
+      <button className="pd-login-btn" type="button" onClick={onSubmit}>
+        {props.mode === "register" ? "Register" : "Sign in"}
+      </button>
+      <button
+        type="button"
+        onClick={() => {
+          location.href = props.mode === "register" ? "/login" : "/register";
+        }}
+      >
+        {props.mode === "register" ? "Go Login" : "Go Register"}
+      </button>
+    </div>
+  );
+}

+ 1 - 3
islands/Modal.tsx

@@ -16,9 +16,7 @@ interface ModalGlobalHook {
 }
 
 declare global {
-  interface GlobalThis {
-    $modal?: ModalGlobalHook;
-  }
+  var $modal: ModalGlobalHook | undefined;
 }
 
 export default function Modal() {

+ 5 - 4
islands/TopBar.tsx

@@ -120,12 +120,13 @@ export default function TopBar(props: TopBarProps) {
   };
 
   // Event listener
-  const modeChangeListener = (e: CustomEvent) => {
+  const modeChangeListener = (e: Event) => {
+    const { detail } = e as CustomEvent;
     if (
-      e.detail &&
-      (props.allowMode === e.detail || props.allowMode === EditorMode.Both)
+      detail &&
+      (props.allowMode === detail || props.allowMode === EditorMode.Both)
     ) {
-      setMode(e.detail);
+      setMode(detail);
     }
   };
 

+ 17 - 17
routes/login.tsx

@@ -1,17 +1,17 @@
-import { Head } from "fresh/runtime";
-import { define } from "utils/state.ts";
-import LoginFrame from "../islands/LoginFrame.tsx";
-
-export default define.page(() => {
-  return (
-    <>
-      <Head>
-        <title>Login</title>
-      </Head>
-      <div className="pd-page pd-page-centered">
-        <h2>Sign in to Postdown</h2>
-        <LoginFrame mode="login" />
-      </div>
-    </>
-  );
-});
+import { Head } from "fresh/runtime";
+import { define } from "utils/state.ts";
+import LoginFrame from "../islands/LoginFrame.tsx";
+
+export default define.page(() => {
+  return (
+    <>
+      <Head>
+        <title>Login</title>
+      </Head>
+      <div className="pd-page pd-page-centered">
+        <h2>Sign in to Postdown</h2>
+        <LoginFrame mode="login" />
+      </div>
+    </>
+  );
+});

+ 17 - 17
routes/register.tsx

@@ -1,17 +1,17 @@
-import { Head } from "fresh/runtime";
-import { define } from "utils/state.ts";
-import LoginFrame from "../islands/LoginFrame.tsx";
-
-export default define.page(() => {
-  return (
-    <>
-      <Head>
-        <title>Register</title>
-      </Head>
-      <div className="pd-page pd-page-centered">
-        <h2>Register to Postdown</h2>
-        <LoginFrame mode="register" />
-      </div>
-    </>
-  );
-});
+import { Head } from "fresh/runtime";
+import { define } from "utils/state.ts";
+import LoginFrame from "../islands/LoginFrame.tsx";
+
+export default define.page(() => {
+  return (
+    <>
+      <Head>
+        <title>Register</title>
+      </Head>
+      <div className="pd-page pd-page-centered">
+        <h2>Register to Postdown</h2>
+        <LoginFrame mode="register" />
+      </div>
+    </>
+  );
+});

+ 12 - 3
static/markdown.css

@@ -1,3 +1,12 @@
-pre {background-color: #f5f5f5;padding: 0.5rem 0.25rem;border-radius: 0.25rem;}
-table {border-collapse: collapse;}
-table th, table td {border: 1px solid #d6d6d6; padding: 0.25rem 0.5rem;}
+pre {
+  background-color: #f5f5f5;
+  padding: 0.5rem 0.25rem;
+  border-radius: 0.25rem;
+}
+table {
+  border-collapse: collapse;
+}
+table th, table td {
+  border: 1px solid #d6d6d6;
+  padding: 0.25rem 0.5rem;
+}

+ 71 - 71
utils/server.ts

@@ -1,71 +1,71 @@
-import { encodeHex } from "@std/encoding";
-import { crypto, DigestAlgorithm } from "@std/crypto";
-import { deleteCookie, getCookies, setCookie } from "@std/http";
-import { find } from "utils/db.ts";
-
-export async function getCryptoString(rawString: string, cryptoMethod: string) {
-  const buffer = await crypto.subtle.digest(
-    cryptoMethod as DigestAlgorithm,
-    new TextEncoder().encode(rawString),
-  );
-  return encodeHex(buffer);
-}
-
-export function checkToken(req: Request) {
-  const cookies = getCookies(req.headers);
-  if (cookies) {
-    const token = find(
-      "Token",
-      {
-        token: cookies["pd-user-token"] || "",
-      },
-      ["user_id"],
-    );
-    if (token.length > 0) {
-      return token[0]["user_id"] as string;
-    }
-  }
-  return false;
-}
-
-export function setToken(res: Response, token: string) {
-  setCookie(res.headers, {
-    name: "pd-user-token",
-    value: token,
-    path: "/",
-  });
-}
-
-export function clearToken(res: Response) {
-  deleteCookie(res.headers, "pd-user-token", { path: "/" });
-}
-
-export function makeSuccessResponse(
-  data:
-    | Record<string, unknown>
-    | Record<string, unknown>[]
-    | string
-    | number
-    | boolean,
-) {
-  return new Response(
-    JSON.stringify({
-      success: true,
-      data: data,
-    }),
-    {
-      headers: { "Content-Type": "application/json" },
-    },
-  );
-}
-
-export function makeErrorResponse() {
-  return new Response(
-    JSON.stringify({
-      success: false,
-    }),
-    {
-      headers: { "Content-Type": "application/json" },
-    },
-  );
-}
+import { encodeHex } from "@std/encoding";
+import { crypto, DigestAlgorithm } from "@std/crypto";
+import { deleteCookie, getCookies, setCookie } from "@std/http";
+import { find } from "utils/db.ts";
+
+export async function getCryptoString(rawString: string, cryptoMethod: string) {
+  const buffer = await crypto.subtle.digest(
+    cryptoMethod as DigestAlgorithm,
+    new TextEncoder().encode(rawString),
+  );
+  return encodeHex(buffer);
+}
+
+export function checkToken(req: Request) {
+  const cookies = getCookies(req.headers);
+  if (cookies) {
+    const token = find(
+      "Token",
+      {
+        token: cookies["pd-user-token"] || "",
+      },
+      ["user_id"],
+    );
+    if (token.length > 0) {
+      return token[0]["user_id"] as string;
+    }
+  }
+  return false;
+}
+
+export function setToken(res: Response, token: string) {
+  setCookie(res.headers, {
+    name: "pd-user-token",
+    value: token,
+    path: "/",
+  });
+}
+
+export function clearToken(res: Response) {
+  deleteCookie(res.headers, "pd-user-token", { path: "/" });
+}
+
+export function makeSuccessResponse(
+  data:
+    | Record<string, unknown>
+    | Record<string, unknown>[]
+    | string
+    | number
+    | boolean,
+) {
+  return new Response(
+    JSON.stringify({
+      success: true,
+      data: data,
+    }),
+    {
+      headers: { "Content-Type": "application/json" },
+    },
+  );
+}
+
+export function makeErrorResponse() {
+  return new Response(
+    JSON.stringify({
+      success: false,
+    }),
+    {
+      headers: { "Content-Type": "application/json" },
+    },
+  );
+}