- {onKickUser && (
-
onKickUser({ id, nickname })}
- data-testid="kick-user-svg"
- className={"absolute top-[5px] right-[6px] cursor-pointer"}
- >
-
-
- )}
+
+ {onKickUser && (
+
onKickUser({ id, nickname })}
+ data-testid="kick-user-svg"
+ className={"absolute top-[5px] right-[6px] cursor-pointer"}
+ >
+
+
+ )}
- {isReady && readyContent}
-
+ {isReady && readyContent}
{nameText}
diff --git a/components/shared/Button/v2/Button.tsx b/components/shared/Button/v2/Button.tsx
index a89731df..92819f68 100644
--- a/components/shared/Button/v2/Button.tsx
+++ b/components/shared/Button/v2/Button.tsx
@@ -17,7 +17,7 @@ export enum ButtonSize {
}
const commonDisabledClasses =
- "disabled:cursor-not-allowed disabled:bg-none disabled:bg-gray-800 disabled:border-gray-500 disabled:text-gray-200 disabled:stroke-gray-200 disabled:fill-gray-200";
+ "disabled:cursor-not-allowed disabled:bg-none disabled:bg-grey-800 disabled:border-grey-500 disabled:text-grey-200 disabled:stroke-grey-200 disabled:fill-grey-200";
const buttonTypeClasses: Record
= {
primary:
@@ -51,7 +51,7 @@ type InnerButtonComponent = (
const iconTypeClasses: Record = {
primary:
"stroke-primary-700 hover:stroke-primary-50 active:stroke-primary-50",
- secondary: "stroke-primary-200 disabled:stroke-gray-500",
+ secondary: "stroke-primary-200 disabled:stroke-grey-500",
highlight: "stroke-primary-50",
};
diff --git a/components/shared/Chat/ChatContent.tsx b/components/shared/Chat/ChatContent.tsx
deleted file mode 100644
index 79ffcd72..00000000
--- a/components/shared/Chat/ChatContent.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-import { PropsWithChildren, useEffect, useRef } from "react";
-import { cn } from "@/lib/utils";
-
-type ChatContentProps = {
- className?: string;
-} & PropsWithChildren;
-
-const ChatContent = ({ className, children }: ChatContentProps) => {
- const chatRef = useRef(null);
-
- useEffect(() => {
- chatRef.current?.scrollTo({ top: chatRef.current.scrollHeight });
- }, [children]);
-
- return (
-
- {children}
-
- );
-};
-
-export default ChatContent;
diff --git a/components/shared/Chat/ChatMessage.tsx b/components/shared/Chat/ChatMessage.tsx
deleted file mode 100644
index 14911c40..00000000
--- a/components/shared/Chat/ChatMessage.tsx
+++ /dev/null
@@ -1,44 +0,0 @@
-import Avatar from "../Avatar";
-import { cn } from "@/lib/utils";
-
-type ChatMessageProps = {
- isMe?: boolean;
- src?: string;
- nickname?: string;
- message?: string;
-};
-
-const ChatMessage: React.FC = ({
- isMe,
- src = "/images/profile.jpg",
- nickname,
- message,
-}) => {
- return (
-
-
{}}
- src={{
- blurDataURL: src,
- height: 34,
- src,
- width: 34,
- }}
- type="button"
- />
-
-
- {nickname}
-
-
{message}
-
-
- );
-};
-
-export default ChatMessage;
diff --git a/components/shared/Chat/index.tsx b/components/shared/Chat/index.tsx
deleted file mode 100644
index 83f3697e..00000000
--- a/components/shared/Chat/index.tsx
+++ /dev/null
@@ -1,95 +0,0 @@
-import { useState } from "react";
-import Tabs from "../Tabs";
-import Button from "../Button";
-import ChatMessage from "./ChatMessage";
-import ChatContent from "./ChatContent";
-import Portal from "@/components/shared/Portal";
-
-enum TabsProps {
- PUBLIC = "PUBLIC",
- TEAM = "TEAM",
- FRIENDS = "FRIENDS",
-}
-
-const mock_messages = Array.from({ length: 20 }, (_, i) => ({
- isMe: !(i % 3),
- id: crypto.randomUUID(),
- nickname: !(i % 3) ? "玩家1" : "玩家2",
- message: `聊天內容${i}`,
-}));
-
-const tabs = [
- {
- key: TabsProps.PUBLIC,
- label: "公開聊天",
- },
- {
- key: TabsProps.TEAM,
- label: "遊戲隊聊",
- },
- {
- key: TabsProps.FRIENDS,
- label: "好友聊天",
- },
-];
-
-const Chat = () => {
- const [selectedTab, setSelectedTab] = useState(TabsProps.PUBLIC);
- const [message, setMessage] = useState();
-
- return (
- <>
-
-
-
setSelectedTab(key as string)}
- />
-
- {/* TODO: 尚未串接 API 再根據 API 文件調整 */}
-
- {mock_messages.map((data) => (
-
- ))}
-
-
-
-
setMessage(e.target.innerText)}
- >
- {message}
-
-
-
- {" "}
-
- >
- );
-};
-
-export default Chat;
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..90dc5616 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,43 @@ 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,
+}: Readonly>) {
+ 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?.(activeTabItem)}
-
+ >
);
}
diff --git a/containers/room/RoomListView.tsx b/containers/room/RoomListView.tsx
index c0fb30f9..8549430f 100644
--- a/containers/room/RoomListView.tsx
+++ b/containers/room/RoomListView.tsx
@@ -4,8 +4,6 @@ import { AxiosError } from "axios";
import { useTranslation } from "next-i18next";
import {
- Room,
- RoomEntryError,
RoomType,
getRooms,
postRoomEntry,
@@ -34,26 +32,16 @@ const RoomsListView: FC
= ({ status }) => {
fetch(getRooms({ page, perPage, status })),
defaultPerPage: 20,
});
- const [selectedRoom, setSelectedRoom] = useState();
+ const [roomId, setRoomId] = useState(null);
const [passwordValues, setPasswordValues] = useState(INIT_PASSWORD);
const [isLoading, setIsLoading] = useState(false);
- const router = useRouter();
const { Popup, firePopup } = usePopup();
-
- const onSelectedRoom = (id: string) => {
- const targetRoom = data.find((room) => room.id === id);
-
- if (targetRoom?.currentPlayers === targetRoom?.maxPlayers) {
- firePopup({ title: t("room_is_full") });
- return;
- }
-
- setSelectedRoom(targetRoom);
- };
+ const router = useRouter();
+ const isLocked = data.find((room) => room.id === roomId)?.isLocked;
const handleClose = () => {
setPasswordValues(INIT_PASSWORD);
- setSelectedRoom(undefined);
+ setRoomId(null);
};
const handlePaste = (e: ClipboardEvent) => {
@@ -67,10 +55,6 @@ const RoomsListView: FC = ({ status }) => {
setPasswordValues(Array.from(pastePassword, (text) => text ?? ""));
};
- const handleValues = (values: string[]) => {
- setPasswordValues(values);
- };
-
useEffect(() => {
async function fetchRoomEntry(_roomId: string) {
setIsLoading(true);
@@ -80,27 +64,26 @@ const RoomsListView: FC = ({ status }) => {
return;
}
- fetch(postRoomEntry(_roomId, passwordValues.join("")))
- .then(() => {
- router.push(`/rooms/${_roomId}`);
- })
- .catch((err: AxiosError) => {
+ try {
+ await fetch(postRoomEntry(_roomId, passwordValues.join("")));
+ router.push(`/rooms/${_roomId}`);
+ } catch (err) {
+ if (err instanceof AxiosError) {
const msg = err.response?.data.message.replaceAll(" ", "_");
if (!msg) return firePopup({ title: "error!" });
firePopup({ title: t(msg) });
- })
- .finally(() => {
- // setSelectedRoom(undefined);
- setPasswordValues(INIT_PASSWORD);
- isLoading && setIsLoading(false);
- });
+ }
+ } finally {
+ setPasswordValues(INIT_PASSWORD);
+ setIsLoading(false);
+ }
}
- if (!selectedRoom || isLoading) return;
- if (!selectedRoom.isLocked || passwordValues.every((char) => char !== "")) {
- fetchRoomEntry(selectedRoom.id);
+ if (!roomId || isLoading) return;
+ if (!isLocked || passwordValues.every((char) => char !== "")) {
+ fetchRoomEntry(roomId);
}
- }, [selectedRoom, passwordValues, isLoading, router, fetch, firePopup, t]);
+ }, [roomId, passwordValues, isLoading, router, fetch, firePopup, t]);
const Pagination = () => {
return (
@@ -129,8 +112,8 @@ const RoomsListView: FC = ({ status }) => {
))}
@@ -138,10 +121,10 @@ const RoomsListView: FC = ({ status }) => {
diff --git a/pages/_document.tsx b/pages/_document.tsx
index 181899f6..4f44a40c 100644
--- a/pages/_document.tsx
+++ b/pages/_document.tsx
@@ -12,7 +12,7 @@ export default function Document() {
-
+
diff --git a/pages/index.tsx b/pages/index.tsx
index 1e6598f7..2ed59834 100644
--- a/pages/index.tsx
+++ b/pages/index.tsx
@@ -10,6 +10,7 @@ import { mockCarouselItems } from "@/components/shared/Carousel";
import CarouselV2 from "@/components/shared/Carousel/v2";
import FastJoinButton from "@/components/lobby/FastJoinButton";
import SearchBar from "@/components/shared/SearchBar";
+import Tabs, { TabItemType } from "@/components/shared/Tabs";
function CarouselCard({
imgUrl,
@@ -94,8 +95,39 @@ function CarouselCard({
);
}
-export default function Home() {
+enum TabKey {
+ HOT = "hot",
+ NEW = "new",
+ LAST = "last",
+ GOOD = "good",
+ COLLECT = "collect",
+}
+
+const tabs: TabItemType[] = [
+ { tabKey: TabKey.HOT, label: "熱門遊戲" },
+ { tabKey: TabKey.NEW, label: "最新遊戲" },
+ { tabKey: TabKey.LAST, label: "上次遊玩" },
+ { tabKey: TabKey.GOOD, label: "好評遊戲" },
+ { tabKey: TabKey.COLLECT, label: "收藏遊戲" },
+];
+
+const TabPaneContent = (tabItem: TabItemType) => {
const { t } = useTranslation("rooms");
+ if (tabItem.tabKey === TabKey.HOT) {
+ return (
+
+
+
+
+
+ );
+ }
+ return 實作中...
;
+};
+
+export default function Home() {
return (
@@ -114,12 +146,8 @@ export default function Home() {
Component={CarouselCard}
/>
-
-
-
-
+
+
);
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 = () => {
(
-
+
)}
/>
diff --git a/pages/rooms/[roomId]/index.tsx b/pages/rooms/[roomId]/index.tsx
index 8ddcdd2f..ba8dfbbf 100644
--- a/pages/rooms/[roomId]/index.tsx
+++ b/pages/rooms/[roomId]/index.tsx
@@ -5,7 +5,6 @@ import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import RoomUserCardList from "@/components/rooms/RoomUserCardList";
import RoomButtonGroup from "@/components/rooms/RoomButtonGroup";
import RoomBreadcrumb from "@/components/rooms/RoomBreadcrumb";
-import RoomChatroom from "@/components/rooms/RoomChatroom";
import GameWindow from "@/components/rooms/GameWindow";
import useRequest from "@/hooks/useRequest";
import useRoom from "@/hooks/useRoom";
@@ -206,24 +205,39 @@ export default function Room() {
};
return (
-
-
+
+
+
![cover](https://images.unsplash.com/photo-1601987177651-8edfe6c20009?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2070&q=80)
+
+
+
+
+
+ {roomInfo.isLocked ? "非公開" : "公開"}
+
+
+ {roomInfo.currentPlayers} / {roomInfo.maxPlayers} 人
+
+
+
+
+
-
-
-
-
{gameUrl && }