Просмотр исходного кода

Optimizations for icons, buttons and the PostList by OpenCode

jerryliao 1 день назад
Родитель
Сommit
91aa818e27
7 измененных файлов с 144 добавлено и 40 удалено
  1. 3 1
      components/form/Button.tsx
  2. 33 23
      islands/PostList.tsx
  3. 2 2
      routes/[id].tsx
  4. 9 1
      routes/index.tsx
  5. 2 2
      routes/user/[name].tsx
  6. 14 0
      tests/ui/button_test.tsx
  7. 81 11
      tests/ui/post_list_test.tsx

+ 3 - 1
components/form/Button.tsx

@@ -1,7 +1,7 @@
 import { JSX } from "preact";
 import { JSX } from "preact";
 
 
 interface ButtonProps extends JSX.ButtonHTMLAttributes<HTMLButtonElement> {
 interface ButtonProps extends JSX.ButtonHTMLAttributes<HTMLButtonElement> {
-  variant?: "default" | "primary" | "danger";
+  variant?: "default" | "primary" | "danger" | "danger-outline";
   size?: "sm" | "md" | "lg";
   size?: "sm" | "md" | "lg";
   children: JSX.Element | string;
   children: JSX.Element | string;
 }
 }
@@ -20,6 +20,8 @@ export default function Button({
     default: "bg-white dark:bg-gray-700 text-gray-800 dark:text-gray-100",
     default: "bg-white dark:bg-gray-700 text-gray-800 dark:text-gray-100",
     primary: "bg-blue-600 text-white border-blue-600",
     primary: "bg-blue-600 text-white border-blue-600",
     danger: "bg-red-600 text-white border-red-600",
     danger: "bg-red-600 text-white border-red-600",
+    "danger-outline":
+      "bg-transparent text-red-600 border-red-600 dark:text-red-400 dark:border-red-400",
   };
   };
 
 
   const sizeClasses = {
   const sizeClasses = {

+ 33 - 23
islands/PostList.tsx

@@ -45,39 +45,49 @@ export default function PostList(props: PostListProps) {
   };
   };
 
 
   return (
   return (
-    <div className="w-full grid grid-cols-4 gap-4 mt-4 pb-4 overflow-auto">
+    <div className="w-full grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 mt-4 pb-4 overflow-auto">
       {props.posts.map((post) => (
       {props.posts.map((post) => (
         <div
         <div
-          className="w-full min-w-[180px] border border-gray-200 dark:border-gray-700 dark:bg-gray-800 rounded box-border relative p-4 text-base flex flex-col"
+          className="w-full border border-gray-200 dark:border-gray-700 dark:bg-gray-800 rounded box-border relative p-4 text-base flex flex-col"
           key={post.id}
           key={post.id}
         >
         >
-          <span className="font-medium mb-2">
-            {post.hasPassword && <i className="bi bi-lock-fill text-sm mr-1" />}
-            <a href={`/${post.id}`}>{post.title || "Untitled"}</a>
-          </span>
+          <div className="flex items-center justify-between mb-2">
+            <span className="font-medium line-clamp-1">
+              <a href={`/${post.id}`}>{post.title || "Untitled"}</a>
+            </span>
+            <div className="flex items-center gap-1 shrink-0">
+              {!props.readOnly && post.shared && (
+                <i className="bi bi-share-fill text-gray-500 dark:text-gray-400 text-sm" />
+              )}
+              {post.hasPassword && (
+                <i className="bi bi-lock-fill text-gray-500 dark:text-gray-400 text-sm" />
+              )}
+            </div>
+          </div>
+          <p className="mb-2 line-clamp-2">
+            {post.content || "No content"}
+          </p>
           {!props.readOnly && (
           {!props.readOnly && (
-            <div className="absolute right-4 top-3 text-2xl z-[1]">
-              <i
-                className="bi bi-x cursor-pointer"
+            <div className="flex gap-2 mt-auto">
+              <Button
+                type="button"
+                onClick={() => {
+                  onEdit(post.id);
+                }}
+              >
+                Edit
+              </Button>
+              <Button
+                type="button"
+                variant="danger-outline"
                 onClick={() => {
                 onClick={() => {
                   onDelete(post.id, post.title);
                   onDelete(post.id, post.title);
                 }}
                 }}
-              />
+              >
+                Delete
+              </Button>
             </div>
             </div>
           )}
           )}
-          <span className="overflow-hidden text-ellipsis whitespace-nowrap mb-2">
-            {post.content || "No content"}
-          </span>
-          {!props.readOnly && (
-            <Button
-              type="button"
-              onClick={() => {
-                onEdit(post.id);
-              }}
-            >
-              Edit
-            </Button>
-          )}
         </div>
         </div>
       ))}
       ))}
     </div>
     </div>

+ 2 - 2
routes/[id].tsx

@@ -74,7 +74,7 @@ export default function Post(props: PageProps<PostProps>) {
     return (
     return (
       <>
       <>
         <Head>
         <Head>
-          <title>{props.data.title}</title>
+          <title>{props.data.title || "Untitled"}</title>
         </Head>
         </Head>
         <PageContainer>
         <PageContainer>
           <SharePasswordFrame id={props.data.id} title={props.data.title} />
           <SharePasswordFrame id={props.data.id} title={props.data.title} />
@@ -86,7 +86,7 @@ export default function Post(props: PageProps<PostProps>) {
   return (
   return (
     <>
     <>
       <Head>
       <Head>
-        <title>{props.data.title}</title>
+        <title>{props.data.title || "Untitled"}</title>
       </Head>
       </Head>
       <PageContainer>
       <PageContainer>
         <TopBar
         <TopBar

+ 9 - 1
routes/index.tsx

@@ -10,7 +10,13 @@ import PageContainer from "../components/layout/PageContainer.tsx";
 
 
 interface HomeProps {
 interface HomeProps {
   name: string;
   name: string;
-  list: { id: string; title: string; content: string; shared: boolean }[];
+  list: {
+    id: string;
+    title: string;
+    content: string;
+    shared: boolean;
+    hasPassword: boolean;
+  }[];
 }
 }
 
 
 export const handler = define.handlers({
 export const handler = define.handlers({
@@ -28,6 +34,7 @@ export const handler = define.handlers({
           "title",
           "title",
           "content",
           "content",
           "shared",
           "shared",
+          "share_password",
         ]);
         ]);
         return page({
         return page({
           name: user[0]["name"] as string,
           name: user[0]["name"] as string,
@@ -36,6 +43,7 @@ export const handler = define.handlers({
             title: post["title"] as string,
             title: post["title"] as string,
             content: post["content"] as string,
             content: post["content"] as string,
             shared: post["shared"] === 1,
             shared: post["shared"] === 1,
+            hasPassword: Boolean(post["share_password"]),
           })),
           })),
         });
         });
       }
       }

+ 2 - 2
routes/user/[name].tsx

@@ -77,13 +77,13 @@ export default define.page((props: PageProps<UserPageProps>) => {
   return (
   return (
     <>
     <>
       <Head>
       <Head>
-        <title>{userName}'s Shared Posts</title>
+        <title>{userName}'s Posts</title>
       </Head>
       </Head>
       <PageContainer>
       <PageContainer>
         <div className="absolute top-3 right-3">
         <div className="absolute top-3 right-3">
           <ThemeToggle />
           <ThemeToggle />
         </div>
         </div>
-        <h1 className="text-2xl font-bold mt-2">{userName}'s Shared Posts</h1>
+        <h1 className="text-2xl font-bold mt-2">{userName}'s Posts</h1>
         <PostList posts={posts} readOnly />
         <PostList posts={posts} readOnly />
       </PageContainer>
       </PageContainer>
     </>
     </>

+ 14 - 0
tests/ui/button_test.tsx

@@ -53,6 +53,20 @@ Deno.test({
   sanitizeOps: false,
   sanitizeOps: false,
 });
 });
 
 
+Deno.test({
+  name: "Button - applies danger-outline variant classes",
+  fn() {
+    render(<Button variant="danger-outline">Danger Outline</Button>);
+    const btn = screen.getByText("Danger Outline");
+    assertEquals(btn.className.includes("bg-transparent"), true);
+    assertEquals(btn.className.includes("text-red-600"), true);
+    assertEquals(btn.className.includes("border-red-600"), true);
+    cleanup();
+  },
+  sanitizeResources: false,
+  sanitizeOps: false,
+});
+
 Deno.test({
 Deno.test({
   name: "Button - applies sm size classes",
   name: "Button - applies sm size classes",
   fn() {
   fn() {

+ 81 - 11
tests/ui/post_list_test.tsx

@@ -2,9 +2,31 @@ import { assertEquals, cleanup, fireEvent, render, screen } from "./setup.ts";
 import PostList from "../../islands/PostList.tsx";
 import PostList from "../../islands/PostList.tsx";
 
 
 const mockPosts = [
 const mockPosts = [
-  { id: "1", title: "First Post", content: "Hello world", shared: false },
-  { id: "2", title: "Second Post", content: "Another post", shared: true },
-  { id: "3", title: "", content: "", shared: false },
+  {
+    id: "1",
+    title: "First Post",
+    content: "Hello world",
+    shared: false,
+    hasPassword: false,
+  },
+  {
+    id: "2",
+    title: "Second Post",
+    content: "Another post",
+    shared: true,
+    hasPassword: false,
+  },
+  { id: "3", title: "", content: "", shared: false, hasPassword: false },
+];
+
+const mockPostWithPassword = [
+  {
+    id: "4",
+    title: "Protected Post",
+    content: "Secret",
+    shared: true,
+    hasPassword: true,
+  },
 ];
 ];
 
 
 Deno.test({
 Deno.test({
@@ -78,11 +100,11 @@ Deno.test({
 });
 });
 
 
 Deno.test({
 Deno.test({
-  name: "PostList - each card has a delete icon",
+  name: "PostList - each card has a Delete button",
   fn() {
   fn() {
-    const { container } = render(<PostList posts={mockPosts} />);
-    const deleteIcons = container.querySelectorAll(".bi-x");
-    assertEquals(deleteIcons.length, 3);
+    render(<PostList posts={mockPosts} />);
+    const deleteButtons = screen.getAllByText("Delete");
+    assertEquals(deleteButtons.length, 3);
     cleanup();
     cleanup();
   },
   },
   sanitizeResources: false,
   sanitizeResources: false,
@@ -90,7 +112,7 @@ Deno.test({
 });
 });
 
 
 Deno.test({
 Deno.test({
-  name: "PostList - delete icon triggers $modal.show",
+  name: "PostList - Delete button triggers $modal.show",
   fn() {
   fn() {
     let modalTitle = "";
     let modalTitle = "";
     let modalContent = "";
     let modalContent = "";
@@ -102,9 +124,9 @@ Deno.test({
       hide: () => {},
       hide: () => {},
     };
     };
 
 
-    const { container } = render(<PostList posts={[mockPosts[0]]} />);
-    const deleteIcon = container.querySelector(".bi-x")!;
-    fireEvent.click(deleteIcon);
+    render(<PostList posts={[mockPosts[0]]} />);
+    const deleteButton = screen.getByText("Delete");
+    fireEvent.click(deleteButton);
 
 
     assertEquals(modalTitle, "Confirm delete");
     assertEquals(modalTitle, "Confirm delete");
     assertEquals(modalContent.includes("First Post"), true);
     assertEquals(modalContent.includes("First Post"), true);
@@ -116,6 +138,54 @@ Deno.test({
   sanitizeOps: false,
   sanitizeOps: false,
 });
 });
 
 
+Deno.test({
+  name: "PostList - shows shared icon for shared posts",
+  fn() {
+    const { container } = render(<PostList posts={mockPosts} />);
+    const sharedIcons = container.querySelectorAll(".bi-share-fill");
+    assertEquals(sharedIcons.length, 1);
+    cleanup();
+  },
+  sanitizeResources: false,
+  sanitizeOps: false,
+});
+
+Deno.test({
+  name: "PostList - shows lock icon for password-protected posts",
+  fn() {
+    const { container } = render(<PostList posts={mockPostWithPassword} />);
+    const lockIcons = container.querySelectorAll(".bi-lock-fill");
+    assertEquals(lockIcons.length, 1);
+    cleanup();
+  },
+  sanitizeResources: false,
+  sanitizeOps: false,
+});
+
+Deno.test({
+  name: "PostList - hides shared icon in readOnly mode",
+  fn() {
+    const { container } = render(<PostList posts={mockPosts} readOnly />);
+    const sharedIcons = container.querySelectorAll(".bi-share-fill");
+    assertEquals(sharedIcons.length, 0);
+    cleanup();
+  },
+  sanitizeResources: false,
+  sanitizeOps: false,
+});
+
+Deno.test({
+  name: "PostList - hides Edit and Delete buttons in readOnly mode",
+  fn() {
+    render(<PostList posts={mockPosts} readOnly />);
+    assertEquals(screen.queryByText("Edit"), null);
+    assertEquals(screen.queryByText("Delete"), null);
+    cleanup();
+  },
+  sanitizeResources: false,
+  sanitizeOps: false,
+});
+
 Deno.test({
 Deno.test({
   name: "PostList - renders empty grid with no posts",
   name: "PostList - renders empty grid with no posts",
   fn() {
   fn() {