瀏覽代碼

Add mock login, get posts

jerryliao 1 年之前
父節點
當前提交
310af5f5f2
共有 12 個文件被更改,包括 365 次插入31 次删除
  1. 12 4
      fresh.gen.ts
  2. 1 0
      import_map.json
  3. 16 5
      islands/Editor.tsx
  4. 74 0
      islands/LoginFrame.tsx
  5. 21 0
      islands/TopBar.tsx
  6. 4 0
      routes/_app.tsx
  7. 21 0
      routes/api/post.tsx
  8. 29 0
      routes/api/user/login.tsx
  9. 1 4
      routes/index.tsx
  10. 12 0
      routes/login.tsx
  11. 100 18
      static/global.css
  12. 74 0
      utils.ts

+ 12 - 4
fresh.gen.ts

@@ -3,18 +3,26 @@
 // This file is automatically updated during development when running `dev.ts`.
 
 import * as $0 from "./routes/_app.tsx";
-import * as $1 from "./routes/index.tsx";
+import * as $1 from "./routes/api/post.tsx";
+import * as $2 from "./routes/api/user/login.tsx";
+import * as $3 from "./routes/index.tsx";
+import * as $4 from "./routes/login.tsx";
 import * as $$0 from "./islands/Editor.tsx";
-import * as $$1 from "./islands/TopBar.tsx";
+import * as $$1 from "./islands/LoginFrame.tsx";
+import * as $$2 from "./islands/TopBar.tsx";
 
 const manifest = {
   routes: {
     "./routes/_app.tsx": $0,
-    "./routes/index.tsx": $1,
+    "./routes/api/post.tsx": $1,
+    "./routes/api/user/login.tsx": $2,
+    "./routes/index.tsx": $3,
+    "./routes/login.tsx": $4,
   },
   islands: {
     "./islands/Editor.tsx": $$0,
-    "./islands/TopBar.tsx": $$1,
+    "./islands/LoginFrame.tsx": $$1,
+    "./islands/TopBar.tsx": $$2,
   },
   baseUrl: import.meta.url,
 };

+ 1 - 0
import_map.json

@@ -1,6 +1,7 @@
 {
   "imports": {
     "$fresh/": "https://deno.land/x/fresh@1.0.1/",
+    "$http/": "https://deno.land/std@0.152.0/http/",
     "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",

+ 16 - 5
islands/Editor.tsx

@@ -2,9 +2,10 @@
 import { h, render } from "preact";
 import { useEffect, useState, useRef } from "preact/hooks";
 import showdown, { Converter } from "showdown";
+import { showLoading, hideLoading } from "../utils.ts";
 
 interface EditorProps {
-  content: string;
+  id: string;
   allowMode: "edit" | "read" | "both";
 }
 
@@ -97,6 +98,7 @@ export default function Editor(props: EditorProps) {
 
   // Init event listeners
   useEffect(() => {
+    showLoading();
     addEventListener("ModeChange", modeChangeListener);
 
     return () => {
@@ -121,10 +123,19 @@ export default function Editor(props: EditorProps) {
 
   // Init conversion
   useEffect(() => {
-    if (props.content) {
-      convertText(props.content);
-    }
-  }, [props.content]);
+    const loadPost = async () => {
+      if (props.id) {
+        const resp = await fetch("/api/post");
+        const respJson = await resp.json();
+        if (respJson.success) {
+          setDisplayContent(respJson.data);
+          convertText(respJson.data);
+          hideLoading();
+        }
+      }
+    };
+    loadPost();
+  }, [props.id]);
 
   const convertText = (text: string) => {
     // Init converter

+ 74 - 0
islands/LoginFrame.tsx

@@ -0,0 +1,74 @@
+/** @jsx h */
+import { h } from "preact";
+import { useState, useEffect } from "preact/hooks";
+
+export default function LoginFrame() {
+  const [email, setEmail] = useState("");
+  const [password, setPassword] = useState("");
+
+  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;
+  };
+
+  useEffect(() => {
+    checkUserLogin();
+  }, []);
+
+  const onSubmit = () => {
+    if (email && password) {
+      doUserLogin();
+    }
+  };
+
+  return (
+    <div className="pd-login-frame">
+      <span className="pd-login-input-label">Email</span>
+      <input
+        className="pd-login-input"
+        type="text"
+        placeholder="Your email"
+        value={email}
+        onInput={(e) => {
+          setEmail((e.target as HTMLInputElement).value);
+        }}
+      />
+      <span className="pd-login-input-label">Password</span>
+      <input
+        className="pd-login-input"
+        type="password"
+        placeholder="Your password"
+        value={password}
+        onInput={(e) => {
+          setPassword((e.target as HTMLInputElement).value);
+        }}
+      />
+      <button className="pd-login-btn" type="button" onClick={onSubmit}>
+        Sign in
+      </button>
+    </div>
+  );
+}

+ 21 - 0
islands/TopBar.tsx

@@ -8,6 +8,7 @@ interface TopBarProps {
 
 export default function TopBar(props: TopBarProps) {
   const [mode, setMode] = useState(props.allowMode);
+  const [isLogin, setIsLogin] = useState(false);
 
   // Event listener
   const modeChangeListener = (e: CustomEvent) => {
@@ -24,8 +25,21 @@ export default function TopBar(props: TopBarProps) {
     dispatchEvent(new CustomEvent("ModeChange", { detail: mode }));
   };
 
+  const checkUserLogin = async () => {
+    const resp = await fetch("/api/user/login");
+    const respJson = await resp.json();
+    if (respJson.success) {
+      setIsLogin(true);
+      return true;
+    }
+    // Redirect to login page if not valid
+    location.href = "/login";
+    return false;
+  };
+
   // Init event listeners
   useEffect(() => {
+    checkUserLogin();
     addEventListener("ModeChange", modeChangeListener);
 
     return () => {
@@ -73,6 +87,13 @@ export default function TopBar(props: TopBarProps) {
           Both
         </button>
       </div>
+      {isLogin ? (
+        <div className="pd-top-bar-tool-icons">
+          <i className="bi bi-house-door" />
+          <i className="bi bi-share" />
+          <i className="bi bi-gear" />
+        </div>
+      ) : null}
     </div>
   );
 }

+ 4 - 0
routes/_app.tsx

@@ -9,6 +9,10 @@ export default function App(props: AppProps) {
     <>
       <Head>
         <link href={asset("/global.css")} rel="stylesheet" />
+        <link
+          rel="stylesheet"
+          href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.9.1/font/bootstrap-icons.css"
+        />
       </Head>
       <props.Component />
     </>

+ 21 - 0
routes/api/post.tsx

@@ -0,0 +1,21 @@
+import { Handlers } from "$fresh/server.ts";
+import {
+  checkToken,
+  makeErrorResponse,
+  makeSuccessResponse,
+} from "../../utils.ts";
+
+export const handler: Handlers = {
+  async GET(req: Request) {
+    // Mock post content
+    if (checkToken(req)) {
+      const resp = await fetch(
+        "https://raw.githubusercontent.com/denoland/deno/main/README.md"
+      );
+      if (resp.status === 200) {
+        return makeSuccessResponse(await resp.text());
+      }
+    }
+    return makeErrorResponse();
+  },
+};

+ 29 - 0
routes/api/user/login.tsx

@@ -0,0 +1,29 @@
+import { Handlers } from "$fresh/server.ts";
+import {
+  checkToken,
+  makeErrorResponse,
+  makeSuccessResponse,
+  setToken,
+} from "../../../utils.ts";
+
+export const handler: Handlers = {
+  GET(req: Request) {
+    // Mock a default user
+    if (checkToken(req)) {
+      return makeSuccessResponse({
+        name: "Jerry Liao",
+        email: "jerryliao26@gmail.com",
+      });
+    }
+    return makeErrorResponse();
+  },
+  async POST(req: Request) {
+    const reqJson = await req.json();
+    if (reqJson.email && reqJson.password) {
+      const successResponse = makeSuccessResponse(true);
+      setToken(successResponse);
+      return successResponse;
+    }
+    return makeErrorResponse();
+  },
+};

+ 1 - 4
routes/index.tsx

@@ -1,16 +1,13 @@
 /** @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" />
+      <Editor id="id" allowMode="both" />
     </div>
   );
 }

+ 12 - 0
routes/login.tsx

@@ -0,0 +1,12 @@
+/** @jsx h */
+import { h } from "preact";
+import LoginFrame from "../islands/LoginFrame.tsx";
+
+export default function Login() {
+  return (
+    <div className="pd-page pd-page-centered">
+      <h2>Sign in to Postdown</h2>
+      <LoginFrame />
+    </div>
+  );
+}

+ 100 - 18
static/global.css

@@ -13,9 +13,79 @@
   overflow: hidden;
 }
 
+.pd-page.pd-page-centered {
+  display: flex;
+  align-items: center;
+}
+
+/* Global form styles start */
+input,
+textarea {
+  width: 100%;
+  display: block;
+  box-sizing: border-box;
+  border-radius: 0.375rem;
+  border: 1px solid #ced4da;
+  font-size: 1rem;
+  outline: none;
+}
+
+input {
+  height: 38px;
+  line-height: 30px;
+  padding: 4px 0.375rem;
+}
+
+textarea {
+  height: 100%;
+  padding: 0.375rem;
+  resize: none;
+}
+
+button {
+  box-sizing: border-box;
+  padding: 6px 12px;
+  background-color: #fff;
+  line-height: 16px;
+  color: #212529;
+  cursor: pointer;
+  font-size: 1rem;
+  border: 1px solid #ced4da;
+  border-radius: 0.375rem;
+}
+/* Global form styles end */
+
+/* Login frame styles start */
+.pd-login-frame {
+  width: 375px;
+  margin-top: 16px;
+  border: 1px solid #ced4da;
+  border-radius: 0.375rem;
+  box-sizing: border-box;
+  padding: 16px;
+  font-size: 1rem;
+  color: #212529;
+  display: flex;
+  flex-direction: column;
+}
+
+.pd-login-frame .pd-login-input {
+  margin-bottom: 8px;
+}
+
+.pd-login-frame .pd-login-input-label {
+  margin-bottom: 4px;
+}
+
+.pd-login-frame .pd-login-btn {
+  margin-top: 8px;
+  height: 38px;
+}
+/* Login frame styles end */
+
 /* TopBar styles start */
 .pd-top-bar {
-  width: 100vw;
+  width: 100%;
   display: flex;
   margin-bottom: 0.75rem;
   justify-content: space-between;
@@ -27,15 +97,11 @@
   border: 1px solid #ced4da;
   border-radius: 0.375rem;
   box-sizing: border-box;
+  font-size: 1rem;
 }
 
 .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 {
@@ -51,19 +117,43 @@
 }
 
 .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;
+  border-top-right-radius: 0;
+  border-bottom-right-radius: 0;
 }
 
 .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;
+  border-top-left-radius: 0;
+  border-bottom-left-radius: 0;
 }
 
 .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;
+  border-radius: 0;
+}
+
+.pd-top-bar .pd-top-bar-tool-icons {
+  height: 28px;
+  line-height: 26px;
+  box-sizing: border-box;
+  padding: 0 8px;
+  border: 1px solid #ced4da;
+  border-radius: 0.375rem;
+}
+
+.pd-top-bar .pd-top-bar-tool-icons i.bi {
+  margin-right: 16px;
+  font-size: 16px;
+  cursor: pointer;
+}
+
+.pd-top-bar .pd-top-bar-tool-icons i.bi:hover {
+  color: #0d6efd;
+}
+
+.pd-top-bar .pd-top-bar-tool-icons i.bi:last-child {
+  margin-right: 0;
 }
 /* TopBar styles end */
 
@@ -94,15 +184,7 @@
 }
 
 .pd-editor .pd-edit-view textarea {
-  width: 100%;
-  height: 100%;
-  display: block;
-  box-sizing: border-box;
-  padding: 0.375rem;
-  border-radius: 0.375rem;
   border: none;
-  resize: none;
-  outline: none;
 }
 
 .pd-editor .pd-edit-view textarea::-webkit-scrollbar,

+ 74 - 0
utils.ts

@@ -0,0 +1,74 @@
+import { setCookie, getCookies, deleteCookie } from "$http/cookie.ts";
+
+export function checkToken(req: Request) {
+  const cookies = getCookies(req.headers);
+  console.log("DIOR::", cookies);
+  if (cookies && cookies["pd-user-token"]) {
+    return true;
+  }
+  return false;
+}
+
+export function setToken(res: Response) {
+  setCookie(res.headers, {
+    name: "pd-user-token",
+    value: "testTEST123!@#",
+    path: "/",
+  });
+}
+
+export function clearToken(res: Response) {
+  deleteCookie(res.headers, "pd-user-token");
+}
+
+export function makeSuccessResponse(
+  data: 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" },
+    }
+  );
+}
+
+export function showLoading() {
+  if (document && document.body) {
+    const coverEle = document.body.querySelector(".pd-cover");
+    if (!coverEle) {
+      const newCoverEle = document.createElement("div");
+      newCoverEle.className = "pd-cover";
+      newCoverEle.style.position = "fixed";
+      newCoverEle.style.top = "0";
+      newCoverEle.style.left = "0";
+      newCoverEle.style.right = "0";
+      newCoverEle.style.bottom = "0";
+      newCoverEle.style.backgroundColor = "rgba(0, 0, 0, 0.8)";
+      newCoverEle.style.zIndex = "9";
+      document.body.appendChild(newCoverEle);
+    }
+  }
+}
+
+export function hideLoading() {
+  if (document && document.body) {
+    const coverEle = document.body.querySelector(".pd-cover");
+    if (coverEle) {
+      document.body.removeChild(coverEle);
+    }
+  }
+}