From acd625b45f19c71b6e359668dac99b3caa2991de Mon Sep 17 00:00:00 2001
From: Johnson Mao <tutelary.maomao@gmail.com>
Date: Sun, 20 Oct 2024 14:22:20 +0800
Subject: [PATCH] refactor: website style

---
 components/rooms/GameWindow.tsx    |   8 +--
 components/shared/Chat/v2/Chat.tsx |   4 +-
 containers/layout/AppLayout.tsx    |  27 ++++++--
 hooks/useChat.ts                   |   7 +-
 hooks/useUser.ts                   |  17 +++--
 pages/_document.tsx                |   2 +-
 pages/auth/token/[token].tsx       |   2 +-
 pages/index.tsx                    |   8 +--
 pages/rooms/[roomId]/index.tsx     | 107 +++++++++++++++++++----------
 styles/global.css                  |   4 ++
 10 files changed, 123 insertions(+), 63 deletions(-)

diff --git a/components/rooms/GameWindow.tsx b/components/rooms/GameWindow.tsx
index bd666ea9..235ff274 100644
--- a/components/rooms/GameWindow.tsx
+++ b/components/rooms/GameWindow.tsx
@@ -1,14 +1,12 @@
 type GameWindowProps = {
+  className?: string;
   gameUrl: string;
 };
 
-export default function GameWindow({ gameUrl }: GameWindowProps) {
+export default function GameWindow({ className, gameUrl }: GameWindowProps) {
   return (
     <div>
-      <iframe
-        className="absolute inset-0 m-auto w-[95%] h-[95vh] border"
-        src={gameUrl}
-      >
+      <iframe className={className} src={gameUrl}>
         <p>Your browser does not support iframes.</p>
       </iframe>
     </div>
diff --git a/components/shared/Chat/v2/Chat.tsx b/components/shared/Chat/v2/Chat.tsx
index 3e4567ad..ec4faee8 100644
--- a/components/shared/Chat/v2/Chat.tsx
+++ b/components/shared/Chat/v2/Chat.tsx
@@ -15,6 +15,7 @@ export type ChatProps = {
   friendList: FriendType[];
   roomMessages: MessageType[];
   className?: string;
+  defaultTarget?: ChatTab["id"];
   onSubmit: (message: Pick<MessageType, "content" | "target">) => void;
 };
 
@@ -25,11 +26,12 @@ export default function Chat({
   friendList,
   roomMessages,
   className,
+  defaultTarget,
   onSubmit,
 }: Readonly<ChatProps>) {
   const [messages, setMessages] = useState(lobbyMessages);
   const [target, setTarget] = useState<[ChatTab["id"], string | null]>([
-    "lobby",
+    defaultTarget || "lobby",
     null,
   ]);
   const [activeTab, friendRoom] = target;
diff --git a/containers/layout/AppLayout.tsx b/containers/layout/AppLayout.tsx
index 3a1746d4..2b01e9a5 100644
--- a/containers/layout/AppLayout.tsx
+++ b/containers/layout/AppLayout.tsx
@@ -1,38 +1,51 @@
-import { PropsWithChildren } from "react";
+import { PropsWithChildren, useEffect } from "react";
+import { useRouter } from "next/router";
 import Header from "@/components/shared/Header";
 import Sidebar from "@/components/shared/Sidebar";
 import Chat from "@/components/shared/Chat/v2/Chat";
 import useChat from "@/hooks/useChat";
 
 export default function Layout({ children }: PropsWithChildren) {
+  const router = useRouter();
   const {
     roomId,
     messageList,
     isChatVisible,
+    openChat,
     toggleChatVisibility,
     handleSubmitText,
   } = useChat();
+  const roomPathname = "/rooms/[roomId]";
+
+  useEffect(() => {
+    if (router.pathname === roomPathname) {
+      openChat();
+    }
+  }, [router.pathname, openChat]);
 
   return (
     <>
       <Header
-        className="sticky top-0 z-40"
+        className="fixed top-0 inset-x-0 z-40"
         onClickChatButton={toggleChatVisibility}
       />
-      <div className="ml-2 mr-4 my-6 flex grow">
-        <div className="shrink-0">
-          <Sidebar className="sticky top-20 z-30 h-main-height" />
+      <div className="pl-2 pt-20 flex grow">
+        <div className="shrink-0 w-18">
+          <Sidebar className="fixed top-20 bottom-6 z-30" />
         </div>
         <main className="grow overflow-x-hidden">{children}</main>
         {isChatVisible && (
-          <div className="shrink-0">
+          <div className="shrink-0 w-80 mr-4">
             <Chat
-              className="sticky top-20 z-30 h-main-height"
+              className="fixed top-20 bottom-6 z-30"
               userId=""
               roomId={roomId}
               friendList={[]}
               lobbyMessages={[]}
               roomMessages={messageList}
+              defaultTarget={
+                router.pathname === roomPathname ? "room" : "lobby"
+              }
               onSubmit={handleSubmitText}
             />
           </div>
diff --git a/hooks/useChat.ts b/hooks/useChat.ts
index 7770dc1f..1e4614a9 100644
--- a/hooks/useChat.ts
+++ b/hooks/useChat.ts
@@ -1,4 +1,4 @@
-import { useEffect, useState } from "react";
+import { useCallback, useEffect, useState } from "react";
 import type { MessageType } from "@/components/shared/Chat/v2/ChatMessages";
 import useChatroom from "./context/useChatroom";
 import useSocketCore from "./context/useSocketCore";
@@ -17,6 +17,10 @@ export default function useChat() {
     setIsChatVisible((prev) => !prev);
   };
 
+  const openChat = useCallback(() => {
+    setIsChatVisible(true);
+  }, []);
+
   // join chatroom by roomId
   useEffect(() => {
     if (!roomId) return;
@@ -48,6 +52,7 @@ export default function useChat() {
     roomId,
     messageList,
     isChatVisible,
+    openChat,
     sendChatMessage,
     toggleChatVisibility,
     handleSubmitText,
diff --git a/hooks/useUser.ts b/hooks/useUser.ts
index 6fca55d4..5c654828 100644
--- a/hooks/useUser.ts
+++ b/hooks/useUser.ts
@@ -58,13 +58,16 @@ const useUser = () => {
     return roomIdOperator.get();
   };
 
-  const updateRoomId = (roomId?: string) => {
-    if (roomId) {
-      roomIdOperator.set(roomId);
-    } else {
-      roomIdOperator.remove();
-    }
-  };
+  const updateRoomId = useCallback(
+    (roomId?: string) => {
+      if (roomId) {
+        roomIdOperator.set(roomId);
+      } else {
+        roomIdOperator.remove();
+      }
+    },
+    [roomIdOperator]
+  );
 
   return {
     getLoginEndpoint,
diff --git a/pages/_document.tsx b/pages/_document.tsx
index 22632ceb..ed031e8b 100644
--- a/pages/_document.tsx
+++ b/pages/_document.tsx
@@ -12,7 +12,7 @@ export default function Document() {
         <meta name="og:title" content={siteTitle} />
         <meta name="twitter:card" content="summary_large_image" />
       </Head>
-      <body className="body-bg text-primary-200">
+      <body className="body-bg min-h-screen text-primary-200">
         <Main />
         <NextScript />
       </body>
diff --git a/pages/auth/token/[token].tsx b/pages/auth/token/[token].tsx
index 75531572..837e1b70 100644
--- a/pages/auth/token/[token].tsx
+++ b/pages/auth/token/[token].tsx
@@ -18,7 +18,7 @@ const Token: NextPageWithProps = () => {
     }
   }, [token, login, push]);
 
-  return <h1>{token}</h1>;
+  return <></>;
 };
 
 Token.getLayout = (page: ReactElement) => page;
diff --git a/pages/index.tsx b/pages/index.tsx
index 5f67e110..91ae05fb 100644
--- a/pages/index.tsx
+++ b/pages/index.tsx
@@ -71,7 +71,7 @@ function CarouselCard({
           draggable={false}
           priority
           fill
-          objectFit="cover"
+          className="object-cover"
           onError={onImageError}
         />
         <div className="m-4 flex gap-4">
@@ -106,7 +106,7 @@ function CarouselCard({
                   draggable={false}
                   priority
                   fill
-                  objectFit="cover"
+                  className="object-cover"
                   onError={onImageError}
                 />
               )}
@@ -181,12 +181,12 @@ const TabPaneContent = ({
             >
               <picture className="relative aspect-game-cover overflow-hidden">
                 <Image
-                  src={game.img}
+                  src={game.img || gameDefaultCoverImg.src}
                   alt={game.name}
                   draggable={false}
                   priority
                   fill
-                  objectFit="cover"
+                  className="object-cover"
                   onError={onImageError}
                 />
               </picture>
diff --git a/pages/rooms/[roomId]/index.tsx b/pages/rooms/[roomId]/index.tsx
index b0ae9c3f..f859f768 100644
--- a/pages/rooms/[roomId]/index.tsx
+++ b/pages/rooms/[roomId]/index.tsx
@@ -1,6 +1,7 @@
-import { useEffect, useState } from "react";
-import { useRouter } from "next/router";
+import { ReactEventHandler, useEffect, useState } from "react";
 import { GetStaticProps, GetStaticPaths } from "next";
+import { useRouter } from "next/router";
+import Image from "next/image";
 import { serverSideTranslations } from "next-i18next/serverSideTranslations";
 import RoomUserCardList from "@/components/rooms/RoomUserCardList";
 import RoomButtonGroup from "@/components/rooms/RoomButtonGroup";
@@ -22,10 +23,18 @@ import {
   playerCancelReady,
   startGame,
 } from "@/requests/rooms";
+import { GameType, getAllGamesEndpoint } from "@/requests/games";
 import useUser from "@/hooks/useUser";
+import gameDefaultCoverImg from "@/public/images/game-default-cover.png";
 
 type User = Omit<RoomInfo.User, "isReady">;
 
+const onImageError: ReactEventHandler<HTMLImageElement> = (e) => {
+  if (e.target instanceof HTMLImageElement) {
+    e.target.src = gameDefaultCoverImg.src;
+  }
+};
+
 export default function Room() {
   const {
     roomInfo,
@@ -44,16 +53,27 @@ export default function Room() {
   const { fetch } = useRequest();
   const { query, replace } = useRouter();
   const [gameUrl, setGameUrl] = useState("");
+  const [gameList, setGameList] = useState<GameType[]>([]);
   const roomId = query.roomId as string;
   const player = roomInfo.players.find(
     (player) => player.id === currentUser?.id
   );
   const isHost = roomInfo.host.id === currentUser?.id;
+  const gameInfo = gameList.find((game) => game.id === roomInfo.game.id);
+
+  useEffect(() => {
+    fetch(getAllGamesEndpoint()).then(setGameList);
+  }, [fetch]);
 
   useEffect(() => {
     async function getRoomInfo() {
-      const roomInfo = await fetch(getRoomInfoEndpoint(roomId));
-      initializeRoom(roomInfo);
+      try {
+        const roomInfo = await fetch(getRoomInfoEndpoint(roomId));
+        initializeRoom(roomInfo);
+      } catch (err) {
+        updateRoomId();
+        replace("/rooms");
+      }
     }
 
     getRoomInfo();
@@ -119,6 +139,7 @@ export default function Room() {
     socket,
     currentUser?.id,
     roomId,
+    updateRoomId,
     addPlayer,
     removePlayer,
     updateUserReadyStatus,
@@ -211,40 +232,54 @@ export default function Room() {
   };
 
   return (
-    <section className="px-6">
-      <div className="relative w-full h-[280px] overflow-hidden">
-        <img
-          className="absolute inset-0 w-full h-full object-cover"
-          src="https://images.unsplash.com/photo-1601987177651-8edfe6c20009?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2070&q=80"
-          alt="cover"
+    <section className="px-4">
+      {gameUrl ? (
+        <GameWindow
+          className="h-[calc(100dvh-104px)] w-full"
+          gameUrl={gameUrl}
         />
-        <div className="absolute top-0 left-0 right-0 bottom-0 bg-gradient-to-t from-[#0f0919] to-50% to-[#170D2500]"></div>
-        <div className="m-2 py-1 px-2 w-fit bg-gray-950/50 backdrop-blur-sm rounded-lg text-sm">
-          <RoomBreadcrumb roomInfo={roomInfo} />
-        </div>
-        <div className="m-2 py-1 px-2 w-fit bg-gray-950/50 backdrop-blur-sm rounded-lg text-sm">
-          {roomInfo.isLocked ? "非公開" : "公開"}
-        </div>
-        <div className="m-2 py-1 px-2 w-fit bg-gray-950/50 backdrop-blur-sm rounded-lg text-sm">
-          {roomInfo.currentPlayers} / {roomInfo.maxPlayers} 人
-        </div>
-        <div className="absolute bottom-0 right-0 flex items-center">
-          <RoomButtonGroup
-            onToggleReady={handleToggleReady}
-            onClickClose={handleClickClose}
-            onClickLeave={handleLeave}
-            onClickStart={handleStart}
-            isHost={isHost}
-            isReady={isHost || !!player?.isReady}
+      ) : (
+        <>
+          <div className="relative w-full h-[280px] overflow-hidden">
+            {roomInfo.currentPlayers && (
+              <Image
+                src={gameInfo?.img || gameDefaultCoverImg.src}
+                alt={gameInfo?.name || "default game cover"}
+                draggable={false}
+                priority
+                fill
+                className="object-cover"
+                onError={onImageError}
+              />
+            )}
+            <div className="absolute top-0 left-0 right-0 bottom-0 bg-gradient-to-t from-[#0f0919] to-50% to-[#170D2500]"></div>
+            <div className="m-2 py-1 px-2 w-fit bg-gray-950/50 backdrop-blur-sm rounded-lg text-sm">
+              <RoomBreadcrumb roomInfo={roomInfo} />
+            </div>
+            <div className="m-2 py-1 px-2 w-fit bg-gray-950/50 backdrop-blur-sm rounded-lg text-sm">
+              {roomInfo.isLocked ? "非公開" : "公開"}
+            </div>
+            <div className="m-2 py-1 px-2 w-fit bg-gray-950/50 backdrop-blur-sm rounded-lg text-sm">
+              {roomInfo.currentPlayers} / {roomInfo.maxPlayers} 人
+            </div>
+            <div className="absolute bottom-0 right-0 flex items-center">
+              <RoomButtonGroup
+                onToggleReady={handleToggleReady}
+                onClickClose={handleClickClose}
+                onClickLeave={handleLeave}
+                onClickStart={handleStart}
+                isHost={isHost}
+                isReady={isHost || !!player?.isReady}
+              />
+            </div>
+          </div>
+          <RoomUserCardList
+            roomInfo={roomInfo}
+            currentUserId={currentUser?.id}
+            onKickUser={handleClickKick}
           />
-        </div>
-      </div>
-      <RoomUserCardList
-        roomInfo={roomInfo}
-        currentUserId={currentUser?.id}
-        onKickUser={handleClickKick}
-      />
-      {gameUrl && <GameWindow gameUrl={gameUrl} />}
+        </>
+      )}
       <Popup />
     </section>
   );
diff --git a/styles/global.css b/styles/global.css
index b5c61c95..83cb701b 100644
--- a/styles/global.css
+++ b/styles/global.css
@@ -1,3 +1,7 @@
 @tailwind base;
 @tailwind components;
 @tailwind utilities;
+
+html {
+  color-scheme: dark;
+}