Repository: T0hsakaR1n126/sopra-fs25-group-10-client
Pull request #40: join game implement
Author: login: yzqmeowist
Created: 2025-04-06T20:28:56Z | Merged: 2025-04-07T06:36:11Z

## Achievement
* users can click on game list in the lobby to join an existing game
* users can enter password to get validated with a private game

## Layout
![image](https://github.com/user-attachments/assets/6fa3fc24-53d2-4bfd-b1dc-1947ea2d3b50)

Close #39

Linked issue #39: [Task 11.2] Game Join
As a player, I want to join the game by clicking on the items of game list.
* Design behaviors when users click on the games.
* Implement the REST API for notifying backend and the validation if the game has password.

Diff:
diff --git a/app/lobby/create/page.tsx b/app/lobby/create/page.tsx
index e5f87ed..dccff4a 100644
--- a/app/lobby/create/page.tsx
+++ b/app/lobby/create/page.tsx
@@ -1,12 +1,115 @@
-import React from 'react';
-
-const GuestPage: React.FC = () => {
-    return (
-        <div>
-            <h1>Welcome, Guest!</h1>
-            <p>Please sign in to access more features.</p>
-        </div>
-    );
+"use client";
+
+import React, { useState } from 'react';
+import styles from "@/styles/lobby.module.css";
+import { useApi } from '@/hooks/useApi';
+import { useRouter } from 'next/navigation';
+import { useSelector } from 'react-redux';
+import { Game } from '@/types/game';
+
+const CreateForm: React.FC = () => {
+  const apiService = useApi();
+  const router = useRouter();
+  const username = useSelector((state: { user: { username: string } }) => state.user.username)
+  const [isPrivate, setIsPrivate] = useState(false);
+  const [gameName, setGameName] = useState("");
+  const [password, setPassword] = useState("");
+  const [maxPlayers, setMaxPlayers] = useState("2");
+  const [duration, setDuration] = useState("5");
+
+  const handleCreateGame = async () => {
+    if (!gameName.trim()) {
+      alert("Game name cannot be empty!");
+      return;
+    }
+
+    const newGame: Game = {
+      gameName: gameName,
+      playersNumber: maxPlayers,
+      time: duration,
+      password: password,
+      owner: username,
+      gameId: null,
+      realPlayersNumber: null,
+      modeType: "combat" // placeholder
+    };
+
+    try {
+      const response = await apiService.post<Game>("/games", newGame);
+      console.log(JSON.stringify(response, null, 2));
+      if (response.gameId)  {
+        alert("Game created successfully!");
+        router.push("/game/1v1")
+      }
+    } catch (error) {
+      if (error instanceof Error) {
+        alert(`Something went wrong during game creation:\n${error.message}`);
+      } else {
+        console.error("An unknown error occurred during game creation.");
+      }
+    }
+  };
+
+  return (
+    <form className={styles.form} onSubmit={handleCreateGame}>
+      <h2 className={styles.createTitle}>Create Game Session</h2>
+      <label>
+        Game Name:
+        <input 
+          type="text" 
+          className={styles.input} 
+          onChange={(e) => setGameName(e.target.value)} 
+        />
+      </label>
+
+      <label>
+        Max Players:
+        <select className={styles.input} onChange={(e) => setMaxPlayers(e.target.value)}>
+          <option value="2">2</option>
+          <option value="3">3</option>
+          <option value="4">4</option>
+          <option value="5">5</option>
+        </select>
+      </label>
+
+      <label>
+        Duration:
+        <select className={styles.input} onChange={(e) => setDuration(e.target.value)}>
+          <option value="5">5 Minutes</option>
+          <option value="10">10 Minutes</option>
+          <option value="15">15 Minutes</option>
+          <option value="30">30 Minutes</option>
+        </select>
+      </label>
+
+      <div className={styles.inlineField}>
+        <span className={styles.labelText}>Private 🔒</span>
+
+        <label className={styles.switch}>
+          <input
+            type="checkbox"
+            checked={isPrivate}
+            onChange={(e) => setIsPrivate(e.target.checked)}
+          />
+          <span className={styles.slider}></span>
+        </label>
+      </div>
+
+      {isPrivate && (
+        <label>
+          Password:
+          <input
+            type="text"
+            className={styles.input}
+            value={password}
+            onChange={(e) => setPassword(e.target.value)}
+          />
+        </label>
+      )}
+
+      <button type="submit" className={styles.createButton}>Create</button>
+    </form>
+  );
 };
 
-export default GuestPage;
\ No newline at end of file
+export default CreateForm;
diff --git a/app/lobby/join/handleJoinGame.ts b/app/lobby/join/handleJoinGame.ts
new file mode 100644
index 0000000..9da54b7
--- /dev/null
+++ b/app/lobby/join/handleJoinGame.ts
@@ -0,0 +1,26 @@
+import { Game } from '@/types/game';
+import { ApiService } from '@/api/apiService';
+import { AppRouterInstance } from 'next/dist/shared/lib/app-router-context.shared-runtime';
+
+export const handleJoinGame = async (game: Game, userId: string, apiService: ApiService, router: AppRouterInstance): Promise<void> => {
+  if (game.password) {
+    const password = prompt("Enter the game password:");
+    if (password === null || password.trim() === "") {
+      return;
+    }
+    game.password = password;
+  } else {
+    game.password = "";
+  }
+  console.log(JSON.stringify(game, null, 2));
+  try {
+    await apiService.put<Game>(`/lobbyIn/${userId}`, game);
+    router.push(`/game/start/${game.gameId}`);
+  } catch (error) {
+    if (error instanceof Error) {
+      alert(`Something went wrong during game joining:\n${error.message}`);
+    } else {
+      console.error("An unknown error occurred during game joining.");
+    }
+  }
+};
diff --git a/app/lobby/join/page.tsx b/app/lobby/join/page.tsx
deleted file mode 100644
index e5f87ed..0000000
--- a/app/lobby/join/page.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import React from 'react';
-
-const GuestPage: React.FC = () => {
-    return (
-        <div>
-            <h1>Welcome, Guest!</h1>
-            <p>Please sign in to access more features.</p>
-        </div>
-    );
-};
-
-export default GuestPage;
\ No newline at end of file
diff --git a/app/lobby/page.tsx b/app/lobby/page.tsx
index 1f1926b..5b44efc 100644
--- a/app/lobby/page.tsx
+++ b/app/lobby/page.tsx
@@ -5,12 +5,16 @@ import { useApi } from "@/hooks/useApi";
 import { Game } from "@/types/game";
 import { useRouter } from "next/navigation";
 import styles from "@/styles/lobby.module.css";
+import CreateForm from './create/page';
+import { handleJoinGame } from './join/handleJoinGame';
+import { useSelector } from 'react-redux';
 
 const Lobby: React.FC = () => {
   const apiService = useApi();
   const router = useRouter();
   const [showSidebar, setShowSidebar] = useState(false);
   const [games, setGames] = useState<Game[]>([]);
+  const userId = useSelector((state: { user: { userId: string } }) => state.user.userId)
 
   // only for mock, remove when backend is ready
   const [currentPage, setCurrentPage] = useState(1);
@@ -33,22 +37,23 @@ const Lobby: React.FC = () => {
   // }, []);
 
   useEffect(() => {
-      const fetchGames = async () => {
-        try {
-          const response: Game[] = await apiService.get("/lobby");
-          setGames(response);
-        } catch (error) {
-          if (error instanceof Error) {
-            alert(`Something went wrong while fetching user:\n${error.message}`);
-            router.push("/game");
-          } else {
-            console.error("An unknown error occurred while fetching user.");
-          }
+    const fetchGames = async () => {
+      try {
+        const response: Game[] = await apiService.get("/lobby");
+        setGames(response);
+        console.log(JSON.stringify(response, null, 2));
+      } catch (error) {
+        if (error instanceof Error) {
+          alert(`Something went wrong while fetching user:\n${error.message}`);
+          router.push("/game");
+        } else {
+          console.error("An unknown error occurred while fetching user.");
         }
-      };
+      }
+    };
 
-      fetchGames();
-    }, [apiService]);
+    fetchGames();
+  }, [apiService]);
 
   return (
     <div className={styles.page}>
@@ -62,16 +67,20 @@ const Lobby: React.FC = () => {
           <div>Owner</div>
         </div>
 
-        {paginatedGames.map((lobby, idx) => (
-          <div key={idx} className={styles.lobbyCard}>
-            <div className={styles.teamName}>
-              {lobby.gameName}
-              {lobby.password !== null && <span title="Private game">🔒</span>}
+        {paginatedGames.length === 0 ? (
+          <div className={styles.emptyMessage}>No Available Game</div>
+        ) : (
+          paginatedGames.map((game, idx) => (
+            <div key={idx} className={styles.lobbyCard} onClick={() => handleJoinGame(game, userId, apiService, router)}>
+              <div className={styles.teamName}>
+                {game.gameName}
+                {game.password !== "" && <span title="Private game">🔒</span>}
+              </div>
+              <div>{game.realPlayersNumber} / {game.playersNumber}</div>
+              <div className={styles.ownerLink}>{game.owner}</div>
             </div>
-            <div>{lobby.playersNumber}</div>
-            <div className={styles.ownerLink}>{lobby.owner}</div>
-          </div>
-        ))}
+          ))
+        )}
         <div className={styles.pagination}>
           <button
             onClick={() => setCurrentPage((p) => Math.max(p - 1, 1))}
@@ -98,35 +107,7 @@ const Lobby: React.FC = () => {
 
       {/* Sidebar */}
       <div className={`${styles.rightPanel} ${showSidebar ? styles.show : ""}`}>
-        <h2 className={styles.createTitle}>Create Game Session</h2>
-        <form className={styles.form}>
-          <label>
-            Max Players:
-            <select className={styles.input}>
-              <option value="2">2</option>
-              <option value="3">3</option>
-              <option value="4">4</option>
-              <option value="5">5</option>
-            </select>
-          </label>
-
-          <label>
-            Duration:
-            <select className={styles.input}>
-              <option value="5">5 Minutes</option>
-              <option value="10">10 Minutes</option>
-              <option value="15">15 Minutes</option>
-              <option value="30">30 Minutes</option>
-            </select>
-          </label>
-
-          <label>
-            Password (optional):
-            <input type="text" className={styles.input} />
-          </label>
-
-          <button type="submit" className={styles.createButton}>Create</button>
-        </form>
+        <CreateForm />
       </div>
     </div>
   );
diff --git a/app/page.tsx b/app/page.tsx
index b3c243c..32b90e9 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -62,7 +62,7 @@ const HomePage = () => {
 
           {/* Play Button */}
           <div style={{ textAlign: 'center', marginTop: '30px' }}>
-            <Button type="primary" size="large" shape="circle" onClick={() => router.push("/game")}>
+            <Button type="primary" size="large" shape="circle" onClick={() => router.push("/users/login")}>
               <Image src="/play-button.png" alt="Play" width={30} height={30} />
             </Button>
             <Text style={{ color: '#fff', marginTop: '10px', display: 'block' }}>
diff --git a/app/styles/createForm.module.css b/app/styles/createForm.module.css
new file mode 100644
index 0000000..0357268
--- /dev/null
+++ b/app/styles/createForm.module.css
@@ -0,0 +1,69 @@
+.form {
+    display: flex;
+    flex-direction: column;
+    gap: 16px;
+    padding: 20px;
+    width: 300px;
+    background-color: #1e1e1e;
+    border-radius: 12px;
+    box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
+    color: #fff;
+  }
+  
+  /* 标题 */
+  .createTitle {
+    font-size: 1.3rem;
+    font-weight: bold;
+    margin-bottom: 0.5rem;
+    color: #ffffff;
+    text-align: center;
+  }
+  
+  /* 输入框和选择器统一风格 */
+  .input {
+    width: 100%;
+    padding: 8px 12px;
+    font-size: 0.95rem;
+    border-radius: 6px;
+    border: none;
+    background-color: #2a2a2a;
+    color: white;
+  }
+  
+  /* 提交按钮 */
+  .createButton {
+    width: 100%;
+    padding: 10px 15px;
+    background-color: #0ea5e9;
+    color: white;
+    font-weight: bold;
+    border: none;
+    border-radius: 6px;
+    cursor: pointer;
+    transition: background-color 0.3s ease;
+  }
+  
+  .createButton:hover {
+    background-color: #0284c7;
+  }
+  
+  /* 自定义 toggle switch 容器（可用在外部 Form.Item） */
+  .switch {
+    display: flex;
+    align-items: center;
+    justify-content: flex-start;
+    height: 24px;
+  }
+  
+  /* 让 label 文字美观 */
+  :global(.ant-form-item-label > label) {
+    color: #ccc;
+    font-size: 0.9rem;
+    margin-bottom: 4px;
+  }
+  
+  /* 减少垂直间距（字段之间） */
+  :global(.ant-form-item) {
+    margin-bottom: 12px;
+  }
+  
\ No newline at end of file
diff --git a/app/styles/dashboard.css b/app/styles/dashboard.css
index 2712187..5ed4ecc 100644
--- a/app/styles/dashboard.css
+++ b/app/styles/dashboard.css
@@ -1,145 +1,123 @@
-/* Game Body */
 .game-body {
     background-color: #000;
     height: 100vh;
     display: flex;
     justify-content: center;
     align-items: center;
-    font-family: Arial, Helvetica, sans-serif;
+    font-family: sans-serif;
 }
 
-.image-container {
-    display: flex;
-    justify-content: center;
-    margin-top: -20vh;
-    margin-bottom: -20vh; /* Adds space below the image */
-}
-/* Button Container */
 .game-container {
     display: flex;
     justify-content: center;
     align-items: center;
-    gap: 4vw;
+    gap: 40px;
     position: relative;
-    margin-top: -20vh;
+    margin-top: -20px;
 }
 
-/* General Button Styles */
-.big-button{
-    width: 26vw;
-    height: 12vh;
-    border-radius: 2vw;
-    color: white;
-    font-size: 2vw;
+.button {
+    width: 400px;
+    height: 300px;
+    border-radius: 40px;
+    color: #fff;
+    font-size: 48px;
     font-weight: bold;
     border: none;
     cursor: pointer;
-    transition: background 0.3s ease-in-out;
 }
 
-.small-button {
-    width: 12vw;
-    height: 6vh;
-    border-radius: 2vw;
-    color: white;
-    font-size: 1.5vw;
-    font-weight: bold;
-    border: none;
-    cursor: pointer;
-    transition: background 0.3s ease-in-out;
+.solo-button {
+    background-color: #4CAF50;
 }
 
-/* Button Colors */
-.blue {
+.combat-button {
     background-color: #03A9F4;
 }
 
-.green {
-    background-color: #4CAF50;
-}
-
-/* Popup Container */
 .popup {
     position: absolute;
     text-align: center;
     justify-content: center;
     align-items: center;
-    top: 110%;
-    width: 25vw;
-    background-color: #000;
-    border-radius: 2vw;
-    font-size: 1.5vw;
-    color: #fff;
-    display: flex;
-    flex-direction: column;
-    align-items: center;
+    gap: 30px;
+    top: 100%;
+    margin-top: 10px;
+    width: 400px;
+    background-color: #fff;
+    border-radius: 25px;
+    padding: 10px;
+    font-size: 18px;
+    color: #000;
+    box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
     opacity: 0;
+    transform: translateY(-20px);
     pointer-events: none;
     transition: opacity 0.3s ease, transform 0.3s ease;
 }
 
-/* When Popup is Visible */
 .popup.visible {
     opacity: 1;
     transform: translateY(0);
     pointer-events: auto;
 }
 
-/* Popup Header Styling */
-.popup-header {
-    font-size: 1.2vw;
-    font-weight: bold;
-    color: #fff;
+.popup.hidden {
+    opacity: 0;
+    transform: translateY(-20px);
+    pointer-events: none;
+}
+
+.popup-label {
+    font-size: 16px;
+    color: #333;
 }
 
-/* Popup Content Layout */
-.popup-content {
+.popup-controls {
     display: flex;
-    flex-direction: column;
     align-items: center;
-    /* gap: 2vw; */
-    width: 100%;
+    justify-content: center;
+    gap: 16px;
+    margin-top: 8px;
 }
 
-.time-select {
-    /* padding: 0.5vw; */
-    font-size: 1.2vw;
+.popup-header {
+    font-size: 18px;
+    font-weight: 500;
     text-align: center;
-    border-radius: 0.5vw;
-    border: 0px solid #ccc;
-    width: 40%;
-    margin-bottom: 2vh;
+    margin-bottom: 10px;
 }
 
-.time-select option {
-    text-align: center; /* This centers the text for most browsers */
+.time-select {
+    margin-top: 5px;
+    padding: 8px 12px;
+    font-size: 16px;
+    border-radius: 8px;
+    border: 1px solid #ccc;
 }
 
-.start-btn, .start-combat-btn {
-    padding: 1.5vh 2vw;
-    font-size: 1.2vw;
-    border-radius: 2vw;
+.start-btn {
+    padding: 10px 20px;
+    font-size: 18px;
+    border-radius: 15px;
     border: none;
-    background-color:#03A9F4;
+    background-color: #4caf50;
     color: white;
     cursor: pointer;
-    transition: background 0.3s ease;
+    transition: background 0.2s;
 }
 
-.start-btn:hover, .start-combat-btn:hover {
+.start-btn:hover {
     background-color: #45a049;
 }
 
-.combat-mode-select {
-    display: flex;
-    gap: 1vw;
-    margin-bottom: 2vh;
+.small-text {
+    display: block;
+    font-size: 12px;
+    margin-top: 4px;
+    color: #333;
 }
 
-.combat-private-public {
-    display: flex;
-    align-items: center;
-    gap: 1vw;
-    font-size: 1.2vw;
-    margin-bottom: 2vh;
-}
+.solo-container {
+    position: relative;
+}
\ No newline at end of file
diff --git a/app/styles/lobby.module.css b/app/styles/lobby.module.css
index e0f64dc..305d431 100644
--- a/app/styles/lobby.module.css
+++ b/app/styles/lobby.module.css
@@ -107,7 +107,7 @@
     right: -320px;
     transform: translateY(-50%);
     width: 300px;
-    height: 350px;
+    height: 490px;
     background-color: #1e1e1e;
     border-radius: 12px 0 0 12px;
     padding: 20px;
@@ -170,7 +170,7 @@
 
 .sidebarToggle {
     position: fixed;
-    top: 120px;
+    top: 85px;
     right: 0;
     background-color: #0ea5e9;
     color: white;
@@ -207,4 +207,65 @@
     background-color: #555;
     cursor: not-allowed;
   }
+
+  .emptyMessage {
+    text-align: center;
+    color: #ccc;
+    font-size: 1.1rem;
+    margin-top: 40px;
+  }
+  
+  .inlineField {
+    display: flex;
+    align-items: center;
+    gap: 10px;
+    font-size: 0.9rem;
+    margin-bottom: 10px;
+  }
+  
+  .labelText {
+    min-width: 80px; 
+  }
+
+  .switch {
+    position: relative;
+    display: inline-block;
+    width: 44px;
+    height: 24px;
+  }
+  
+  .switch input {
+    opacity: 0;
+    width: 0;
+    height: 0;
+  }
+  
+  .slider {
+    position: absolute;
+    cursor: pointer;
+    inset: 0;
+    background-color: #555;
+    border-radius: 34px;
+    transition: 0.4s;
+  }
+  
+  .slider::before {
+    content: "";
+    position: absolute;
+    height: 18px;
+    width: 18px;
+    left: 3px;
+    top: 3px;
+    background-color: white;
+    border-radius: 50%;
+    transition: 0.4s;
+  }
+  
+  .switch input:checked + .slider {
+    background-color: #0ea5e9;
+  }
+  
+  .switch input:checked + .slider::before {
+    transform: translateX(20px);
+  }
   
\ No newline at end of file
diff --git a/app/types/game.ts b/app/types/game.ts
index f190906..2deb7b4 100644
--- a/app/types/game.ts
+++ b/app/types/game.ts
@@ -19,5 +19,6 @@ export interface Game {
     modeType: string | null;
     time: string | null;
     playersNumber: string | null;
+    realPlayersNumber: string | null;
     owner: string | null;
   }
\ No newline at end of file
diff --git a/app/users/profile/page.tsx b/app/users/profile/page.tsx
index fee882f..a29c271 100644
--- a/app/users/profile/page.tsx
+++ b/app/users/profile/page.tsx
@@ -2,7 +2,6 @@
 import { useEffect, useState } from "react";
 import { Input, Button, Form } from "antd";
 import { useApi } from "@/hooks/useApi";
-import Image from "next/image";
 import { User } from "@/types/user"
 import { useParams, useRouter } from "next/navigation";
 import useLocalStorage from "@/hooks/useLocalStorage";
@@ -15,12 +14,12 @@ const ProfilePage = () => {
   const {
     value: token, // is commented out because we do not need the token value
   } = useLocalStorage<string>("token", "");
-  
+
   const [form] = Form.useForm();
   const [user, setUser] = useState<User | null>(null);
   const [isEditing, setIsEditing] = useState<boolean>(false);
   const avatar = ["/avatar_1.png", "/avatar_2.png", "/avatar_3.png", "/avatar_4.png", "/avatar_5.png", "/avatar_6.png"];
-  
+
   useEffect(() => {
     const fetchUser = async () => {
       try {
@@ -35,19 +34,19 @@ const ProfilePage = () => {
         }
       }
     };
-    
+
     fetchUser();
   }, [apiService]);
-  
+
   const handleEdit = () => {
     setIsEditing(true);
   };
-  
+
   const handleCancel = () => {
     setIsEditing(false);
     form.resetFields();
   };
-  
+
   const handleSave = async () => {
     try {
       const values = await form.validateFields();
@@ -71,97 +70,95 @@ const ProfilePage = () => {
       }
     }
   };
-  
+
   return (
     // <Authenticator>
     <div style={{ maxWidth: "400px", margin: "auto", padding: "20px", background: "#333", color: "#fff", borderRadius: "8px" }}>
-    <h2>User Profile</h2>
-    
-    {/* Profile Form */}
-    {/* {!user && ( // only for test */}
-    {user && (
-      <>
-      <Form
-      form={form}
-      layout="vertical"
-      onFinish={handleSave}
-      initialValues={user} // only for test
-      style={{ marginTop: "20px" }}
-      >
-      <Form.Item label="Avatar" shouldUpdate>
-      {() => (isEditing ? (
-        <div style={{ display: "flex", gap: "10px" }}>
-        {avatar.map((url) => {
-          const selected = form.getFieldValue("avatar") === url;
-          return (
-            <div
-            key={url}
-            onClick={() => {
-              form.setFieldsValue({ avatar: url });
-            }}
-            style={{
-              border: selected ? "2px solid #1890ff" : "2px solid transparent",
-              borderRadius: "50%",
-              cursor: "pointer",
-              transition: "transform 0.2s, border 0.2s",
-              transform: selected ? "scale(1.2)" : "scale(1)", 
-              width: 48,
-              height: 48,
-              boxSizing: "border-box",
-              display: "flex",
-              alignItems: "center",
-              justifyContent: "center",
-            }}
-            >
-            <img src={url} alt="avatar" style={{ width: 48, height: 48, borderRadius: "50%" }} />
-            </div>
-          );
-        })}
-        </div>
-      ) : (
-        <Image
-        src={form.getFieldValue("avatar") || "/avatar_1.png"}
-        alt="avatar"
-        width={48}
-        height={48}
-        style={{ borderRadius: "50%", display: "block" }}
-        />
-      ))}
-      </Form.Item>
-      <Form.Item label="Username" name="username" required={false} rules={[{ required: true, message: "Username cannot be empty!" }, { whitespace: true, message: "Username cannot be just spaces!" },]}>
-      <Input disabled={!isEditing} placeholder="Not Set" />
-      </Form.Item>
-      <Form.Item label="Email" name="email" rules={[{ type: "email", message: "Enter a valid email" }]}>
-      <Input disabled={!isEditing} placeholder="example: ex@example.com" />
-      </Form.Item>
-      <Form.Item label="Bio" name="bio">
-      <Input.TextArea rows={2} disabled={!isEditing} placeholder="example: I can guess countries over the world!" />
-      </Form.Item>
-      </Form>
-      <br />
-      {user.token === token ? (
-        // {!user ? ( // only for test
-        !isEditing ? (
-          <>
-          <Button type="primary" onClick={handleEdit} style={{ marginRight: '20px' }}>
-          Edit
-          </Button>
-          </>
-        ) : (
-          <>
-          <Button type="primary" onClick={handleSave} style={{ marginRight: '20px' }}>
-          Save
-          </Button>
-          <Button type="primary" onClick={handleCancel} style={{ marginRight: '20px' }}>
-          Cancel
-          </Button>
-          </>
-        )
-      ) : (
-        <></>
+      <h2>User Profile</h2>
+
+      {/* Profile Form */}
+      {/* {!user && ( // only for test */}
+      {user && (
+        <>
+          <Form
+            form={form}
+            layout="vertical"
+            onFinish={handleSave}
+            initialValues={user} // only for test
+            style={{ marginTop: "20px" }}
+          >
+            <Form.Item label="Avatar" shouldUpdate>
+              {() => (isEditing ? (
+                <div style={{ display: "flex", gap: "10px" }}>
+                  {avatar.map((url) => {
+                    const selected = form.getFieldValue("avatar") === url;
+                    return (
+                      <div
+                        key={url}
+                        onClick={() => {
+                          form.setFieldsValue({ avatar: url });
+                        }}
+                        style={{
+                          border: selected ? "2px solid #1890ff" : "2px solid transparent",
+                          borderRadius: "50%",
+                          cursor: "pointer",
+                          transition: "transform 0.2s, border 0.2s",
+                          transform: selected ? "scale(1.2)" : "scale(1)",
+                          width: 48,
+                          height: 48,
+                          boxSizing: "border-box",
+                          display: "flex",
+                          alignItems: "center",
+                          justifyContent: "center",
+                        }}
+                      >
+                        <img src={url} alt="avatar" style={{ width: 48, height: 48, borderRadius: "50%" }} />
+                      </div>
+                    );
+                  })}
+                </div>
+              ) : (
+                <img
+                  src={form.getFieldValue("avatar") || "/avatar_1.png"} // only for show
+                  alt="avatar"
+                  style={{ width: 48, height: 48, borderRadius: "50%", display: "block" }}
+                />
+              ))}
+            </Form.Item>
+            <Form.Item label="Username" name="username" required={false} rules={[{ required: true, message: "Username cannot be empty!" }, { whitespace: true, message: "Username cannot be just spaces!" },]}>
+              <Input disabled={!isEditing} placeholder="Not Set" />
+            </Form.Item>
+            <Form.Item label="Email" name="email" rules={[{ type: "email", message: "Enter a valid email" }]}>
+              <Input disabled={!isEditing} placeholder="example: ex@example.com" />
+            </Form.Item>
+            <Form.Item label="Bio" name="bio">
+              <Input.TextArea rows={2} disabled={!isEditing} placeholder="example: I can guess countries over the world!" />
+            </Form.Item>
+          </Form>
+          <br />
+          {user.token === token ? (
+            // {!user ? ( // only for test
+            !isEditing ? (
+              <>
+                <Button type="primary" onClick={handleEdit} style={{ marginRight: '20px' }}>
+                  Edit
+                </Button>
+              </>
+            ) : (
+              <>
+                <Button type="primary" onClick={handleSave} style={{ marginRight: '20px' }}>
+                  Save
+                </Button>
+                <Button type="primary" onClick={handleCancel} style={{ marginRight: '20px' }}>
+                  Cancel
+                </Button>
+              </>
+            )
+          ) : (
+            <></>
+          )}
+        </>
       )}
-      </>
-    )}
     </div>
     // </Authenticator>
   );
diff --git a/app/utils/useLogout.ts b/app/utils/useLogout.ts
index bd968ae..ebfbb0f 100644
--- a/app/utils/useLogout.ts
+++ b/app/utils/useLogout.ts
@@ -37,7 +37,7 @@ export const useLogout = () => {
       dispatch(logout());
 
       console.log("Logged out successfully!");
-      router.push("/game");
+      router.push("/users/login");
     } catch (error) {
       messageApi.error(`Error logging out: ${String(error)}`);
     }


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: 

Copilot reviewed 8 out of 11 changed files in this pull request and generated 4 comments.

<details>
<summary>Files not reviewed (3)</summary>

* **app/styles/createForm.module.css**: Language not supported
* **app/styles/dashboard.css**: Language not supported
* **app/styles/lobby.module.css**: Language not supported
</details>



state: COMMENTED
author_association: NONE
submitted_at: 2025-04-06T20:30:07Z
user: login: rkoonireddy
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-06T22:51:02Z
user: login: rkoonireddy
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-06T22:53:42Z
user: login: rkoonireddy
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-06T22:56:27Z
user: login: rkoonireddy
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-06T22:59:09Z

Review comments:
diff_hunk: @@ -0,0 +1,26 @@
+import { Game } from '@/types/game';
+import { ApiService } from '@/api/apiService';
+import { AppRouterInstance } from 'next/dist/shared/lib/app-router-context.shared-runtime';
+
+export const handleJoinGame = async (game: Game, userId: string, apiService: ApiService, router: AppRouterInstance): Promise<void> => {
path: app/lobby/join/handleJoinGame.ts
user: login: Copilot
body: Avoid using internal Next.js modules; instead, import AppRouterInstance or related types from a public API like 'next/navigation'.
```suggestion
import { useRouter } from 'next/navigation';

export const handleJoinGame = async (game: Game, userId: string, apiService: ApiService): Promise<void> => {
  const router = useRouter();
```
created_at: 2025-04-06T20:30:06Z
author_association: NONE
diff_hunk: @@ -19,5 +19,6 @@ export interface Game {
     modeType: string | null;
     time: string | null;
     playersNumber: string | null;
+    realPlayersNumber: string | null;
path: app/types/game.ts
user: login: Copilot
body: [nitpick] Consider using a numeric type for player counts instead of a string if these values are meant for numerical comparisons or arithmetic operations.
```suggestion
    playersNumber: number | null;
    realPlayersNumber: number | null;
```
created_at: 2025-04-06T20:30:07Z
author_association: NONE
diff_hunk: @@ -62,16 +67,20 @@ const Lobby: React.FC = () => {
           <div>Owner</div>
         </div>
 
-        {paginatedGames.map((lobby, idx) => (
-          <div key={idx} className={styles.lobbyCard}>
-            <div className={styles.teamName}>
-              {lobby.gameName}
-              {lobby.password !== null && <span title="Private game">🔒</span>}
+        {paginatedGames.length === 0 ? (
+          <div className={styles.emptyMessage}>No Available Game</div>
+        ) : (
+          paginatedGames.map((game, idx) => (
+            <div key={idx} className={styles.lobbyCard} onClick={() => handleJoinGame(game, userId, apiService, router)}>
path: app/lobby/page.tsx
user: login: Copilot
body: Using the array index as a key may lead to rendering issues; consider using a unique identifier such as the game's ID.
```suggestion
          paginatedGames.map((game) => (
            <div key={game.id} className={styles.lobbyCard} onClick={() => handleJoinGame(game, userId, apiService, router)}>
```
created_at: 2025-04-06T20:30:07Z
author_association: NONE
diff_hunk: @@ -1,12 +1,115 @@
-import React from 'react';
-
-const GuestPage: React.FC = () => {
-    return (
-        <div>
-            <h1>Welcome, Guest!</h1>
-            <p>Please sign in to access more features.</p>
-        </div>
-    );
+"use client";
+
+import React, { useState } from 'react';
+import styles from "@/styles/lobby.module.css";
+import { useApi } from '@/hooks/useApi';
+import { useRouter } from 'next/navigation';
+import { useSelector } from 'react-redux';
+import { Game } from '@/types/game';
+
+const CreateForm: React.FC = () => {
+  const apiService = useApi();
+  const router = useRouter();
+  const username = useSelector((state: { user: { username: string } }) => state.user.username)
+  const [isPrivate, setIsPrivate] = useState(false);
+  const [gameName, setGameName] = useState("");
+  const [password, setPassword] = useState("");
+  const [maxPlayers, setMaxPlayers] = useState("2");
+  const [duration, setDuration] = useState("5");
+
+  const handleCreateGame = async () => {
+    if (!gameName.trim()) {
+      alert("Game name cannot be empty!");
+      return;
+    }
+
+    const newGame: Game = {
+      gameName: gameName,
+      playersNumber: maxPlayers,
+      time: duration,
+      password: password,
+      owner: username,
+      gameId: null,
+      realPlayersNumber: null,
+      modeType: "combat" // placeholder
+    };
+
+    try {
+      const response = await apiService.post<Game>("/games", newGame);
+      console.log(JSON.stringify(response, null, 2));
+      if (response.gameId)  {
+        alert("Game created successfully!");
+        router.push("/game/1v1")
path: app/lobby/create/page.tsx
user: login: Copilot
body: Hard-coding the route to '/game/1v1' may not accurately reflect the newly created game; consider using the game ID returned from the API response for redirection.
```suggestion
        router.push(`/game/${response.gameId}`)
```
created_at: 2025-04-06T20:30:07Z
author_association: NONE
diff_hunk: @@ -0,0 +1,26 @@
+import { Game } from '@/types/game';
+import { ApiService } from '@/api/apiService';
+import { AppRouterInstance } from 'next/dist/shared/lib/app-router-context.shared-runtime';
+
+export const handleJoinGame = async (game: Game, userId: string, apiService: ApiService, router: AppRouterInstance): Promise<void> => {
path: app/lobby/join/handleJoinGame.ts
user: login: rkoonireddy
body: this suggestion feels relevant. next/navigation is better imo
created_at: 2025-04-06T22:51:02Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,26 @@
+import { Game } from '@/types/game';
+import { ApiService } from '@/api/apiService';
+import { AppRouterInstance } from 'next/dist/shared/lib/app-router-context.shared-runtime';
+
+export const handleJoinGame = async (game: Game, userId: string, apiService: ApiService, router: AppRouterInstance): Promise<void> => {
+  if (game.password) {
+    const password = prompt("Enter the game password:");
+    if (password === null || password.trim() === "") {
+      return;
+    }
+    game.password = password;
+  } else {
+    game.password = "";
+  }
+  console.log(JSON.stringify(game, null, 2));
+  try {
+    await apiService.put<Game>(`/lobbyIn/${userId}`, game);
+    router.push(`/game/start/${game.gameId}`);
path: app/lobby/join/handleJoinGame.ts
user: login: rkoonireddy
body: this makes sense. I was trying to implement the dynamic route but faced some issues with the routing. we will have to sort this. Right now i am using /game/gameSession as default slug for game but this way makes more reasonable logic.
created_at: 2025-04-06T22:53:42Z
author_association: COLLABORATOR
diff_hunk: @@ -19,5 +19,6 @@ export interface Game {
     modeType: string | null;
     time: string | null;
     playersNumber: string | null;
+    realPlayersNumber: string | null;
path: app/types/game.ts
user: login: rkoonireddy
body: AI suggested comment is reasonable, better to change to number. 
created_at: 2025-04-06T22:56:27Z
author_association: COLLABORATOR

PR comments:
user: login: vercel[bot]
created_at: 2025-04-06T20:29:01Z
body: [vc]: #l57Ge3SCAu8qCNVkecxaYokxlq908oCiInHvy5aFohA=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJzb3ByYS1mczI1LWdyb3VwLTEwLWNsaWVudCIsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiJzb3ByYS1mczI1LWdyb3VwLTEwLWNsaWVudC1naXQtZC01MzIwOWMtdDBoc2FrYXIxbjEyNnMtcHJvamVjdHMudmVyY2VsLmFwcCJ9LCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vdDBoc2FrYXIxbjEyNnMtcHJvamVjdHMvc29wcmEtZnMyNS1ncm91cC0xMC1jbGllbnQvNk1aRVA1amtwTTZhTnNBTmM5Z3hyUEJzMWhmOCIsInByZXZpZXdVcmwiOiJzb3ByYS1mczI1LWdyb3VwLTEwLWNsaWVudC1naXQtZC01MzIwOWMtdDBoc2FrYXIxbjEyNnMtcHJvamVjdHMudmVyY2VsLmFwcCIsIm5leHRDb21taXRTdGF0dXMiOiJERVBMT1lFRCIsInJvb3REaXJlY3RvcnkiOm51bGx9XX0=
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **sopra-fs25-group-10-client** | ✅ Ready ([Inspect](https://vercel.com/t0hsakar1n126s-projects/sopra-fs25-group-10-client/6MZEP5jkpM6aNsANc9gxrPBs1hf8)) | [Visit Preview](https://sopra-fs25-group-10-client-git-d-53209c-t0hsakar1n126s-projects.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/sopra-fs25-group-10-client-git-d-53209c-t0hsakar1n126s-projects.vercel.app?via=pr-comment-feedback-link) | Apr 6, 2025 9:21pm |


author_association: NONE
user: login: sonarqubecloud[bot]
created_at: 2025-04-06T20:29:44Z
body: ## [![Quality Gate Failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-failed-20px.png 'Quality Gate Failed')](https://sonarcloud.io/dashboard?id=T0hsakaR1n126_sopra-fs25-group-10-client&pullRequest=40) **Quality Gate failed**  
Failed conditions  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/failed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=T0hsakaR1n126_sopra-fs25-group-10-client&pullRequest=40&metric=new_coverage&view=list) (required ≥ 80%)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/failed-16px.png '') [B Reliability Rating on New Code](https://sonarcloud.io/dashboard?id=T0hsakaR1n126_sopra-fs25-group-10-client&pullRequest=40) (required ≥ A)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=T0hsakaR1n126_sopra-fs25-group-10-client&pullRequest=40)

##   
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/light_bulb-16px.png '') Catch issues before they fail your Quality Gate with our IDE extension ![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/sonarlint-16px.png '') [SonarQube for IDE](https://www.sonarsource.com/products/sonarlint/features/connected-mode/?referrer=pull-request)


author_association: NONE