From ee1e4b44d7ec43b53f3ccdb42c58f30b128431e4 Mon Sep 17 00:00:00 2001 From: Johnson Mao Date: Thu, 3 Oct 2024 21:32:29 +0800 Subject: [PATCH 1/7] refactor: tabs component --- components/shared/Tabs/Tab.test.tsx | 18 ++-- components/shared/Tabs/Tab.tsx | 29 +++---- components/shared/Tabs/Tabs.stories.tsx | 44 ++-------- components/shared/Tabs/Tabs.test.tsx | 27 ++---- components/shared/Tabs/Tabs.tsx | 105 ++++++------------------ pages/rooms.tsx | 16 ++-- 6 files changed, 60 insertions(+), 179 deletions(-) diff --git a/components/shared/Tabs/Tab.test.tsx b/components/shared/Tabs/Tab.test.tsx index 62519a41..df0ec53a 100644 --- a/components/shared/Tabs/Tab.test.tsx +++ b/components/shared/Tabs/Tab.test.tsx @@ -11,16 +11,9 @@ describe("Tab", () => { expect(screen.getByRole("tab")).toHaveTextContent(label); }); - it("should renders tab button with the correct className", () => { - const className = "test-class test-class2"; - render(); - - expect(screen.getByRole("tab")).toHaveClass(className); - }); - it("should call the onClick handler when the tab is clicked", () => { const handleClickTab = jest.fn(); - render(); + render(); fireEvent.click(screen.getByRole("tab")); @@ -28,14 +21,17 @@ describe("Tab", () => { }); it('should apply the active class when the "active" prop is true', () => { - render(); + render(); - expect(screen.getByRole("tab")).toHaveClass("is-active"); + expect(screen.getByRole("tab")).toHaveAttribute("aria-selected", "true"); }); it('should not apply the active class when the "active" prop is falsy', () => { render(); - expect(screen.getByRole("tab")).not.toHaveClass("is-active"); + expect(screen.getByRole("tab")).not.toHaveAttribute( + "aria-selected", + "true" + ); }); }); diff --git a/components/shared/Tabs/Tab.tsx b/components/shared/Tabs/Tab.tsx index 25e4e705..9b9177ab 100644 --- a/components/shared/Tabs/Tab.tsx +++ b/components/shared/Tabs/Tab.tsx @@ -1,6 +1,7 @@ import { cn } from "@/lib/utils"; import { Key } from "react"; -interface TabProps { + +export interface TabProps { /** * The key of tab */ @@ -13,33 +14,27 @@ interface TabProps { * Whether the tab is active. * Controlled by tabs. */ - active?: boolean; - /** - * The className of tab - */ - className?: string; + isActive?: boolean; /** * Callback executed when tab is clicked */ - onTabClick?: (tabKey: T) => void; + onClick?: (tabKey: T) => void; } export default function Tab(props: TabProps) { - const { tabKey, label, className, active, onTabClick } = props; - - const tabBaseClass = ` w-fit py-3 text-center text-base text-white relative after:content-'' after:w-full after:h-[4px] after:block after:absolute after:bottom-0 after:left-0 after:transition-colors hover:after:bg-[#2F88FF]`; - - const tabActiveClass = active && "is-active after:bg-[#2F88FF]"; - - const tabClass = cn(tabBaseClass, tabActiveClass, className); + const { tabKey, label, isActive, onClick } = props; return ( diff --git a/components/shared/Tabs/Tabs.stories.tsx b/components/shared/Tabs/Tabs.stories.tsx index 241bd203..16537d02 100644 --- a/components/shared/Tabs/Tabs.stories.tsx +++ b/components/shared/Tabs/Tabs.stories.tsx @@ -13,47 +13,15 @@ type Story = StoryObj>; export const Playground: Story = { args: { tabs: [ - { key: 0, label: "熱門遊戲" }, - { key: 1, label: "最新遊戲" }, - { key: 2, label: "好評遊戲" }, + { tabKey: 0, label: "熱門遊戲" }, + { tabKey: 1, label: "最新遊戲" }, + { tabKey: 2, label: "好評遊戲" }, ], defaultActiveKey: 2, - size: "default", renderTabPaneContent: (tabItem) => ( -
This is {tabItem.label} TabPane.
+
+ This is {tabItem.label} TabPane. +
), - children:
This is children prop.
, }, }; - -export const Size: Story = { - args: { - tabs: [ - { key: 0, label: "公開聊天" }, - { key: 1, label: "遊戲隊聊" }, - { key: 2, label: "好友聊天" }, - ], - defaultActiveKey: 0, - size: "default", - }, - render: () => ( -
- - -
- ), -}; diff --git a/components/shared/Tabs/Tabs.test.tsx b/components/shared/Tabs/Tabs.test.tsx index 7b1b469c..bc787403 100644 --- a/components/shared/Tabs/Tabs.test.tsx +++ b/components/shared/Tabs/Tabs.test.tsx @@ -4,13 +4,13 @@ import Tabs, { TabsProps, TabItemType } from "./Tabs"; describe("Tabs", () => { const tabs: TabsProps["tabs"] = [ - { key: "1", label: "Tab 1" }, - { key: "2", label: "Tab 2" }, - { key: "3", label: "Tab 3" }, + { tabKey: "1", label: "Tab 1" }, + { tabKey: "2", label: "Tab 2" }, + { tabKey: "3", label: "Tab 3" }, ]; const tabPaneText = "Tab Pane Text"; const renderTabPaneContent = (tabItem: TabItemType) => ( -
{`${tabPaneText} ${tabItem.key}`}
+
{`${tabPaneText} ${tabItem.tabKey}`}
); it("should renders all Tabs and the first TabPane when not given 'defaultActiveKey' prop", () => { @@ -33,7 +33,7 @@ describe("Tabs", () => { ); @@ -52,21 +52,4 @@ describe("Tabs", () => { expect(screen.queryByText(`${tabPaneText} 1`)).not.toBeInTheDocument(); expect(screen.queryByText(`${tabPaneText} 2`)).not.toBeInTheDocument(); }); - - it("should call the onChange handler when the active tab changed", () => { - const handleTabChanged = jest.fn(); - render( - - ); - - const tab1 = screen.getByRole("tab", { name: "Tab 1" }); - fireEvent.click(tab1); - - expect(handleTabChanged).toHaveBeenCalled(); - }); }); diff --git a/components/shared/Tabs/Tabs.tsx b/components/shared/Tabs/Tabs.tsx index 3371f0fd..127638ae 100644 --- a/components/shared/Tabs/Tabs.tsx +++ b/components/shared/Tabs/Tabs.tsx @@ -1,12 +1,7 @@ import { useState, Key, ReactNode } from "react"; -import Tab from "./Tab"; -import { cn } from "@/lib/utils"; -export interface TabItemType { - key: T; - label?: string; -} +import Tab, { TabProps } from "./Tab"; -type TabSizeType = "default" | "large"; +export type TabItemType = TabProps; export interface TabsProps { /** @@ -17,95 +12,45 @@ export interface TabsProps { * Initial active tab's key */ defaultActiveKey?: T; - /** - * The className of tabs that wraps tabBar and tabPane - */ - tabsClass?: string; - /** - * The className of tabBar that wraps the tab - */ - tabBarClass?: string; - /** - * The className of tab - */ - tabClass?: string; - /** - * The className of tabPane - */ - tabPaneClass?: string; - /** - * The size of tab - */ - size?: TabSizeType; - /** - * Custom content of tabPane - */ - children?: ReactNode | undefined; /** * Function that recieved activeTabItem and render content of tabPane */ renderTabPaneContent?: (tabItem: TabItemType) => ReactNode; - /** - * Callback executed when tab is clicked - */ - onChange?: (tabKey: T) => void; } -export default function Tabs(props: TabsProps) { - const { - tabs, - defaultActiveKey, - tabsClass, - tabBarClass: customTabBarClass, - tabClass: customTabClass, - tabPaneClass, - size = "default", - children, - renderTabPaneContent, - onChange, - } = props; - const [activeKey, setActiveKey] = useState(defaultActiveKey || tabs[0].key); - const activeTabItem = tabs.find((tab) => tab.key === activeKey)!; - const isLageSize = size === "large"; - - const tabsWrapperClass = cn("flex flex-col", tabsClass); - - const tabBarClass = cn( - "flex bg-dark29", - isLageSize ? "gap-[40px]" : "gap-[14px] px-3", - customTabBarClass +export default function Tabs({ + tabs, + defaultActiveKey, + renderTabPaneContent, +}: TabsProps) { + const [activeKey, setActiveKey] = useState( + defaultActiveKey || tabs[0]?.tabKey ); + const activeTabItem = tabs.find((tab) => tab.tabKey === activeKey); - const tabClass = cn(isLageSize ? "px-3" : "px-0", customTabClass); - - function handleChangeActiveTab(nextTabkey: T) { - if (nextTabkey === activeKey) return; - setActiveKey(nextTabkey); - onChange && onChange(nextTabkey); - } + const handleChangeActiveTab = (nextTabKey: T) => { + if (nextTabKey === activeKey) return; + setActiveKey(nextTabKey); + }; return ( -
-
+ <> +
{tabs.map((tab) => ( ))}
-
- {renderTabPaneContent && renderTabPaneContent(activeTabItem)} - {children} +
+ {activeTabItem && + renderTabPaneContent && + renderTabPaneContent(activeTabItem)}
-
+ ); } diff --git a/pages/rooms.tsx b/pages/rooms.tsx index a8da5afe..8265f751 100644 --- a/pages/rooms.tsx +++ b/pages/rooms.tsx @@ -1,25 +1,20 @@ import { RoomType } from "@/requests/rooms"; import RoomsListView from "@/containers/room/RoomListView"; -import Tabs from "@/components/shared/Tabs"; +import Tabs, { TabItemType } from "@/components/shared/Tabs"; import { serverSideTranslations } from "next-i18next/serverSideTranslations"; import { useTranslation } from "next-i18next"; import { GetStaticProps } from "next"; -type TabsProps = { - key: RoomType; - label: string; -}[]; - const Rooms = () => { const { t } = useTranslation("rooms"); - const tabs: TabsProps = [ + const tabs: TabItemType[] = [ { - key: RoomType.WAITING, + tabKey: RoomType.WAITING, label: t("rooms_waiting"), }, { - key: RoomType.PLAYING, + tabKey: RoomType.PLAYING, label: t("rooms_playing"), }, ]; @@ -29,9 +24,8 @@ const Rooms = () => { ( - + )} />
From 26bfd3f05f95f2319a50d3892321432ebdbed839 Mon Sep 17 00:00:00 2001 From: Johnson Mao Date: Thu, 3 Oct 2024 21:43:10 +0800 Subject: [PATCH 2/7] style: home page --- components/lobby/FastJoinButton.tsx | 2 +- pages/_document.tsx | 2 +- pages/index.tsx | 42 ++++++++++++++++++++++++----- 3 files changed, 37 insertions(+), 9 deletions(-) diff --git a/components/lobby/FastJoinButton.tsx b/components/lobby/FastJoinButton.tsx index 0341e8cd..8ebbfa27 100644 --- a/components/lobby/FastJoinButton.tsx +++ b/components/lobby/FastJoinButton.tsx @@ -74,7 +74,7 @@ const FastJoinButton: FC = () => {