From 091425e2243aec5652393d62d680756fa944a904 Mon Sep 17 00:00:00 2001 From: Johnson Mao Date: Fri, 8 Nov 2024 23:37:37 +0800 Subject: [PATCH 01/11] refactor: room card --- components/rooms/RoomCard.tsx | 66 ------------------- components/rooms/RoomsList.tsx | 2 +- containers/room/RoomListView.tsx | 2 +- .../room/components/RoomCard/RoomCard.tsx | 53 +++++++++++++++ features/room/components/RoomCard/index.ts | 1 + features/room/index.ts | 1 + 6 files changed, 57 insertions(+), 68 deletions(-) delete mode 100644 components/rooms/RoomCard.tsx create mode 100644 features/room/components/RoomCard/RoomCard.tsx create mode 100644 features/room/components/RoomCard/index.ts diff --git a/components/rooms/RoomCard.tsx b/components/rooms/RoomCard.tsx deleted file mode 100644 index b383d4b0..00000000 --- a/components/rooms/RoomCard.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { cn } from "@/lib/utils"; -import { Room } from "@/requests/rooms"; -import Icon from "@/components/shared/Icon"; -import Cover from "@/components/shared/Cover"; - -type RoomsCardProps = { - key: string; - room: Room; - active: boolean; - className?: string; - onClick: (id: string) => void; -}; -const RoomCard = ({ room, active, onClick }: RoomsCardProps) => { - const LockIcon = () => ( - - ); - - const roomCardClass = cn( - "room__card", - "relative cursor-pointer hover:border-blue2f transition-all duration-300", - "grid grid-cols-[34px_1fr] gap-[12px] rounded-[10px] border-2 border-dark1E py-[11px] pl-[11px] pr-[20px] bg-dark1E", - { - "border-blue": active, - } - ); - - const lackTotalPlayers = room.maxPlayers - room.currentPlayers; - - return ( -
onClick(room.id)}> - -
-

{room.name}

-
- {room.maxPlayers} - 人房, - {lackTotalPlayers > 0 ? ( - <> - 還缺 - {lackTotalPlayers}人 - - ) : ( - <>人數已滿 - )} -
-
- {/* 檢查是否上鎖 */} - {room.isLocked && ( -
- {LockIcon()} -
- )} -
- ); -}; - -export default RoomCard; diff --git a/components/rooms/RoomsList.tsx b/components/rooms/RoomsList.tsx index 4f678c9c..22340ef9 100644 --- a/components/rooms/RoomsList.tsx +++ b/components/rooms/RoomsList.tsx @@ -8,7 +8,7 @@ type RoomsListProps = { export const RoomsListWrapper = ({ className, children }: RoomsListProps) => { const listClass = cn( "rooms__list__wrapper", - "grid grid-cols-[repeat(auto-fill,_minmax(200px,_1fr))] gap-2.5 my-5", + "grid grid-cols-3 gap-5 my-5", className ); diff --git a/containers/room/RoomListView.tsx b/containers/room/RoomListView.tsx index c8a14ab4..e9af5556 100644 --- a/containers/room/RoomListView.tsx +++ b/containers/room/RoomListView.tsx @@ -10,7 +10,7 @@ import { getRoomInfoEndpoint, } from "@/requests/rooms"; import Button from "@/components/shared/Button"; -import RoomCard from "@/components/rooms/RoomCard"; +import { RoomCard } from "@/features/room"; import EnterPrivateRoomModal from "@/components/lobby/EnterPrivateRoomModal"; import { RoomsList, RoomsListWrapper } from "@/components/rooms/RoomsList"; import useRequest from "@/hooks/useRequest"; diff --git a/features/room/components/RoomCard/RoomCard.tsx b/features/room/components/RoomCard/RoomCard.tsx new file mode 100644 index 00000000..00b47e0b --- /dev/null +++ b/features/room/components/RoomCard/RoomCard.tsx @@ -0,0 +1,53 @@ +import type { Room } from "@/requests/rooms"; + +import Cover from "@/components/shared/Cover"; +import Button, { ButtonSize } from "@/components/shared/Button/v2"; + +interface RoomsCardProps { + room: Room; + className?: string; + onClick: (id: string) => void; +} + +function RoomCard({ room, onClick }: RoomsCardProps) { + const lackTotalPlayers = room.maxPlayers - room.currentPlayers; + + return ( +
+
+ +
+

{room.game.name}

+

{room.name}

+
+
+
+ {lackTotalPlayers > 0 ? ( +
+ + 剩餘 {lackTotalPlayers} 個位置 + + / {room.maxPlayers} 人 +
+ ) : ( +
人數已滿
+ )} + +
+
+ ); +} + +export default RoomCard; diff --git a/features/room/components/RoomCard/index.ts b/features/room/components/RoomCard/index.ts new file mode 100644 index 00000000..62856aac --- /dev/null +++ b/features/room/components/RoomCard/index.ts @@ -0,0 +1 @@ +export { default } from "./RoomCard"; diff --git a/features/room/index.ts b/features/room/index.ts index c0c7666a..a976d792 100644 --- a/features/room/index.ts +++ b/features/room/index.ts @@ -1,2 +1,3 @@ export { default as CreateRoomModal } from "./components/CreateRoomForm"; export { default as GameRoomActions } from "./components/GameRoomActions"; +export { default as RoomCard } from "./components/RoomCard"; From 91f420b7c31f4ba5d36bf4d33e93503365cdfeed Mon Sep 17 00:00:00 2001 From: Johnson Mao Date: Fri, 8 Nov 2024 23:38:42 +0800 Subject: [PATCH 02/11] refactor: search bar to app layout --- containers/layout/AppLayout.tsx | 25 ++++++++++++++++++++++++- pages/index.tsx | 16 ---------------- pages/rooms.tsx | 2 +- 3 files changed, 25 insertions(+), 18 deletions(-) diff --git a/containers/layout/AppLayout.tsx b/containers/layout/AppLayout.tsx index 123cfb6f..e1475a60 100644 --- a/containers/layout/AppLayout.tsx +++ b/containers/layout/AppLayout.tsx @@ -5,8 +5,11 @@ import Sidebar from "@/components/shared/Sidebar"; import Chat from "@/components/shared/Chat/v2/Chat"; import useChat from "@/hooks/useChat"; import Head from "next/head"; +import SearchBar from "@/components/shared/SearchBar"; +import { useToast } from "@/components/shared/Toast"; export default function Layout({ children }: PropsWithChildren) { + const toast = useToast(); const router = useRouter(); const { roomId, @@ -38,7 +41,27 @@ export default function Layout({ children }: PropsWithChildren) {
-
{children}
+
+
+ + toast( + { children: "此功能暫未實現", state: "warning" }, + { position: "top" } + ) + } + leftSlot={ + + } + /> +
+ {children} +
{isChatVisible && (
([]); useEffect(() => { @@ -83,21 +82,6 @@ export default function Home() { return (
-
- - toast( - { children: "此功能暫未實現", state: "warning" }, - { position: "top" } - ) - } - leftSlot={ - - } - /> -
diff --git a/pages/rooms.tsx b/pages/rooms.tsx index 8265f751..bee22a8d 100644 --- a/pages/rooms.tsx +++ b/pages/rooms.tsx @@ -20,7 +20,7 @@ const Rooms = () => { ]; return ( -
+
Date: Sat, 9 Nov 2024 14:05:03 +0800 Subject: [PATCH 03/11] feat(game): add game list provider --- containers/layout/AppLayout.tsx | 5 +- .../GameCardDetailed.tsx | 0 .../game/components/GameCardDetailed/index.ts | 1 - .../{GameCardSimple => }/GameCardSimple.tsx | 0 .../game/components/GameCardSimple/index.ts | 1 - features/game/components/index.ts | 2 + features/game/contexts/GameList.tsx | 37 ++++++++++++ features/game/contexts/index.ts | 2 + features/game/index.ts | 4 +- .../{CreateRoomForm => }/CreateRoomForm.tsx | 0 .../room/components/CreateRoomForm/index.ts | 1 - .../{GameRoomActions => }/GameRoomActions.tsx | 2 +- .../room/components/GameRoomActions/index.ts | 1 - .../components/{RoomCard => }/RoomCard.tsx | 2 +- features/room/components/RoomCard/index.ts | 1 - features/room/components/index.ts | 3 + features/room/index.ts | 4 +- pages/_app.tsx | 38 +++++++----- pages/_document.tsx | 1 - pages/index.tsx | 60 +++++++------------ pages/{rooms.tsx => rooms/index.tsx} | 0 21 files changed, 97 insertions(+), 68 deletions(-) rename features/game/components/{GameCardDetailed => }/GameCardDetailed.tsx (100%) delete mode 100644 features/game/components/GameCardDetailed/index.ts rename features/game/components/{GameCardSimple => }/GameCardSimple.tsx (100%) delete mode 100644 features/game/components/GameCardSimple/index.ts create mode 100644 features/game/components/index.ts create mode 100644 features/game/contexts/GameList.tsx create mode 100644 features/game/contexts/index.ts rename features/room/components/{CreateRoomForm => }/CreateRoomForm.tsx (100%) delete mode 100644 features/room/components/CreateRoomForm/index.ts rename features/room/components/{GameRoomActions => }/GameRoomActions.tsx (98%) delete mode 100644 features/room/components/GameRoomActions/index.ts rename features/room/components/{RoomCard => }/RoomCard.tsx (97%) delete mode 100644 features/room/components/RoomCard/index.ts create mode 100644 features/room/components/index.ts rename pages/{rooms.tsx => rooms/index.tsx} (100%) diff --git a/containers/layout/AppLayout.tsx b/containers/layout/AppLayout.tsx index e1475a60..1073ac02 100644 --- a/containers/layout/AppLayout.tsx +++ b/containers/layout/AppLayout.tsx @@ -7,6 +7,7 @@ import useChat from "@/hooks/useChat"; import Head from "next/head"; import SearchBar from "@/components/shared/SearchBar"; import { useToast } from "@/components/shared/Toast"; +import { GameListProvider } from "@/features/game"; export default function Layout({ children }: PropsWithChildren) { const toast = useToast(); @@ -28,7 +29,7 @@ export default function Layout({ children }: PropsWithChildren) { }, [router.pathname, openChat]); return ( - <> + 遊戲微服務大平台 @@ -79,6 +80,6 @@ export default function Layout({ children }: PropsWithChildren) {
)}
- + ); } diff --git a/features/game/components/GameCardDetailed/GameCardDetailed.tsx b/features/game/components/GameCardDetailed.tsx similarity index 100% rename from features/game/components/GameCardDetailed/GameCardDetailed.tsx rename to features/game/components/GameCardDetailed.tsx diff --git a/features/game/components/GameCardDetailed/index.ts b/features/game/components/GameCardDetailed/index.ts deleted file mode 100644 index 77e1d14f..00000000 --- a/features/game/components/GameCardDetailed/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./GameCardDetailed"; diff --git a/features/game/components/GameCardSimple/GameCardSimple.tsx b/features/game/components/GameCardSimple.tsx similarity index 100% rename from features/game/components/GameCardSimple/GameCardSimple.tsx rename to features/game/components/GameCardSimple.tsx diff --git a/features/game/components/GameCardSimple/index.ts b/features/game/components/GameCardSimple/index.ts deleted file mode 100644 index fe332cff..00000000 --- a/features/game/components/GameCardSimple/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./GameCardSimple"; diff --git a/features/game/components/index.ts b/features/game/components/index.ts new file mode 100644 index 00000000..91477fb7 --- /dev/null +++ b/features/game/components/index.ts @@ -0,0 +1,2 @@ +export { default as GameCardDetailed } from "./GameCardDetailed"; +export { default as GameCardSimple } from "./GameCardSimple"; diff --git a/features/game/contexts/GameList.tsx b/features/game/contexts/GameList.tsx new file mode 100644 index 00000000..41621e7d --- /dev/null +++ b/features/game/contexts/GameList.tsx @@ -0,0 +1,37 @@ +import { getAllGamesEndpoint, type GameType } from "@/requests/games"; + +import { + createContext, + PropsWithChildren, + useContext, + useEffect, + useState, +} from "react"; +import useRequest from "@/hooks/useRequest"; + +const GameListContext = createContext([]); + +function GameListProvider({ children }: PropsWithChildren) { + const { fetch } = useRequest(); + const [gameList, setGameList] = useState([]); + + useEffect(() => { + fetch(getAllGamesEndpoint()).then(setGameList); + }, [fetch]); + + return ( + + {children} + + ); +} + +export const useGameList = () => { + const result = useContext(GameListContext); + if (!result) { + throw new Error("useGameList must be used within a GameListProvider"); + } + return result; +}; + +export default GameListProvider; diff --git a/features/game/contexts/index.ts b/features/game/contexts/index.ts new file mode 100644 index 00000000..e0219f75 --- /dev/null +++ b/features/game/contexts/index.ts @@ -0,0 +1,2 @@ +export { default as GameListProvider } from "./gameList"; +export * from "./gameList"; diff --git a/features/game/index.ts b/features/game/index.ts index d7a8897a..d84ff7d8 100644 --- a/features/game/index.ts +++ b/features/game/index.ts @@ -1,2 +1,2 @@ -export { default as GameCardDetailed } from "./components/GameCardDetailed"; -export { default as GameCardSimple } from "./components/GameCardSimple"; +export * from "./components"; +export * from "./contexts"; diff --git a/features/room/components/CreateRoomForm/CreateRoomForm.tsx b/features/room/components/CreateRoomForm.tsx similarity index 100% rename from features/room/components/CreateRoomForm/CreateRoomForm.tsx rename to features/room/components/CreateRoomForm.tsx diff --git a/features/room/components/CreateRoomForm/index.ts b/features/room/components/CreateRoomForm/index.ts deleted file mode 100644 index ff7236f0..00000000 --- a/features/room/components/CreateRoomForm/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./CreateRoomForm"; diff --git a/features/room/components/GameRoomActions/GameRoomActions.tsx b/features/room/components/GameRoomActions.tsx similarity index 98% rename from features/room/components/GameRoomActions/GameRoomActions.tsx rename to features/room/components/GameRoomActions.tsx index bebf8861..8649539c 100644 --- a/features/room/components/GameRoomActions/GameRoomActions.tsx +++ b/features/room/components/GameRoomActions.tsx @@ -11,7 +11,7 @@ import { fastJoinGameEndpoint } from "@/requests/rooms"; import Icon from "@/components/shared/Icon"; import Modal from "@/components/shared/Modal"; import { cn } from "@/lib/utils"; -import CreateRoomModal from "../CreateRoomForm"; +import CreateRoomModal from "./CreateRoomForm"; interface GameRoomActions extends GameType { tabIndex?: number; diff --git a/features/room/components/GameRoomActions/index.ts b/features/room/components/GameRoomActions/index.ts deleted file mode 100644 index 4aacc14a..00000000 --- a/features/room/components/GameRoomActions/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./GameRoomActions"; diff --git a/features/room/components/RoomCard/RoomCard.tsx b/features/room/components/RoomCard.tsx similarity index 97% rename from features/room/components/RoomCard/RoomCard.tsx rename to features/room/components/RoomCard.tsx index 00b47e0b..8e17fb4b 100644 --- a/features/room/components/RoomCard/RoomCard.tsx +++ b/features/room/components/RoomCard.tsx @@ -17,7 +17,7 @@ function RoomCard({ room, onClick }: RoomsCardProps) {
- - - {getHistory( - - - - {getLayout()} - {!isProduction && } - - - - )} - - - + <> + + 遊戲微服務大平台 + + + + + {getHistory( + + + + {getLayout()} + {!isProduction && } + + + + )} + + + + ); } diff --git a/pages/_document.tsx b/pages/_document.tsx index 5fd40813..ed031e8b 100644 --- a/pages/_document.tsx +++ b/pages/_document.tsx @@ -11,7 +11,6 @@ export default function Document() { {/* */} - {siteTitle}
diff --git a/pages/index.tsx b/pages/index.tsx index 8954acc1..e9b0e9f1 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -1,17 +1,22 @@ +import type { GameType } from "@/requests/games"; + import { GetStaticProps } from "next"; -import { useEffect, useState } from "react"; import { serverSideTranslations } from "next-i18next/serverSideTranslations"; import CarouselV2 from "@/components/shared/Carousel/v2"; -import SearchBar from "@/components/shared/SearchBar"; import Tabs, { TabItemType } from "@/components/shared/Tabs"; -import { useToast } from "@/components/shared/Toast"; -import { GameType, getAllGamesEndpoint } from "@/requests/games"; -import useRequest from "@/hooks/useRequest"; import { CarouselItemProps } from "@/components/shared/Carousel/v2/Carousel.type"; -import { GameCardDetailed, GameCardSimple } from "@/features/game"; +import { GameCardDetailed, GameCardSimple, useGameList } from "@/features/game"; import { GameRoomActions } from "@/features/room"; +enum TabKey { + HOT = "hot", + NEW = "new", + LAST = "last", + GOOD = "good", + COLLECT = "collect", +} + function CarouselCard({ showIndex, index, @@ -24,26 +29,9 @@ function CarouselCard({ ); } -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: "收藏遊戲" }, -]; +function TabPaneContent({ tabKey }: TabItemType) { + const gameList = useGameList(); -const TabPaneContent = ({ - tabKey, - gameList, -}: TabItemType & { gameList: GameType[] }) => { if ([TabKey.HOT, TabKey.NEW].includes(tabKey)) { const data = tabKey === TabKey.HOT @@ -70,15 +58,18 @@ const TabPaneContent = ({ } return
實作中...
; -}; +} export default function Home() { - const { fetch } = useRequest(); - const [gameList, setGameList] = useState([]); + const gameList = useGameList(); - useEffect(() => { - fetch(getAllGamesEndpoint()).then(setGameList); - }, [fetch]); + const tabs: TabItemType[] = [ + { tabKey: TabKey.HOT, label: "熱門遊戲" }, + { tabKey: TabKey.NEW, label: "最新遊戲" }, + { tabKey: TabKey.LAST, label: "上次遊玩" }, + { tabKey: TabKey.GOOD, label: "好評遊戲" }, + { tabKey: TabKey.COLLECT, label: "收藏遊戲" }, + ]; return (
@@ -86,12 +77,7 @@ export default function Home() {
- ( - - )} - /> +
); diff --git a/pages/rooms.tsx b/pages/rooms/index.tsx similarity index 100% rename from pages/rooms.tsx rename to pages/rooms/index.tsx From cf6990bfa2d8f480b20b8e8025a93536449312ff Mon Sep 17 00:00:00 2001 From: Johnson Mao Date: Sat, 9 Nov 2024 22:36:40 +0800 Subject: [PATCH 04/11] refactor(room): join room and password modal style --- .../EnterPrivateRoomModal.tsx | 45 ------ .../lobby/EnterPrivateRoomModal/index.tsx | 3 - components/shared/Button/v2/Button.tsx | 4 +- components/shared/InputOTP/InputOTP.tsx | 21 ++- containers/room/RoomListView.tsx | 143 ------------------ features/room/components/CreateRoomForm.tsx | 6 +- features/room/components/GameRoomActions.tsx | 4 +- features/room/components/JoinLockRoomForm.tsx | 58 +++++++ features/room/components/RoomCard.tsx | 8 +- features/room/components/RoomList.tsx | 140 +++++++++++++++++ features/room/components/index.ts | 2 + pages/rooms/index.tsx | 12 +- 12 files changed, 234 insertions(+), 212 deletions(-) delete mode 100644 components/lobby/EnterPrivateRoomModal/EnterPrivateRoomModal.tsx delete mode 100644 components/lobby/EnterPrivateRoomModal/index.tsx delete mode 100644 containers/room/RoomListView.tsx create mode 100644 features/room/components/JoinLockRoomForm.tsx create mode 100644 features/room/components/RoomList.tsx diff --git a/components/lobby/EnterPrivateRoomModal/EnterPrivateRoomModal.tsx b/components/lobby/EnterPrivateRoomModal/EnterPrivateRoomModal.tsx deleted file mode 100644 index 591a1abd..00000000 --- a/components/lobby/EnterPrivateRoomModal/EnterPrivateRoomModal.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import type { ClipboardEvent } from "react"; - -import Modal from "@/components/shared/Modal"; -import PasswordField from "@/components/shared/PasswordField"; - -type EnterPrivateRoomModalProps = { - isOpen: boolean; - loading?: boolean; - passwordValues: string[]; - setPasswordValues: (values: string[]) => void; - onClose: () => void; - onPaste?: (e: ClipboardEvent) => void; -}; - -export default function EnterPrivateRoomModal({ - isOpen, - loading, - passwordValues, - setPasswordValues, - onClose, - onPaste, -}: EnterPrivateRoomModalProps) { - return ( - -
- -
- {loading && ( -
-
-
- )} -
- ); -} diff --git a/components/lobby/EnterPrivateRoomModal/index.tsx b/components/lobby/EnterPrivateRoomModal/index.tsx deleted file mode 100644 index 37536a36..00000000 --- a/components/lobby/EnterPrivateRoomModal/index.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import EnterPrivateRoomModal from "./EnterPrivateRoomModal"; - -export default EnterPrivateRoomModal; diff --git a/components/shared/Button/v2/Button.tsx b/components/shared/Button/v2/Button.tsx index 4b26dd48..67a86777 100644 --- a/components/shared/Button/v2/Button.tsx +++ b/components/shared/Button/v2/Button.tsx @@ -32,7 +32,7 @@ const buttonTypeClasses: Record = { const buttonSizeClasses: Record = { icon: "p-2", - small: "py-2 px-4 gap-1", + small: "py-0 px-4 gap-1", regular: "py-2 px-6 gap-2", }; @@ -73,7 +73,7 @@ const InteralButton: InnerButtonComponent = ( ); const boxFancyClassName = cn( - "w-fit items-center fz-16-b transition-colors transition-[border-image] ease-in", + "w-fit items-center fz-16-b transition-colors transition-[border-image] ease-in whitespace-nowrap", commonDisabledClasses, buttonTypeClasses[variant], buttonSizeClasses[size], diff --git a/components/shared/InputOTP/InputOTP.tsx b/components/shared/InputOTP/InputOTP.tsx index 4353ea52..d9639f0c 100644 --- a/components/shared/InputOTP/InputOTP.tsx +++ b/components/shared/InputOTP/InputOTP.tsx @@ -7,6 +7,7 @@ import { ForwardRefRenderFunction, forwardRef, useImperativeHandle, + ClipboardEvent, } from "react"; import Input from "@/components/shared/Input"; import { cn } from "@/lib/utils"; @@ -24,6 +25,7 @@ interface InputOTPProps { hintText?: string; labelClassName?: string; hintTextClassName?: string; + autoFocus?: boolean; onChange?: (value: string) => void; } @@ -37,6 +39,7 @@ const InternalInputOTP: ForwardRefRenderFunction = ( hintText, labelClassName, hintTextClassName, + autoFocus, onChange, }, ref @@ -94,12 +97,27 @@ const InternalInputOTP: ForwardRefRenderFunction = ( } }; + const handlePaste = (e: ClipboardEvent) => { + const pastePassword = e.clipboardData + .getData("text") + .replace(/\D/g, "") + .split(""); + + pastePassword.length = length; + + setChars(pastePassword); + }; + useEffect(() => { - if (value) { + if (typeof value === "string") { setChars(value.split("")); } }, [value]); + useEffect(() => { + if (autoFocus) inputsRef.current[0]?.focus(); + }, [autoFocus]); + useImperativeHandle( ref, () => { @@ -138,6 +156,7 @@ const InternalInputOTP: ForwardRefRenderFunction = ( error={error} value={chars[index] || ""} onKeyUp={handleKeyUp(index)} + onPaste={handlePaste} /> ))}
diff --git a/containers/room/RoomListView.tsx b/containers/room/RoomListView.tsx deleted file mode 100644 index e9af5556..00000000 --- a/containers/room/RoomListView.tsx +++ /dev/null @@ -1,143 +0,0 @@ -import { ClipboardEvent, FC, useState, useEffect } from "react"; -import { useRouter } from "next/router"; -import { AxiosError } from "axios"; -import { useTranslation } from "next-i18next"; - -import { - RoomType, - getRooms, - postRoomEntry, - getRoomInfoEndpoint, -} from "@/requests/rooms"; -import Button from "@/components/shared/Button"; -import { RoomCard } from "@/features/room"; -import EnterPrivateRoomModal from "@/components/lobby/EnterPrivateRoomModal"; -import { RoomsList, RoomsListWrapper } from "@/components/rooms/RoomsList"; -import useRequest from "@/hooks/useRequest"; -import usePagination from "@/hooks/usePagination"; -import usePopup from "@/hooks/usePopup"; -import useUser from "@/hooks/useUser"; - -type Props = { - status: RoomType; -}; - -const INIT_PASSWORD = ["", "", "", ""]; - -const RoomsListView: FC = ({ status }) => { - const { t } = useTranslation("rooms"); - const { fetch } = useRequest(); - const { nextPage, backPage, data, loading, isError, errorMessage } = - usePagination({ - source: (page: number, perPage: number) => - fetch(getRooms({ page, perPage, status })), - defaultPerPage: 20, - }); - const [roomId, setRoomId] = useState(null); - const [passwordValues, setPasswordValues] = useState(INIT_PASSWORD); - const [isLoading, setIsLoading] = useState(false); - const { Popup, firePopup } = usePopup(); - const { updateRoomId } = useUser(); - const router = useRouter(); - const isLocked = data.find((room) => room.id === roomId)?.isLocked; - - const handleClose = () => { - setPasswordValues(INIT_PASSWORD); - setRoomId(null); - }; - - const handlePaste = (e: ClipboardEvent) => { - const pastePassword = e.clipboardData - .getData("text") - .replace(/\D/g, "") - .split(""); - - pastePassword.length = 4; - - setPasswordValues(Array.from(pastePassword, (text) => text ?? "")); - }; - - useEffect(() => { - async function fetchRoomEntry(_roomId: string) { - setIsLoading(true); - - // Automatically enter the room if room information is accessible, - // indicating the user is already in the room - if (await fetch(getRoomInfoEndpoint(_roomId)).catch(() => {})) { - router.push(`/rooms/${_roomId}`); - updateRoomId(_roomId); - return; - } - - try { - await fetch(postRoomEntry(_roomId, passwordValues.join(""))); - router.push(`/rooms/${_roomId}`); - updateRoomId(_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 { - setPasswordValues(INIT_PASSWORD); - setIsLoading(false); - setRoomId(null); - } - } - - if (!roomId || isLoading) return; - if (!isLocked || passwordValues.every((char) => char !== "")) { - fetchRoomEntry(roomId); - } - }, [roomId, passwordValues, isLoading, router, fetch, firePopup, t]); - - const Pagination = () => { - return ( -
- - -
- ); - }; - - if (loading) - return
Loading...
; - if (isError) - return ( -
- Response Error: {errorMessage} -
- ); - - return ( - <> - - - {data.length > 0 && - data.map((_room) => ( - - ))} - - - - - - - - ); -}; - -export default RoomsListView; diff --git a/features/room/components/CreateRoomForm.tsx b/features/room/components/CreateRoomForm.tsx index 3d5cff1c..b6242505 100644 --- a/features/room/components/CreateRoomForm.tsx +++ b/features/room/components/CreateRoomForm.tsx @@ -52,7 +52,6 @@ function CreateRoomForm({ gameId, minPlayers, maxPlayers, - password: "", }); const { fetch } = useRequest(); const router = useRouter(); @@ -99,10 +98,7 @@ function CreateRoomForm({ if (isLockRoom) { passwordInputRef.current?.focus(); } else { - setRoomForm((pre) => ({ - ...pre, - password: "", - })); + setRoomForm((pre) => ({ ...pre, password: undefined })); } }, [isLockRoom]); diff --git a/features/room/components/GameRoomActions.tsx b/features/room/components/GameRoomActions.tsx index 8649539c..f600f144 100644 --- a/features/room/components/GameRoomActions.tsx +++ b/features/room/components/GameRoomActions.tsx @@ -11,7 +11,7 @@ import { fastJoinGameEndpoint } from "@/requests/rooms"; import Icon from "@/components/shared/Icon"; import Modal from "@/components/shared/Modal"; import { cn } from "@/lib/utils"; -import CreateRoomModal from "./CreateRoomForm"; +import CreateRoomForm from "./CreateRoomForm"; interface GameRoomActions extends GameType { tabIndex?: number; @@ -117,7 +117,7 @@ function GameRoomActions({ onClose={() => setShowCreateRoomModal(false)} size="medium" > - Promise; +} + +function JoinLockRoomForm({ children, onSubmit }: JoinLockRoomFormProps) { + const { t } = useTranslation("rooms"); + const [password, setPassword] = useState(""); + const [errorMessage, setErrorMessage] = useState(""); + + const handleChange = (value: string) => { + setErrorMessage(""); + setPassword(value); + }; + + const handleSubmit = async (event: FormEvent) => { + event.preventDefault(); + setErrorMessage(""); + try { + await onSubmit(password); + } catch (error) { + if (typeof error === "string") { + setErrorMessage(error); + } + } + }; + + return ( +
+ {children} + + + + + + ); +} + +export default JoinLockRoomForm; diff --git a/features/room/components/RoomCard.tsx b/features/room/components/RoomCard.tsx index 8e17fb4b..b202bd32 100644 --- a/features/room/components/RoomCard.tsx +++ b/features/room/components/RoomCard.tsx @@ -6,10 +6,10 @@ import Button, { ButtonSize } from "@/components/shared/Button/v2"; interface RoomsCardProps { room: Room; className?: string; - onClick: (id: string) => void; + onJoin: (id: string) => void; } -function RoomCard({ room, onClick }: RoomsCardProps) { +function RoomCard({ room, onJoin: onClick }: RoomsCardProps) { const lackTotalPlayers = room.maxPlayers - room.currentPlayers; return ( @@ -27,7 +27,7 @@ function RoomCard({ room, onClick }: RoomsCardProps) {

{room.name}

-
+
{lackTotalPlayers > 0 ? (
@@ -45,7 +45,7 @@ function RoomCard({ room, onClick }: RoomsCardProps) { > 加入 -
+
); } diff --git a/features/room/components/RoomList.tsx b/features/room/components/RoomList.tsx new file mode 100644 index 00000000..6c002230 --- /dev/null +++ b/features/room/components/RoomList.tsx @@ -0,0 +1,140 @@ +import { useState, useEffect, useMemo, useCallback } from "react"; +import { useRouter } from "next/router"; +import { AxiosError } from "axios"; +import { useTranslation } from "next-i18next"; + +import { + RoomType, + getRooms, + postRoomEntry, + getRoomInfoEndpoint, +} from "@/requests/rooms"; +import { JoinLockRoomForm, RoomCard } from "@/features/room"; +import useRequest from "@/hooks/useRequest"; +import usePagination from "@/hooks/usePagination"; +import useUser from "@/hooks/useUser"; +import { useGameList } from "@/features/game"; +import Modal from "@/components/shared/Modal"; +import Cover from "@/components/shared/Cover"; +import { useToast } from "@/components/shared/Toast"; + +interface RoomListProps { + tabKey: RoomType; +} + +function RoomList({ tabKey }: RoomListProps) { + const { t } = useTranslation("rooms"); + const { fetch } = useRequest(); + const { updateRoomId } = useUser(); + const toast = useToast(); + const router = useRouter(); + const { data } = usePagination({ + source: (page: number, perPage: number) => + fetch(getRooms({ page, perPage, status: tabKey })), + defaultPerPage: 20, + }); + const gameList = useGameList(); + const rooms = useMemo( + () => + Array.isArray(data) + ? data.map((room) => ({ + ...room, + game: { + ...room.game, + imgUrl: + gameList.find((game) => game.id === room.game.id)?.img || "", + }, + })) + : [], + [data, gameList] + ); + const [roomId, setRoomId] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const room = useMemo( + () => rooms.find((room) => room.id === roomId), + [roomId, rooms] + ); + const isLocked = room?.isLocked; + const isOpenPasswordModel = !!isLocked && !!roomId; + + const handleClose = () => { + setRoomId(null); + setIsLoading(false); + }; + + const fetchRoomEntry = useCallback( + async (password: string | null = null) => { + if (!roomId) return; + + setIsLoading(true); + + // Automatically enter the room if room information is accessible, + // indicating the user is already in the room + if (await fetch(getRoomInfoEndpoint(roomId)).catch(() => {})) { + router.push(`/rooms/${roomId}`); + updateRoomId(roomId); + return; + } + + try { + await fetch(postRoomEntry(roomId, password)); + router.push(`/rooms/${roomId}`); + updateRoomId(roomId); + handleClose(); + } catch (err) { + if (err instanceof AxiosError) { + const msg = err.response?.data.message.replaceAll(" ", "_"); + if (!msg) return toast({ children: "error!", state: "error" }); + toast({ children: t(msg), state: "error" }); + setIsLoading(false); + return Promise.reject(t(msg)); + } + } + }, + [roomId, fetch, router, t, toast, updateRoomId] + ); + + useEffect(() => { + if (isLoading || isLocked) return; + fetchRoomEntry(); + }, [isLoading, isLocked, fetchRoomEntry]); + + return ( + <> +
    + {rooms.map((_room) => ( +
  • + +
  • + ))} +
+ + + + {room && ( +
+ +
+

{room.game.name}

+

{room.name}

+
+
+ )} +
+
+ + ); +} + +export default RoomList; diff --git a/features/room/components/index.ts b/features/room/components/index.ts index a5266bb4..f8fcbdef 100644 --- a/features/room/components/index.ts +++ b/features/room/components/index.ts @@ -1,3 +1,5 @@ export { default as CreateRoomForm } from "./CreateRoomForm"; export { default as GameRoomActions } from "./GameRoomActions"; +export { default as JoinLockRoomForm } from "./JoinLockRoomForm"; export { default as RoomCard } from "./RoomCard"; +export { default as RoomList } from "./RoomList"; diff --git a/pages/rooms/index.tsx b/pages/rooms/index.tsx index bee22a8d..83fd9711 100644 --- a/pages/rooms/index.tsx +++ b/pages/rooms/index.tsx @@ -1,9 +1,9 @@ +import { GetStaticProps } from "next"; +import { useTranslation } from "next-i18next"; +import { serverSideTranslations } from "next-i18next/serverSideTranslations"; import { RoomType } from "@/requests/rooms"; -import RoomsListView from "@/containers/room/RoomListView"; +import { RoomList } from "@/features/room"; import Tabs, { TabItemType } from "@/components/shared/Tabs"; -import { serverSideTranslations } from "next-i18next/serverSideTranslations"; -import { useTranslation } from "next-i18next"; -import { GetStaticProps } from "next"; const Rooms = () => { const { t } = useTranslation("rooms"); @@ -24,9 +24,7 @@ const Rooms = () => { ( - - )} + renderTabPaneContent={RoomList} /> ); From 2fe9d35a25347da1732999115be16b718fd28a8a Mon Sep 17 00:00:00 2001 From: Johnson Mao Date: Sun, 10 Nov 2024 11:16:01 +0800 Subject: [PATCH 05/11] feat: add logo png and refactor login page --- components/shared/Header.tsx | 5 +- pages/_document.tsx | 2 +- pages/login.tsx | 110 ++++++++++++----------------------- public/logo.png | Bin 0 -> 7918 bytes 4 files changed, 41 insertions(+), 76 deletions(-) create mode 100644 public/logo.png diff --git a/components/shared/Header.tsx b/components/shared/Header.tsx index dd85d62a..0fcf1b37 100644 --- a/components/shared/Header.tsx +++ b/components/shared/Header.tsx @@ -2,10 +2,11 @@ import { useEffect, useState } from "react"; import Icon, { IconName } from "@/components/shared/Icon"; import Badge from "@/components/shared/Badge"; import { cn } from "@/lib/utils"; -import Modal from "./Modal"; import { UserInfoForm } from "@/features/user"; import useUser from "@/hooks/useUser"; import { UserInfo } from "@/requests/users"; +import Modal from "./Modal"; +import Cover from "./Cover"; enum HeaderActions { CHAT = "CHAT", @@ -71,7 +72,7 @@ export default function Header({ )} >
- +

遊戲微服務大平台

diff --git a/pages/_document.tsx b/pages/_document.tsx index ed031e8b..b9ad9ead 100644 --- a/pages/_document.tsx +++ b/pages/_document.tsx @@ -6,7 +6,7 @@ export default function Document() { return ( - + {/* */} diff --git a/pages/login.tsx b/pages/login.tsx index 7fa065e5..de926aeb 100644 --- a/pages/login.tsx +++ b/pages/login.tsx @@ -1,10 +1,4 @@ -import { - SyntheticEvent, - useCallback, - useEffect, - useMemo, - useState, -} from "react"; +import { SyntheticEvent, useEffect, useState } from "react"; import { useRouter } from "next/router"; import { GetStaticProps } from "next"; import { serverSideTranslations } from "next-i18next/serverSideTranslations"; @@ -21,9 +15,8 @@ import { LoginType } from "@/requests/auth"; import { NextPageWithProps } from "./_app"; import { BoxFancy } from "@/components/shared/BoxFancy"; -import { useSearchParams } from "next/navigation"; -const LoginMethods: { text: string; type: LoginType; icon: IconName }[] = [ +const loginMethods: { text: string; type: LoginType; icon: IconName }[] = [ { text: "Google 帳號登入", type: LoginType.GOOGLE, icon: "Google" }, { text: "GitHub 帳號登入", type: LoginType.GITHUB, icon: "Github" }, { text: "LinkedIn 帳號登入", type: LoginType.LINKEDIN, icon: "Linkedin" }, @@ -33,86 +26,57 @@ const LoginMethods: { text: string; type: LoginType; icon: IconName }[] = [ const Login: NextPageWithProps = () => { const { getLoginEndpoint } = useUser(); const { token } = useAuth(); - const { push } = useRouter(); + const router = useRouter(); const [checkAuth, setCheckAuth] = useState(false); const { internalEndpoint, isMock } = getEnv(); - const searchParams = useSearchParams(); - // if the account is withdrawn, show the message - const bye = searchParams.get("bye") !== null; useEffect(() => { if (token) { - push("/"); + router.push("/"); } else { setCheckAuth(true); } - }, [token, push]); + }, [token, router]); - const onLoginClick = useCallback( - async (e: SyntheticEvent, type: LoginType) => { - if (isMock) { - e.preventDefault(); - e.stopPropagation(); + const onLoginClick = async (e: SyntheticEvent, type: LoginType) => { + if (isMock) { + e.preventDefault(); + e.stopPropagation(); - const endpoint = await getLoginEndpoint(type); - // mock: redirect to /auth/token - push(endpoint.url); - } - }, - [getLoginEndpoint, isMock, push] - ); - - const loginButtons = useMemo(() => { - return LoginMethods.map(({ text, type, icon }) => ( - - onLoginClick(e, type)} - > - - {text} - - - )); - }, [internalEndpoint, onLoginClick]); + const endpoint = await getLoginEndpoint(type); + router.push(endpoint.url); + } + }; return checkAuth ? (
- {/* fog */} -
+
- {bye ? ( -

- {"原帳號已註銷成功。\n我們非常歡迎你再加入,\n和我們一起遊樂!"} -

- ) : null} -

- +

+ 遊戲微服務大平台

- {!bye ? ( - <> -

- 一起創造與冒險! -

-

- 加入遊戲微服務大平台,和100+遊戲開發者共同創建更多可能! -

- - ) : null} +

+ 一起創造與冒險! +

+

+ 加入遊戲微服務大平台,和100+遊戲開發者共同創建更多可能! +

- {loginButtons} + {loginMethods.map(({ text, type, icon }) => ( + + onLoginClick(e, type)} + > + + {text} + + + ))}
) : ( @@ -122,7 +86,7 @@ const Login: NextPageWithProps = () => { Login.Anonymous = true; -Login.getLayout = (page) => ( +Login.getLayout = ({ children }) => (
( />
- {page} + {children}
@@ -143,7 +107,7 @@ export default Login; export const getStaticProps: GetStaticProps = async ({ locale }) => { return { props: { - ...(await serverSideTranslations(locale ?? "zh-TW", [""])), + ...(await serverSideTranslations(locale ?? "zh-TW", ["rooms"])), }, }; }; diff --git a/public/logo.png b/public/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..ca4ddfeacde757672dc477c5a390d3a930e28036 GIT binary patch literal 7918 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 literal 0 HcmV?d00001 From 2450f560dff6f5f7b6d8ad95716659249d69c1ae Mon Sep 17 00:00:00 2001 From: Johnson Mao Date: Sun, 10 Nov 2024 11:18:48 +0800 Subject: [PATCH 06/11] refactor: layout --- containers/layout/AppLayout.tsx | 41 ++++++++++++++++++--------------- pages/_app.tsx | 12 ++++++---- pages/auth/login.tsx | 2 +- pages/auth/token/[token].tsx | 2 +- 4 files changed, 31 insertions(+), 26 deletions(-) diff --git a/containers/layout/AppLayout.tsx b/containers/layout/AppLayout.tsx index 1073ac02..0beedee0 100644 --- a/containers/layout/AppLayout.tsx +++ b/containers/layout/AppLayout.tsx @@ -9,7 +9,7 @@ import SearchBar from "@/components/shared/SearchBar"; import { useToast } from "@/components/shared/Toast"; import { GameListProvider } from "@/features/game"; -export default function Layout({ children }: PropsWithChildren) { +export default function AppLayout({ children }: PropsWithChildren) { const toast = useToast(); const router = useRouter(); const { @@ -21,6 +21,7 @@ export default function Layout({ children }: PropsWithChildren) { handleSubmitText, } = useChat(); const roomPathname = "/rooms/[roomId]"; + const isSearchBarVisible = ["/", "/rooms"].includes(router.pathname); useEffect(() => { if (router.pathname === roomPathname) { @@ -43,24 +44,26 @@ export default function Layout({ children }: PropsWithChildren) {
-
- - toast( - { children: "此功能暫未實現", state: "warning" }, - { position: "top" } - ) - } - leftSlot={ - - } - /> -
+ {isSearchBarVisible && ( +
+ + toast( + { children: "此功能暫未實現", state: "warning" }, + { position: "top" } + ) + } + leftSlot={ + + } + /> +
+ )} {children}
{isChatVisible && ( diff --git a/pages/_app.tsx b/pages/_app.tsx index f15abaec..3d377407 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -1,6 +1,6 @@ import { AppProps } from "next/app"; import { NextPage } from "next"; -import { ReactElement, ReactNode } from "react"; +import { FC, PropsWithChildren, ReactElement } from "react"; import { appWithTranslation } from "next-i18next"; import Head from "next/head"; @@ -20,7 +20,7 @@ import { SocketProvider } from "@/containers/provider/SocketProvider"; import ChatroomContextProvider from "@/containers/provider/ChatroomProvider"; export type NextPageWithProps

= NextPage & { - getLayout?: (page: ReactElement) => ReactNode; + getLayout?: FC; Anonymous?: boolean; }; @@ -34,9 +34,9 @@ function App({ Component, pageProps }: AppWithProps) { Component.Anonymous || !!process.env.NEXT_PUBLIC_CI_MODE || false; const isProduction = env !== Env.PROD ? false : true; - const getLayout = + const Layout = Component.getLayout ?? - ((page: ReactElement) => {page}); + (({ children }) => {children}); const getHistory = (children: ReactElement) => { return isProduction ? ( @@ -58,7 +58,9 @@ function App({ Component, pageProps }: AppWithProps) { - {getLayout()} + + + {!isProduction && } diff --git a/pages/auth/login.tsx b/pages/auth/login.tsx index dd424390..e0a9329a 100644 --- a/pages/auth/login.tsx +++ b/pages/auth/login.tsx @@ -25,7 +25,7 @@ const Login: NextPageWithProps = () => { return <>; }; -Login.getLayout = (page: ReactElement) => page; +Login.getLayout = ({ children }) => children; Login.Anonymous = true; export default Login; diff --git a/pages/auth/token/[token].tsx b/pages/auth/token/[token].tsx index 837e1b70..3bab0053 100644 --- a/pages/auth/token/[token].tsx +++ b/pages/auth/token/[token].tsx @@ -21,7 +21,7 @@ const Token: NextPageWithProps = () => { return <>; }; -Token.getLayout = (page: ReactElement) => page; +Token.getLayout = ({ children }) => children; Token.Anonymous = true; export default Token; From a8c3003cd3702df6010c06980f3641964c1ea9fc Mon Sep 17 00:00:00 2001 From: Johnson Mao Date: Sun, 10 Nov 2024 11:20:33 +0800 Subject: [PATCH 07/11] refactor(room): rooms page and style --- components/rooms/RoomsList.tsx | 24 --- components/shared/Tabs/Tabs.tsx | 8 +- features/room/components/JoinLockRoomForm.tsx | 10 +- features/room/components/RoomCard.tsx | 15 +- features/room/components/RoomList.tsx | 140 ------------------ features/room/components/index.ts | 1 - features/room/hooks/index.ts | 1 + features/room/hooks/useJoinRoom.ts | 51 +++++++ features/room/index.ts | 1 + pages/rooms/index.tsx | 73 ++++++++- 10 files changed, 143 insertions(+), 181 deletions(-) delete mode 100644 components/rooms/RoomsList.tsx delete mode 100644 features/room/components/RoomList.tsx create mode 100644 features/room/hooks/index.ts create mode 100644 features/room/hooks/useJoinRoom.ts diff --git a/components/rooms/RoomsList.tsx b/components/rooms/RoomsList.tsx deleted file mode 100644 index 22340ef9..00000000 --- a/components/rooms/RoomsList.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { cn } from "@/lib/utils"; - -type RoomsListProps = { - className?: string; - children: React.ReactNode; -}; - -export const RoomsListWrapper = ({ className, children }: RoomsListProps) => { - const listClass = cn( - "rooms__list__wrapper", - "grid grid-cols-3 gap-5 my-5", - className - ); - - return

{children}
; -}; - -export const RoomsList = ({ className, children }: RoomsListProps) => { - return ( -
- {children} -
- ); -}; diff --git a/components/shared/Tabs/Tabs.tsx b/components/shared/Tabs/Tabs.tsx index 90dc5616..f11e6420 100644 --- a/components/shared/Tabs/Tabs.tsx +++ b/components/shared/Tabs/Tabs.tsx @@ -1,4 +1,4 @@ -import { useState, Key, ReactNode } from "react"; +import { useState, Key, FC } from "react"; import Tab, { TabProps } from "./Tab"; export type TabItemType = TabProps; @@ -15,13 +15,13 @@ export interface TabsProps { /** * Function that recieved activeTabItem and render content of tabPane */ - renderTabPaneContent?: (tabItem: TabItemType) => ReactNode; + renderTabPaneContent: FC>; } export default function Tabs({ tabs, defaultActiveKey, - renderTabPaneContent, + renderTabPaneContent: TabPaneContent, }: Readonly>) { const [activeKey, setActiveKey] = useState( defaultActiveKey ?? tabs[0]?.tabKey @@ -47,7 +47,7 @@ export default function Tabs({ ))}
- {activeTabItem && renderTabPaneContent?.(activeTabItem)} + {activeTabItem && }
); diff --git a/features/room/components/JoinLockRoomForm.tsx b/features/room/components/JoinLockRoomForm.tsx index a0d8e218..fb7953f5 100644 --- a/features/room/components/JoinLockRoomForm.tsx +++ b/features/room/components/JoinLockRoomForm.tsx @@ -1,15 +1,15 @@ import { FormEvent, PropsWithChildren, useState } from "react"; -import { useTranslation } from "react-i18next"; import { Button } from "@/components/shared/Button/v2"; import Icon from "@/components/shared/Icon"; import InputOTP from "@/components/shared/InputOTP"; +import { useJoinRoom } from "../hooks"; interface JoinLockRoomFormProps extends PropsWithChildren { - onSubmit: (password: string) => Promise; + id: string; } -function JoinLockRoomForm({ children, onSubmit }: JoinLockRoomFormProps) { - const { t } = useTranslation("rooms"); +function JoinLockRoomForm({ id, children }: JoinLockRoomFormProps) { + const { handleJoinRoom } = useJoinRoom(id); const [password, setPassword] = useState(""); const [errorMessage, setErrorMessage] = useState(""); @@ -22,7 +22,7 @@ function JoinLockRoomForm({ children, onSubmit }: JoinLockRoomFormProps) { event.preventDefault(); setErrorMessage(""); try { - await onSubmit(password); + await handleJoinRoom(password); } catch (error) { if (typeof error === "string") { setErrorMessage(error); diff --git a/features/room/components/RoomCard.tsx b/features/room/components/RoomCard.tsx index b202bd32..40a1894b 100644 --- a/features/room/components/RoomCard.tsx +++ b/features/room/components/RoomCard.tsx @@ -2,16 +2,23 @@ import type { Room } from "@/requests/rooms"; import Cover from "@/components/shared/Cover"; import Button, { ButtonSize } from "@/components/shared/Button/v2"; +import { useJoinRoom } from "../hooks"; interface RoomsCardProps { room: Room; - className?: string; - onJoin: (id: string) => void; + onClick: () => void; } -function RoomCard({ room, onJoin: onClick }: RoomsCardProps) { +function RoomCard({ room, onClick }: RoomsCardProps) { + const { handleJoinRoom } = useJoinRoom(room.id); const lackTotalPlayers = room.maxPlayers - room.currentPlayers; + const handleClick = () => { + onClick(); + if (room.isLocked) return; + handleJoinRoom(); + }; + return (
@@ -41,7 +48,7 @@ function RoomCard({ room, onJoin: onClick }: RoomsCardProps) { diff --git a/features/room/components/RoomList.tsx b/features/room/components/RoomList.tsx deleted file mode 100644 index 6c002230..00000000 --- a/features/room/components/RoomList.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import { useState, useEffect, useMemo, useCallback } from "react"; -import { useRouter } from "next/router"; -import { AxiosError } from "axios"; -import { useTranslation } from "next-i18next"; - -import { - RoomType, - getRooms, - postRoomEntry, - getRoomInfoEndpoint, -} from "@/requests/rooms"; -import { JoinLockRoomForm, RoomCard } from "@/features/room"; -import useRequest from "@/hooks/useRequest"; -import usePagination from "@/hooks/usePagination"; -import useUser from "@/hooks/useUser"; -import { useGameList } from "@/features/game"; -import Modal from "@/components/shared/Modal"; -import Cover from "@/components/shared/Cover"; -import { useToast } from "@/components/shared/Toast"; - -interface RoomListProps { - tabKey: RoomType; -} - -function RoomList({ tabKey }: RoomListProps) { - const { t } = useTranslation("rooms"); - const { fetch } = useRequest(); - const { updateRoomId } = useUser(); - const toast = useToast(); - const router = useRouter(); - const { data } = usePagination({ - source: (page: number, perPage: number) => - fetch(getRooms({ page, perPage, status: tabKey })), - defaultPerPage: 20, - }); - const gameList = useGameList(); - const rooms = useMemo( - () => - Array.isArray(data) - ? data.map((room) => ({ - ...room, - game: { - ...room.game, - imgUrl: - gameList.find((game) => game.id === room.game.id)?.img || "", - }, - })) - : [], - [data, gameList] - ); - const [roomId, setRoomId] = useState(null); - const [isLoading, setIsLoading] = useState(false); - const room = useMemo( - () => rooms.find((room) => room.id === roomId), - [roomId, rooms] - ); - const isLocked = room?.isLocked; - const isOpenPasswordModel = !!isLocked && !!roomId; - - const handleClose = () => { - setRoomId(null); - setIsLoading(false); - }; - - const fetchRoomEntry = useCallback( - async (password: string | null = null) => { - if (!roomId) return; - - setIsLoading(true); - - // Automatically enter the room if room information is accessible, - // indicating the user is already in the room - if (await fetch(getRoomInfoEndpoint(roomId)).catch(() => {})) { - router.push(`/rooms/${roomId}`); - updateRoomId(roomId); - return; - } - - try { - await fetch(postRoomEntry(roomId, password)); - router.push(`/rooms/${roomId}`); - updateRoomId(roomId); - handleClose(); - } catch (err) { - if (err instanceof AxiosError) { - const msg = err.response?.data.message.replaceAll(" ", "_"); - if (!msg) return toast({ children: "error!", state: "error" }); - toast({ children: t(msg), state: "error" }); - setIsLoading(false); - return Promise.reject(t(msg)); - } - } - }, - [roomId, fetch, router, t, toast, updateRoomId] - ); - - useEffect(() => { - if (isLoading || isLocked) return; - fetchRoomEntry(); - }, [isLoading, isLocked, fetchRoomEntry]); - - return ( - <> -
    - {rooms.map((_room) => ( -
  • - -
  • - ))} -
- - - - {room && ( -
- -
-

{room.game.name}

-

{room.name}

-
-
- )} -
-
- - ); -} - -export default RoomList; diff --git a/features/room/components/index.ts b/features/room/components/index.ts index f8fcbdef..fb519b7a 100644 --- a/features/room/components/index.ts +++ b/features/room/components/index.ts @@ -2,4 +2,3 @@ export { default as CreateRoomForm } from "./CreateRoomForm"; export { default as GameRoomActions } from "./GameRoomActions"; export { default as JoinLockRoomForm } from "./JoinLockRoomForm"; export { default as RoomCard } from "./RoomCard"; -export { default as RoomList } from "./RoomList"; diff --git a/features/room/hooks/index.ts b/features/room/hooks/index.ts new file mode 100644 index 00000000..03010cac --- /dev/null +++ b/features/room/hooks/index.ts @@ -0,0 +1 @@ +export { default as useJoinRoom } from "./useJoinRoom"; diff --git a/features/room/hooks/useJoinRoom.ts b/features/room/hooks/useJoinRoom.ts new file mode 100644 index 00000000..4019447a --- /dev/null +++ b/features/room/hooks/useJoinRoom.ts @@ -0,0 +1,51 @@ +import { useState } from "react"; +import { useRouter } from "next/router"; +import { AxiosError } from "axios"; +import { useTranslation } from "next-i18next"; + +import { postRoomEntry, getRoomInfoEndpoint } from "@/requests/rooms"; +import useRequest from "@/hooks/useRequest"; +import useUser from "@/hooks/useUser"; +import { useToast } from "@/components/shared/Toast"; + +function useJoinRoom(id: string) { + const { t } = useTranslation("rooms"); + const { fetch } = useRequest(); + const { updateRoomId } = useUser(); + const toast = useToast(); + const router = useRouter(); + const [isLoading, setIsLoading] = useState(false); + + const handleJoinRoom = async (password: string | null = null) => { + if (isLoading) return; + + setIsLoading(true); + + // Automatically enter the room if room information is accessible, + // indicating the user is already in the room + if (await fetch(getRoomInfoEndpoint(id)).catch(() => {})) { + router.push(`/rooms/${id}`); + updateRoomId(id); + return; + } + + try { + await fetch(postRoomEntry(id, password)); + router.push(`/rooms/${id}`); + updateRoomId(id); + } catch (err) { + if (err instanceof AxiosError) { + const msg = err.response?.data.message.replaceAll(" ", "_"); + if (!msg) return toast({ children: "error!", state: "error" }); + toast({ children: t(msg), state: "error" }); + return Promise.reject(t(msg)); + } + } finally { + setIsLoading(false); + } + }; + + return { handleJoinRoom }; +} + +export default useJoinRoom; diff --git a/features/room/index.ts b/features/room/index.ts index 40b494c5..a234113b 100644 --- a/features/room/index.ts +++ b/features/room/index.ts @@ -1 +1,2 @@ export * from "./components"; +export * from "./hooks"; diff --git a/pages/rooms/index.tsx b/pages/rooms/index.tsx index 83fd9711..e783c1ef 100644 --- a/pages/rooms/index.tsx +++ b/pages/rooms/index.tsx @@ -1,9 +1,76 @@ +import { useState } from "react"; import { GetStaticProps } from "next"; import { useTranslation } from "next-i18next"; import { serverSideTranslations } from "next-i18next/serverSideTranslations"; -import { RoomType } from "@/requests/rooms"; -import { RoomList } from "@/features/room"; +import Cover from "@/components/shared/Cover"; +import Modal from "@/components/shared/Modal"; import Tabs, { TabItemType } from "@/components/shared/Tabs"; +import { useGameList } from "@/features/game"; +import { JoinLockRoomForm, RoomCard } from "@/features/room"; +import usePagination from "@/hooks/usePagination"; +import useRequest from "@/hooks/useRequest"; +import { Room, RoomType, getRooms } from "@/requests/rooms"; + +function TabPaneContent({ tabKey }: TabItemType) { + const { fetch } = useRequest(); + const gameList = useGameList(); + const [room, setRoom] = useState(null); + + const { data, loading } = usePagination({ + source: (page: number, perPage: number) => + fetch(getRooms({ page, perPage, status: tabKey })), + defaultPerPage: 20, + }); + + if (loading) return
Loading...
; + + return ( + <> +
    + {Array.isArray(data) && + data + .map((room) => ({ + ...room, + game: { + ...room.game, + imgUrl: + gameList.find((game) => game.id === room.game.id)?.img || "", + }, + })) + .map((room) => ( +
  • + setRoom(room)} /> +
  • + ))} +
+ + setRoom(null)} + size="medium" + > + {room && ( + +
+ +
+

{room.game.name}

+

{room.name}

+
+
+
+ )} +
+ + ); +} const Rooms = () => { const { t } = useTranslation("rooms"); @@ -24,7 +91,7 @@ const Rooms = () => {
); From aa9ae0aff3e455d3e236ebc13dac26bf3d05869a Mon Sep 17 00:00:00 2001 From: Johnson Mao Date: Sun, 10 Nov 2024 19:19:16 +0800 Subject: [PATCH 08/11] feat(icon): add icon and modify script --- assets/icons/host.svg | 45 +---- assets/icons/logo.svg | 13 -- assets/icons/user-host.svg | 5 + assets/icons/user.svg | 44 +---- assets/icons/user_host.svg | 43 ----- .../Icon/icons/generate/bright-crown.tsx | 4 +- .../shared/Icon/icons/generate/edit-mode.tsx | 2 +- .../shared/Icon/icons/generate/host.tsx | 147 +--------------- .../shared/Icon/icons/generate/linkedin.tsx | 2 +- .../shared/Icon/icons/generate/logo.tsx | 47 ----- .../shared/Icon/icons/generate/user-host.tsx | 38 +++++ .../shared/Icon/icons/generate/user.tsx | 153 ++--------------- .../shared/Icon/icons/generate/user_host.tsx | 161 ------------------ components/shared/Icon/icons/index.ts | 6 +- features/user/components/UserCard.tsx | 65 +++++++ .../{UserInfoForm => }/UserInfoForm.tsx | 0 .../user/components/UserInfoForm/index.ts | 1 - features/user/components/index.ts | 2 + features/user/index.ts | 2 +- scripts/iconConverter/index.ts | 2 +- 20 files changed, 146 insertions(+), 636 deletions(-) delete mode 100644 assets/icons/logo.svg create mode 100644 assets/icons/user-host.svg delete mode 100644 assets/icons/user_host.svg delete mode 100644 components/shared/Icon/icons/generate/logo.tsx create mode 100644 components/shared/Icon/icons/generate/user-host.tsx delete mode 100644 components/shared/Icon/icons/generate/user_host.tsx create mode 100644 features/user/components/UserCard.tsx rename features/user/components/{UserInfoForm => }/UserInfoForm.tsx (100%) delete mode 100644 features/user/components/UserInfoForm/index.ts create mode 100644 features/user/components/index.ts diff --git a/assets/icons/host.svg b/assets/icons/host.svg index b048c23c..ecbe3c50 100644 --- a/assets/icons/host.svg +++ b/assets/icons/host.svg @@ -1,44 +1,3 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + diff --git a/assets/icons/logo.svg b/assets/icons/logo.svg deleted file mode 100644 index fb277fd7..00000000 --- a/assets/icons/logo.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - diff --git a/assets/icons/user-host.svg b/assets/icons/user-host.svg new file mode 100644 index 00000000..528e4ee6 --- /dev/null +++ b/assets/icons/user-host.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/user.svg b/assets/icons/user.svg index 4aa94beb..39d5e860 100644 --- a/assets/icons/user.svg +++ b/assets/icons/user.svg @@ -1,42 +1,4 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + diff --git a/assets/icons/user_host.svg b/assets/icons/user_host.svg deleted file mode 100644 index 86c9fdf5..00000000 --- a/assets/icons/user_host.svg +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/components/shared/Icon/icons/generate/bright-crown.tsx b/components/shared/Icon/icons/generate/bright-crown.tsx index 81c04a9f..eca50bd9 100644 --- a/components/shared/Icon/icons/generate/bright-crown.tsx +++ b/components/shared/Icon/icons/generate/bright-crown.tsx @@ -22,7 +22,7 @@ export default function BrightCrown({ className }: Readonly) { width="24" height="24" > - + ) { - + diff --git a/components/shared/Icon/icons/generate/edit-mode.tsx b/components/shared/Icon/icons/generate/edit-mode.tsx index bc06f198..b50534c9 100644 --- a/components/shared/Icon/icons/generate/edit-mode.tsx +++ b/components/shared/Icon/icons/generate/edit-mode.tsx @@ -44,7 +44,7 @@ export default function EditMode({ className }: Readonly) { - + diff --git a/components/shared/Icon/icons/generate/host.tsx b/components/shared/Icon/icons/generate/host.tsx index 1e191e7f..c98f30dd 100644 --- a/components/shared/Icon/icons/generate/host.tsx +++ b/components/shared/Icon/icons/generate/host.tsx @@ -8,148 +8,17 @@ export default function Host({ className }: Readonly) { return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + ); } diff --git a/components/shared/Icon/icons/generate/linkedin.tsx b/components/shared/Icon/icons/generate/linkedin.tsx index 2b78af9a..6650acbb 100644 --- a/components/shared/Icon/icons/generate/linkedin.tsx +++ b/components/shared/Icon/icons/generate/linkedin.tsx @@ -18,7 +18,7 @@ export default function Linkedin({ className }: Readonly) { > ); diff --git a/components/shared/Icon/icons/generate/logo.tsx b/components/shared/Icon/icons/generate/logo.tsx deleted file mode 100644 index c5c406ee..00000000 --- a/components/shared/Icon/icons/generate/logo.tsx +++ /dev/null @@ -1,47 +0,0 @@ -/** This file is auto-generated by icon-convert script. Do not modify directly as changes may be overwritten. */ - -type LogoProps = { - className?: string; -}; - -export default function Logo({ className }: Readonly) { - return ( - - - - - - - - - - - - - - ); -} diff --git a/components/shared/Icon/icons/generate/user-host.tsx b/components/shared/Icon/icons/generate/user-host.tsx new file mode 100644 index 00000000..1f96fff7 --- /dev/null +++ b/components/shared/Icon/icons/generate/user-host.tsx @@ -0,0 +1,38 @@ +/** This file is auto-generated by icon-convert script. Do not modify directly as changes may be overwritten. */ + +type UserHostProps = { + className?: string; +}; + +export default function UserHost({ className }: Readonly) { + return ( + + + + + + ); +} diff --git a/components/shared/Icon/icons/generate/user.tsx b/components/shared/Icon/icons/generate/user.tsx index c2534db5..fdc51299 100644 --- a/components/shared/Icon/icons/generate/user.tsx +++ b/components/shared/Icon/icons/generate/user.tsx @@ -8,147 +8,24 @@ export default function User({ className }: Readonly) { return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + ); } diff --git a/components/shared/Icon/icons/generate/user_host.tsx b/components/shared/Icon/icons/generate/user_host.tsx deleted file mode 100644 index 2988a0de..00000000 --- a/components/shared/Icon/icons/generate/user_host.tsx +++ /dev/null @@ -1,161 +0,0 @@ -/** This file is auto-generated by icon-convert script. Do not modify directly as changes may be overwritten. */ - -type User_hostProps = { - className?: string; -}; - -export default function User_host({ className }: Readonly) { - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); -} diff --git a/components/shared/Icon/icons/index.ts b/components/shared/Icon/icons/index.ts index 64155418..92d70d5c 100644 --- a/components/shared/Icon/icons/index.ts +++ b/components/shared/Icon/icons/index.ts @@ -29,7 +29,6 @@ export { default as House } from "./generate/house"; export { default as LeaveGame } from "./generate/leave-game"; export { default as Linkedin } from "./generate/linkedin"; export { default as LogOut } from "./generate/log-out"; -export { default as Logo } from "./generate/logo"; export { default as LongArrowUpLeft } from "./generate/long-arrow-up-left"; export { default as Menu } from "./generate/menu"; export { default as Move } from "./generate/move"; @@ -53,8 +52,8 @@ export { default as Sort } from "./generate/sort"; export { default as Spiral } from "./generate/spiral"; export { default as Star } from "./generate/star"; export { default as Upload } from "./generate/upload"; +export { default as UserHost } from "./generate/user-host"; export { default as User } from "./generate/user"; -export { default as User_host } from "./generate/user_host"; export { default as X } from "./generate/x"; export type IconName = | "Arcade" @@ -87,7 +86,6 @@ export type IconName = | "LeaveGame" | "Linkedin" | "LogOut" - | "Logo" | "LongArrowUpLeft" | "Menu" | "Move" @@ -111,6 +109,6 @@ export type IconName = | "Spiral" | "Star" | "Upload" + | "UserHost" | "User" - | "User_host" | "X"; diff --git a/features/user/components/UserCard.tsx b/features/user/components/UserCard.tsx new file mode 100644 index 00000000..aefadc2c --- /dev/null +++ b/features/user/components/UserCard.tsx @@ -0,0 +1,65 @@ +import { cn } from "@/lib/utils"; +import Icon, { IconName } from "@/components/shared/Icon"; +import BoxFancy from "@/components/shared/BoxFancy"; + +interface UserCardProps { + id?: string; + nickname?: string; + isSelf?: boolean; + isHost?: boolean; +} + +interface IUserRole { + icon: IconName; + text: string; +} + +function UserCard({ id, nickname, isSelf, isHost }: UserCardProps) { + if (!id) { + return ; + } + + const getUserRole = (): IUserRole | undefined => { + if (isSelf && isHost) return { icon: "UserHost", text: "你 ( 房主 )" }; + if (isSelf) return { icon: "User", text: "你" }; + if (isHost) return { icon: "Host", text: "房主" }; + }; + + const userRole = getUserRole(); + + return ( +
+
+
+

非凡之人

+

{nickname}

+
+
+ {userRole && ( +
+
+ {userRole.text} +
+ +
+ )} +
+ ); +} + +export default UserCard; diff --git a/features/user/components/UserInfoForm/UserInfoForm.tsx b/features/user/components/UserInfoForm.tsx similarity index 100% rename from features/user/components/UserInfoForm/UserInfoForm.tsx rename to features/user/components/UserInfoForm.tsx diff --git a/features/user/components/UserInfoForm/index.ts b/features/user/components/UserInfoForm/index.ts deleted file mode 100644 index 6f65365a..00000000 --- a/features/user/components/UserInfoForm/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./UserInfoForm"; diff --git a/features/user/components/index.ts b/features/user/components/index.ts new file mode 100644 index 00000000..458b7ebc --- /dev/null +++ b/features/user/components/index.ts @@ -0,0 +1,2 @@ +export { default as UserCard } from "./UserCard"; +export { default as UserInfoForm } from "./UserInfoForm"; diff --git a/features/user/index.ts b/features/user/index.ts index 008beb04..40b494c5 100644 --- a/features/user/index.ts +++ b/features/user/index.ts @@ -1 +1 @@ -export { default as UserInfoForm } from "./components/UserInfoForm"; +export * from "./components"; diff --git a/scripts/iconConverter/index.ts b/scripts/iconConverter/index.ts index 4513351f..3917d1fa 100644 --- a/scripts/iconConverter/index.ts +++ b/scripts/iconConverter/index.ts @@ -65,7 +65,7 @@ const convertHtmlToJsx = (htmlString: string) => { let iconString = iconTemplate .replace("replace-attributes", wrapperAttributes) .replace("replace-content", convertHtmlToJsx(content)) - .replace(/black/g, "currentColor") + .replace(/(black|white)/g, "currentColor") .replace(/IconName/g, upperCaseFileName); iconString = `/** This file is auto-generated by icon-convert script. Do not modify directly as changes may be overwritten. */\n\n${iconString}`; From cd273b36348cf0e1ac494192487a974c1dc113ef Mon Sep 17 00:00:00 2001 From: Johnson Mao Date: Sun, 10 Nov 2024 19:21:15 +0800 Subject: [PATCH 09/11] refactor(room): game room style and join room logic --- components/rooms/RoomBreadcrumb.tsx | 23 ------ components/rooms/RoomButtonGroup.tsx | 21 ++--- .../RoomUserCardList/RoomUserCardList.tsx | 53 +++++-------- .../UserCard/UserCard.test.tsx | 77 ------------------- .../RoomUserCardList/UserCard/UserCard.tsx | 73 ------------------ components/shared/BoxFancy/BoxFancy.tsx | 7 +- components/shared/Breadcrumb/Breadcrumb.tsx | 29 +++---- .../shared/Breadcrumb/BreadcrumbItem.tsx | 14 +--- lib/utils.ts | 4 + pages/rooms/[roomId]/index.tsx | 31 ++++++-- requests/rooms/index.ts | 4 +- tailwind.config.js | 14 +++- 12 files changed, 91 insertions(+), 259 deletions(-) delete mode 100644 components/rooms/RoomBreadcrumb.tsx delete mode 100644 components/rooms/RoomUserCardList/UserCard/UserCard.test.tsx delete mode 100644 components/rooms/RoomUserCardList/UserCard/UserCard.tsx diff --git a/components/rooms/RoomBreadcrumb.tsx b/components/rooms/RoomBreadcrumb.tsx deleted file mode 100644 index e3ad1374..00000000 --- a/components/rooms/RoomBreadcrumb.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import Breadcrumb from "@/components/shared/Breadcrumb"; -import { RoomInfo } from "@/requests/rooms"; - -type RoomBreadcrumbType = { - roomInfo: RoomInfo.Room; -}; - -function RoomBreadcrumb({ roomInfo }: RoomBreadcrumbType) { - const isPublicText = (roomInfo.isLocked ? "非公開" : "公開") + "遊戲房間"; - const maxPlayerText = `${roomInfo.maxPlayers}人房`; - const statusText = - roomInfo.status === "WAITING" ? "等待玩家中" : "遊戲進行中"; - - const combinedText = roomInfo.name + "-" + maxPlayerText + "-" + statusText; - return ( - - - - - ); -} - -export default RoomBreadcrumb; diff --git a/components/rooms/RoomButtonGroup.tsx b/components/rooms/RoomButtonGroup.tsx index de81896e..40b470f0 100644 --- a/components/rooms/RoomButtonGroup.tsx +++ b/components/rooms/RoomButtonGroup.tsx @@ -1,4 +1,5 @@ import Button from "@/components/shared/Button/v2"; +import Icon from "../shared/Icon"; type RoomButtonGroupProps = { isHost: boolean; @@ -10,31 +11,23 @@ type RoomButtonGroupProps = { }; function RoomButtonGroup(props: RoomButtonGroupProps) { - const { - onToggleReady, - onClickLeave, - onClickClose, - onClickStart, - isHost, - isReady, - } = props; + const { onClickLeave, onClickClose, onClickStart, isHost } = props; return (
- {isHost ? ( - - ) : ( - )}
); diff --git a/components/rooms/RoomUserCardList/RoomUserCardList.tsx b/components/rooms/RoomUserCardList/RoomUserCardList.tsx index 4a759429..116f9bd3 100644 --- a/components/rooms/RoomUserCardList/RoomUserCardList.tsx +++ b/components/rooms/RoomUserCardList/RoomUserCardList.tsx @@ -1,7 +1,6 @@ import { RoomInfo } from "@/requests/rooms"; -import UserCard, { UserCardProps } from "./UserCard/UserCard"; - -const SEAT_AMOUNT = 10; +import { UserCard } from "@/features/user"; +import { generateUUID } from "@/lib/utils"; type RoomUserCardListProps = { roomInfo: RoomInfo.Room; @@ -14,40 +13,26 @@ function RoomUserCardList({ currentUserId, onKickUser, }: RoomUserCardListProps) { - function renderUserCards(users: RoomInfo.User[]) { - const userCount = users.length; - - const haveRightToKick = (userId: string) => - currentUserId === roomInfo.host.id && currentUserId !== userId; - - const userCards = users.map((user) => { - const props: UserCardProps = { - id: user.id, - nickname: user.nickname, - isReady: user.isReady, - isSelf: user.id === currentUserId, - isHost: user.id === roomInfo.host.id, - onKickUser: haveRightToKick(user.id) ? onKickUser : undefined, - }; - return ; - }); - - // render rest seats - const emptyCards = Array.from({ - length: SEAT_AMOUNT - userCount, - }).map((_, index) => { - // render wating seat - if (userCount + index < roomInfo.maxPlayers) - return ; - // render disabled seat - return ; - }); + const players = Array.isArray(roomInfo.players) ? roomInfo.players : []; + const lackTotalPlayers = Array.from( + { length: roomInfo.maxPlayers - players.length }, + generateUUID + ); - return [...userCards, ...emptyCards]; - } return (
- {renderUserCards(roomInfo.players)} + {players.map((player) => ( + + ))} + {lackTotalPlayers.map((id) => ( + + ))}
); } diff --git a/components/rooms/RoomUserCardList/UserCard/UserCard.test.tsx b/components/rooms/RoomUserCardList/UserCard/UserCard.test.tsx deleted file mode 100644 index a92bb534..00000000 --- a/components/rooms/RoomUserCardList/UserCard/UserCard.test.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import React from "react"; -import { render, screen } from "@testing-library/react"; -import "@testing-library/jest-dom"; -import UserCard, { UserCardProps } from "./UserCard"; - -describe("UserCard", () => { - describe("Available UserCard", () => { - it("should render with waiting text", () => { - const userCard = render(); - - expect(userCard.baseElement).toHaveTextContent("等待中..."); - }); - }); - - describe("Disabled UserCard", () => { - it("should render with empty textContent", () => { - const userCard = render(); - - expect(userCard.baseElement).toHaveTextContent(""); - }); - }); - - describe("UserCard with user", () => { - const UserProp: UserCardProps = { - id: "testId", - nickname: "testNickname", - isReady: false, - isSelf: false, - isHost: false, - }; - - it("should render with correct nickname", () => { - render(); - - expect(screen.getByText(UserProp.nickname)).toBeInTheDocument(); - }); - - it("should render with ready status text when user is ready", () => { - const userCard = render(); - - expect(userCard.getByText("已準備")).toBeInTheDocument(); - }); - - it("should render text with 'you' when it represents user self", () => { - const userCard = render(); - - expect(userCard.baseElement.textContent).toMatch(/你/); - expect(userCard.baseElement.textContent).not.toMatch(/mynickname/); - }); - - it("should render host suffix text when user is host ", () => { - const userCard = render(); - - expect(userCard.baseElement.textContent).toMatch(/(房主)/); - }); - - it("should render with correct nickname when it represents both of user self and host", () => { - const userCard = render(); - - expect(userCard.baseElement.textContent).toMatch(/你/); - expect(userCard.baseElement.textContent).toMatch(/(房主)/); - expect(userCard.baseElement.textContent).not.toMatch(/mynickname/); - }); - - it("shouldn't render kick button on right top when not recived onKickUser prop", () => { - render(); - - expect(screen.queryByTestId("kick-user-svg")).toBeFalsy(); - }); - - // it("shouldn render kick button on right top when recived onKickUser prop", () => { - // render( {}} />); - - // expect(screen.queryByTestId("kick-user-svg")).not.toBeFalsy(); - // }); - }); -}); diff --git a/components/rooms/RoomUserCardList/UserCard/UserCard.tsx b/components/rooms/RoomUserCardList/UserCard/UserCard.tsx deleted file mode 100644 index 81ed330b..00000000 --- a/components/rooms/RoomUserCardList/UserCard/UserCard.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { cn } from "@/lib/utils"; -import Icon from "@/components/shared/Icon"; -import { ReactElement } from "react"; -import { RoomInfo } from "@/requests/rooms"; -export interface UserCardProps { - id: string; - nickname: string; - isReady: boolean; - isSelf: boolean; - isHost: boolean; - onKickUser?: (User: Omit) => void; -} -interface WatingUserCardProp { - isWating: boolean; -} -interface DisabledUserCardProp { - disabled: boolean; -} -function UserCard(props: WatingUserCardProp): ReactElement; -function UserCard(props: DisabledUserCardProp): ReactElement; -function UserCard(props: UserCardProps): ReactElement; -function UserCard(props: any) { - const { id, nickname, isReady, isSelf, isHost, onKickUser }: UserCardProps = - props; - const { isWating }: WatingUserCardProp = props; - const { disabled }: DisabledUserCardProp = props; - - const hostClass = "border-[4px] border-[#23A55A]"; - - const disabledClass = "opacity-30 bg-black"; - - const readyContent = ( -
- 已準備 -
- ); - - function getNameText() { - if (disabled) return ""; - if (isWating) return "等待中..."; - const suffix = isHost ? "(房主)" : ""; - const name = isSelf ? "你" : nickname; - return name + suffix; - } - const nameText = getNameText(); - - return ( -
- {onKickUser && ( -
onKickUser({ id, nickname })} - data-testid="kick-user-svg" - className={"absolute top-[5px] right-[6px] cursor-pointer"} - > - -
- )} - - {isReady && readyContent} - - {nameText} - -
- ); -} - -export default UserCard; diff --git a/components/shared/BoxFancy/BoxFancy.tsx b/components/shared/BoxFancy/BoxFancy.tsx index e2a77fc9..fa19e9bf 100644 --- a/components/shared/BoxFancy/BoxFancy.tsx +++ b/components/shared/BoxFancy/BoxFancy.tsx @@ -10,7 +10,11 @@ export type BoxFancyBorderWidthVariant = | "xLarge" | "extraLarge"; export type BoxFancyBorderRadiusVariant = BoxFancyBorderWidthVariant | "full"; -export type BoxFancyBorderGradientVariant = "none" | "purple" | "black"; +export type BoxFancyBorderGradientVariant = + | "none" + | "purple" + | "black" + | "cyberpunk"; // Gradient border with semi-transparent background tips: // The border-radius of ::before should be as consistent as possible with the original, @@ -40,6 +44,7 @@ const borderGradientVariantMap: Record = none: "", purple: "before:gradient-purple", black: "before:gradient-black", + cyberpunk: "before:gradient-cyberpunk", }; export interface BaseBoxFancyProp { diff --git a/components/shared/Breadcrumb/Breadcrumb.tsx b/components/shared/Breadcrumb/Breadcrumb.tsx index a271deae..0a9d56df 100644 --- a/components/shared/Breadcrumb/Breadcrumb.tsx +++ b/components/shared/Breadcrumb/Breadcrumb.tsx @@ -1,6 +1,7 @@ -import React, { ReactNode } from "react"; +import { Children } from "react"; import { cn } from "@/lib/utils"; import BreadcrumbItem, { BreadcrumbItemProps } from "./BreadcrumbItem"; +import Icon from "../Icon"; export interface BreadcrumbProps { /** `Breadcrumb.Item` with text and href */ @@ -11,22 +12,22 @@ export interface BreadcrumbProps { className?: string; } +const defaultSeparator = ; + const Breadcrumb: React.FC & { Item: React.ComponentType; -} = ({ children, separator = ">", className }) => { - const rootClass = cn(`flex space-x-2 text-base`, className); - const childrenRender = React.Children.map(children, (child, index) => ( - <> - {child} - {index < React.Children.count(children) - 1 && ( - {separator} - )} - - )); - +} = ({ children, separator = defaultSeparator, className }) => { return ( -