Kaynağa Gözat

Create new post from local file by OpenCode

jerryliao 1 gün önce
ebeveyn
işleme
29a1f8b4d1
3 değiştirilmiş dosya ile 168 ekleme ve 4 silme
  1. 26 0
      components/form/FileInput.tsx
  2. 45 4
      islands/HomeBar.tsx
  3. 97 0
      tests/ui/file_input_test.tsx

+ 26 - 0
components/form/FileInput.tsx

@@ -0,0 +1,26 @@
+import { JSX } from "preact";
+
+interface FileInputProps
+  extends Omit<JSX.InputHTMLAttributes<HTMLInputElement>, "type" | "value"> {
+  label?: string;
+}
+
+export default function FileInput({
+  label,
+  className = "",
+  ...props
+}: FileInputProps) {
+  const inputClasses =
+    `w-full block box-border rounded border border-gray-300 dark:border-gray-600 text-sm outline-none h-[38px] leading-[30px] py-1 px-1.5 dark:bg-gray-800 dark:text-gray-100 file:mr-2 file:py-1 file:px-3 file:rounded file:border-0 file:text-sm file:font-medium file:cursor-pointer file:bg-gray-100 file:text-gray-700 file:hover:bg-gray-200 file:dark:bg-gray-600 file:dark:text-gray-100 file:dark:hover:bg-gray-500 ${className}`;
+
+  if (label) {
+    return (
+      <div className="mb-2">
+        <label className="block mb-1 text-sm">{label}</label>
+        <input type="file" className={inputClasses} {...props} />
+      </div>
+    );
+  }
+
+  return <input type="file" className={inputClasses} {...props} />;
+}

+ 45 - 4
islands/HomeBar.tsx

@@ -1,5 +1,6 @@
 import Input from "../components/form/Input.tsx";
 import Button from "../components/form/Button.tsx";
+import FileInput from "../components/form/FileInput.tsx";
 import ThemeToggle from "./ThemeToggle.tsx";
 
 interface HomeBarProps {
@@ -7,16 +8,17 @@ interface HomeBarProps {
 }
 
 const settingsData: { [key: string]: string } = {};
+const newPostData: { [key: string]: string } = {};
 
 export default function HomeBar(props: HomeBarProps) {
-  const doNewPost = async () => {
+  const doCreatePost = async () => {
     globalThis.$loading?.show();
     const resp = await fetch("/api/post", {
       method: "POST",
       headers: { "Content-Type": "application/json" },
       body: JSON.stringify({
-        title: "",
-        content: "",
+        title: newPostData["title"] || "",
+        content: newPostData["content"] || "",
       }),
     });
     const respJson = await resp.json();
@@ -27,6 +29,45 @@ export default function HomeBar(props: HomeBarProps) {
     return false;
   };
 
+  const showNewPost = () => {
+    newPostData["title"] = "";
+    newPostData["content"] = "";
+    globalThis.$modal?.show(
+      "New Post",
+      <div>
+        <FileInput
+          label="Import file (optional)"
+          accept=".md,.txt"
+          onInput={(e) => {
+            const file = (e.target as HTMLInputElement).files?.[0];
+            if (!file) {
+              newPostData["title"] = "";
+              newPostData["content"] = "";
+              return;
+            }
+            newPostData["title"] = file.name.replace(/\.(md|txt)$/, "");
+            const reader = new FileReader();
+            reader.onload = () => {
+              newPostData["content"] = reader.result as string;
+            };
+            reader.readAsText(file);
+          }}
+        />
+      </div>,
+      [
+        {
+          text: "Create",
+          onClick: async () => {
+            await doCreatePost();
+          },
+        },
+        {
+          text: "Cancel",
+        },
+      ],
+    );
+  };
+
   const doLogout = async () => {
     const resp = await fetch("/api/user/logout");
     const respJson = await resp.json();
@@ -104,7 +145,7 @@ export default function HomeBar(props: HomeBarProps) {
     <div className="flex items-center justify-between">
       <Button
         type="button"
-        onClick={doNewPost}
+        onClick={showNewPost}
       >
         New Post
       </Button>

+ 97 - 0
tests/ui/file_input_test.tsx

@@ -0,0 +1,97 @@
+import { assertEquals, cleanup, render } from "./setup.ts";
+import FileInput from "../../components/form/FileInput.tsx";
+
+Deno.test({
+  name: "FileInput - renders bare input without label",
+  fn() {
+    const { container } = render(<FileInput />);
+    const fileInput = container.querySelector(
+      'input[type="file"]',
+    ) as HTMLInputElement;
+    assertEquals(fileInput !== null, true);
+    assertEquals(fileInput.type, "file");
+    const labelEl = container.querySelector("label");
+    assertEquals(labelEl, null);
+    cleanup();
+  },
+  sanitizeResources: false,
+  sanitizeOps: false,
+});
+
+Deno.test({
+  name: "FileInput - renders with label wrapper",
+  fn() {
+    const { container } = render(<FileInput label="Import file" />);
+    const labelEl = container.querySelector("label");
+    assertEquals(labelEl !== null, true);
+    assertEquals(labelEl?.textContent?.includes("Import file"), true);
+    const fileInput = container.querySelector(
+      'input[type="file"]',
+    ) as HTMLInputElement;
+    assertEquals(fileInput !== null, true);
+    cleanup();
+  },
+  sanitizeResources: false,
+  sanitizeOps: false,
+});
+
+Deno.test({
+  name: "FileInput - passes through accept attribute",
+  fn() {
+    const { container } = render(<FileInput accept=".md,.txt" />);
+    const fileInput = container.querySelector(
+      'input[type="file"]',
+    ) as HTMLInputElement;
+    assertEquals(fileInput.getAttribute("accept"), ".md,.txt");
+    cleanup();
+  },
+  sanitizeResources: false,
+  sanitizeOps: false,
+});
+
+Deno.test({
+  name: "FileInput - passes through disabled attribute",
+  fn() {
+    const { container } = render(<FileInput disabled />);
+    const fileInput = container.querySelector(
+      'input[type="file"]',
+    ) as HTMLInputElement;
+    assertEquals(fileInput.hasAttribute("disabled"), true);
+    cleanup();
+  },
+  sanitizeResources: false,
+  sanitizeOps: false,
+});
+
+Deno.test({
+  name: "FileInput - appends custom className",
+  fn() {
+    const { container } = render(<FileInput className="my-file" />);
+    const fileInput = container.querySelector(
+      'input[type="file"]',
+    ) as HTMLInputElement;
+    assertEquals(fileInput.classList.contains("my-file"), true);
+    cleanup();
+  },
+  sanitizeResources: false,
+  sanitizeOps: false,
+});
+
+Deno.test({
+  name: "FileInput - includes dark mode file button styles",
+  fn() {
+    const { container } = render(<FileInput />);
+    const fileInput = container.querySelector(
+      'input[type="file"]',
+    ) as HTMLInputElement;
+    assertEquals(fileInput.classList.contains("file:dark:bg-gray-600"), true);
+    assertEquals(
+      fileInput.classList.contains("file:dark:hover:bg-gray-500"),
+      true,
+    );
+    assertEquals(fileInput.classList.contains("file:dark:text-gray-100"), true);
+    cleanup();
+  },
+  sanitizeResources: false,
+  sanitizeOps: false,
+});