Repository: distinctioncoding/REAMP-REACT-TEST
Pull request #15: Ream 162
Author: login: Brady-Zhang
Created: 2025-05-20T15:28:04Z | Merged: 2025-05-23T14:40:09Z



Diff:
diff --git a/REMP/src/App.tsx b/REMP/src/App.tsx
index 28f0e95..a895408 100644
--- a/REMP/src/App.tsx
+++ b/REMP/src/App.tsx
@@ -8,7 +8,7 @@ import ListingDashboard from './components/ListingDashboard';
 
 
 import HeroImageSelect from './components/HeroImageSelect';
-import DashboardLayout from './components/DashboardLayout';
+import AgentList from './components/AgentList/AgentList';
 
 // const mockImages = Array.from({ length: 25 }, (_, i) => `https://picsum.photos/seed/${i}/150`);
 // const mockImages = Array.from({ length: 25 }, (_, i) => `/drop.webp?id=${i}`);
@@ -36,10 +36,9 @@ function App() {
              <CreateAgentModel isVisible={isModalVisible} onClose={closeModal}></CreateAgentModel>
           </>
         } />
+
         <Route path="/AgentPropertyPage" element={<AgentPropertyPage/>} />
         <Route path="/dashboard" element={<ListingDashboard />} />
-        <Route path="/dashboard2" element={<DashboardLayout />} /> 
-        <Route path="/AgentPropertyPage" element={<AgentPropertyPage/>} />
         <Route path="/login" element={<SignInPage/>} />
         <Route
           path="/hero-select"
diff --git a/REMP/src/api/agent/get-agent-by-email.ts b/REMP/src/api/agent/get-agent-by-email.ts
new file mode 100644
index 0000000..8aa13b3
--- /dev/null
+++ b/REMP/src/api/agent/get-agent-by-email.ts
@@ -0,0 +1,8 @@
+import { Agent} from "../../interfaces/agent";
+import { mapToAgent } from "../../lib/map-to-agent";
+import apiClient from "../apiClient";
+
+export const getAgentByEmail = async (email: string): Promise<Agent> => {
+  const response = await apiClient.get(`User/FindAgentByEmail/${email}`);
+  return mapToAgent(response.data);
+};
\ No newline at end of file
diff --git a/REMP/src/api/agent/get-all-agents.ts b/REMP/src/api/agent/get-all-agents.ts
new file mode 100644
index 0000000..88edf21
--- /dev/null
+++ b/REMP/src/api/agent/get-all-agents.ts
@@ -0,0 +1,8 @@
+import { AxiosResponse } from "axios"
+import { Agent } from "../../interfaces/agent";
+import apiClient from "../apiClient";
+
+export const getAllAgents = async():Promise<Agent[]> =>{
+    const response : AxiosResponse<Agent[]> = await apiClient.get('User/GetAllAgents');
+    return response.data
+}
\ No newline at end of file
diff --git a/REMP/src/api/agent/search-agent.ts b/REMP/src/api/agent/search-agent.ts
new file mode 100644
index 0000000..b0a8247
--- /dev/null
+++ b/REMP/src/api/agent/search-agent.ts
@@ -0,0 +1,11 @@
+import { Agent, AgentByEmail } from "../../interfaces/agent";
+import { mapToAgent } from "../../lib/map-to-agent";
+import apiClient from "../apiClient";
+
+export const searchAgent = async (searchTerm: string): Promise<Agent[]> => {
+  const response = await apiClient.get('User/search-agent', {
+    params: { searchTerm }
+  });
+
+  return response.data.map((item: AgentByEmail) => mapToAgent(item));
+};
diff --git a/REMP/src/components/AgentList/AgentList.tsx b/REMP/src/components/AgentList/AgentList.tsx
new file mode 100644
index 0000000..bfb6f08
--- /dev/null
+++ b/REMP/src/components/AgentList/AgentList.tsx
@@ -0,0 +1,107 @@
+import { useEffect, useState } from 'react';
+import { useAuth } from '../../contexts/AuthContext';
+import { CiSearch } from 'react-icons/ci';
+import { Agent } from '../../interfaces/agent';
+import { searchAgent } from '../../api/agent/search-agent';
+import { getAllAgents } from '../../api/agent/get-all-agents';
+
+const AgentList = () => {
+  const { user } = useAuth();
+  const [agentLists, setAgentLists] = useState<Agent[]>([]);
+  const [searchTerm, setSearchTerm] = useState('');
+  const [openMenuId, setOpenMenuId] = useState<string | null>(null);
+  const [loading, setLoading] = useState(false);
+
+  useEffect(() => {
+    const fetchAgents = async () => {
+      if (!searchTerm.trim()) {
+        try {
+            const result = await getAllAgents();
+            setAgentLists(result);
+        } catch (err) {
+            console.error("Failed to fetch all agents", err);
+            setAgentLists([]);
+        }
+        return;
+    }
+
+      setLoading(true);
+      try {
+        const result = await searchAgent(searchTerm);
+        setAgentLists(result);
+      } catch (err) {
+        console.error("Search failed", err);
+        setAgentLists([]);
+      } finally {
+        setLoading(false);
+      }
+    };
+
+    const delay = setTimeout(fetchAgents, 300); // 防抖
+    return () => clearTimeout(delay);
+  }, [searchTerm]);
+
+  return (
+    <div className="p-6">
+      <h1 className="text-3xl font-bold text-center mb-6">Hi, Welcome {user?.userName}!</h1>
+
+      <div className="flex items-center mb-4">
+        <div className="relative w-full max-w-xl mx-auto flex-1">
+          <CiSearch className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
+          <input
+            type="text"
+            value={searchTerm}
+            onChange={(e) => setSearchTerm(e.target.value)}
+            placeholder="Search client..."
+            className="pl-10 pr-4 py-2 w-full border rounded-md shadow-sm"
+          />
+          {!loading && searchTerm && agentLists.length === 0 && (
+            <div className="absolute mt-1 left-0 bg-white shadow border p-2 w-full text-sm text-gray-500">
+              No existing client, please try a new one or{" "}
+              <span className="text-blue-600 underline cursor-pointer">Create New Client</span>.
+            </div>
+          )}
+        </div>
+
+        <button className="ml-4 bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-md whitespace-nowrap">
+          + Create New Client
+        </button>
+      </div>
+
+      <table className="w-full table-auto border-collapse">
+        <thead>
+          <tr className="bg-gray-100 text-sm">
+            <th className="p-2">Client Name</th>
+            <th className="p-2">Company</th>
+            <th className="p-2">Phone</th>
+            <th className="p-2">Email</th>
+            <th className="p-2 text-center">Actions</th>
+          </tr>
+        </thead>
+        <tbody>
+          {agentLists.map((agent) => (
+            <tr key={agent.id} className="text-sm text-center hover:bg-gray-50 relative">
+              <td className="p-2">{agent.firstName} {agent.lastName}</td>
+              <td className="p-2">{agent.companyName}</td>
+              <td className="p-2">{agent.phoneNumber}</td>
+              <td className="p-2">{agent.email}</td>
+              <td className="p-2 relative">
+                <button onClick={() => setOpenMenuId(openMenuId === agent.id ? null : agent.id)}>
+                  ⋯
+                </button>
+                {openMenuId === agent.id && (
+                  <div className="absolute right-0 top-8 z-10 bg-white border rounded shadow-md w-28 text-left">
+                    <button className="block w-full px-4 py-2 hover:bg-gray-100 text-sm text-gray-700">Edit</button>
+                    <button className="block w-full px-4 py-2 hover:bg-gray-100 text-sm text-red-500">Delete</button>
+                  </div>
+                )}
+              </td>
+            </tr>
+          ))}
+        </tbody>
+      </table>
+    </div>
+  );
+};
+
+export default AgentList;
diff --git a/REMP/src/components/AgentPropertyPage/NavBar.tsx b/REMP/src/components/AgentPropertyPage/NavBar.tsx
index dfa19b9..6b25622 100644
--- a/REMP/src/components/AgentPropertyPage/NavBar.tsx
+++ b/REMP/src/components/AgentPropertyPage/NavBar.tsx
@@ -1,10 +1,13 @@
 import { CiSearch } from "react-icons/ci";
-const NavBar = () => {
-  const userName = "Jane Doe"; // Mock name, need to change after finish login,cause get name form login
+import { useAuth } from "../../contexts/AuthContext";
 
+const NavBar = () => {
+  const { user } = useAuth();
+  const agentName = user?.agentName ?? "Unknown";
+  
   return (
     <nav className="w-full px-32 py-4 bg-white">
-      <div className="text-sm text-gray-500 text-left">Hi, <span className="font-medium">{userName}</span></div>
+      <div className="text-sm text-gray-500 text-left">Hi, <span className="font-medium">{agentName}</span></div>
 
       <div className="flex items-center justify-between mt-2">
         <h1 className="text-lg font-semibold text-gray-800">My Order</h1>
diff --git a/REMP/src/components/DashboardLayout.tsx b/REMP/src/components/DashboardLayout.tsx
index eb1eea6..1fbc9eb 100644
--- a/REMP/src/components/DashboardLayout.tsx
+++ b/REMP/src/components/DashboardLayout.tsx
@@ -2,7 +2,8 @@ import { useState, useEffect} from 'react';
 import DashboardNavbar from './DashboardNavbar';
 import { useAuth } from '../contexts/AuthContext';
 import { useNavigate } from 'react-router-dom';
-import ListingDashboard from './ListingDashboard';
+import AgentList from './AgentList/AgentList';
+
 
 
 const allNavItems = ['Listing', 'Agents', 'Photography companies'] as const;
@@ -37,7 +38,7 @@ const DashboardLayout = () => {
       case 'Listing':
         return <ListingDashboard />;
       case 'Agents':
-        return <div className="text-xl">This is the Agents content.</div>;
+        return <div className="text-xl"><AgentList /></div>;
       case 'Photography companies':
         return <div className="text-xl">This is the Photography Companies content.</div>;
       default:
diff --git a/REMP/src/components/SignInPage.tsx b/REMP/src/components/SignInPage.tsx
index 67de511..98bc9e5 100644
--- a/REMP/src/components/SignInPage.tsx
+++ b/REMP/src/components/SignInPage.tsx
@@ -14,9 +14,10 @@ const SignInPage = () => {
     e.preventDefault();
     try {
       const data = await login({ email, password });
-
       localStorage.setItem("user", JSON.stringify(data));
       setUser(data.user);
+      // console.log(JSON.parse(atob(data.token.split('.')[1])))
+
 
       const role = data.user.role;
       if (role === "Admin") {
diff --git a/REMP/src/contexts/AuthContext.tsx b/REMP/src/contexts/AuthContext.tsx
index 6491a14..17a6efa 100644
--- a/REMP/src/contexts/AuthContext.tsx
+++ b/REMP/src/contexts/AuthContext.tsx
@@ -19,14 +19,17 @@ export const useAuth = () => {
   }
   return context;
 };
-
+// const MyComponent = (props: { children: React.ReactNode }) => {
+//   const { children } = props;
+// }
 export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
   const raw = localStorage.getItem('user');
-  const initialUser: User | null = raw ? JSON.parse(raw)?.user ?? null : null;
+  const initialUser: User | null = raw ? JSON.parse(raw)?.user ?? null : null;//如果raw有值就解析他的user{ role: 'Agent', name: 'Alice' };，如果undefined，就返回null，如果raw无值，直接就是null
 
   const [user, setUser] = useState<User | null>(initialUser);
   return (
-    <AuthContext.Provider value={{ user, setUser }}>
+    //是你传给上下文的实际“值”。它是一个对象，包含两个东西：user, setUser
+    <AuthContext.Provider value={{ user, setUser }}> 
       {children}
     </AuthContext.Provider>
   );
diff --git a/REMP/src/interfaces/agent.ts b/REMP/src/interfaces/agent.ts
new file mode 100644
index 0000000..0c1b591
--- /dev/null
+++ b/REMP/src/interfaces/agent.ts
@@ -0,0 +1,20 @@
+interface BaseAgent {
+  id: string;
+  email: string;
+  phoneNumber: string;
+  companyName: string;
+  avatarUrl?: string;
+}
+
+interface FullName {
+  firstName: string;
+  lastName: string;
+}
+
+interface AgentNameByEmail {
+  agentFirstName: string;
+  agentLastName: string;
+}
+
+export type Agent = BaseAgent & FullName;
+export type AgentByEmail = BaseAgent & AgentNameByEmail;
diff --git a/REMP/src/lib/map-to-agent.ts b/REMP/src/lib/map-to-agent.ts
new file mode 100644
index 0000000..3b86edc
--- /dev/null
+++ b/REMP/src/lib/map-to-agent.ts
@@ -0,0 +1,12 @@
+import { Agent, AgentByEmail } from "../interfaces/agent";
+
+// getAllAgents和getAgentByEmail返回的json格式不用
+export const mapToAgent = (data: AgentByEmail): Agent => ({
+  id: data.id,
+  firstName: data.agentFirstName,
+  lastName: data.agentLastName,
+  email: data.email,
+  phoneNumber: data.phoneNumber,
+  companyName: data.companyName,
+  avatarUrl: data.avatarUrl
+});


Reviews:
user: login: Gary-Distinctioncoding
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-22T15:32:52Z
user: login: Gary-Distinctioncoding
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-05-22T15:50:24Z
user: login: Brady-Zhang
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-23T06:46:42Z
user: login: Brady-Zhang
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-23T06:49:43Z
user: login: Gary-Distinctioncoding
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-23T14:40:02Z

Review comments:
path: REMP/src/api/get-all-agents.ts
user: login: Gary-Distinctioncoding
body: put two apis getAgentByEmail and getAllAgents into one folder
created_at: 2025-05-22T15:46:36Z
author_association: COLLABORATOR
path: REMP/src/interfaces/agent.ts
user: login: Gary-Distinctioncoding
body: can use intersection & to reduce redundancy
created_at: 2025-05-22T15:50:17Z
author_association: COLLABORATOR
path: REMP/src/api/get-all-agents.ts
user: login: Brady-Zhang
body: solved
created_at: 2025-05-23T06:46:42Z
author_association: COLLABORATOR
path: REMP/src/interfaces/agent.ts
user: login: Brady-Zhang
body: solved
created_at: 2025-05-23T06:49:43Z
author_association: COLLABORATOR

PR comments:
user: login: Gary-Distinctioncoding
created_at: 2025-05-22T15:33:15Z
body: solve conflicts plz
author_association: COLLABORATOR
user: login: Gary-Distinctioncoding
created_at: 2025-05-22T15:53:28Z
body: 用错api了，getAgentByEmail 是photographycompany的，这个ticket是admin的。搜索用search-agent api
author_association: COLLABORATOR