From 0c9dc1bf13833118563a89edc4f52be215d667b6 Mon Sep 17 00:00:00 2001 From: Jane Kamata Date: Tue, 21 Jan 2025 21:27:10 -0500 Subject: [PATCH] Added navigation controls from account settings to grant list; changed dashboard to account --- backend/dist/auth/auth.controller.js | 1 - frontend/package-lock.json | 26 ++++++ frontend/src/Account.tsx | 57 +++++++++++++ frontend/src/App.tsx | 77 +++++++++-------- frontend/src/Dashboard.tsx | 32 ------- frontend/src/Login.tsx | 84 +++++++++++-------- frontend/src/Profile.tsx | 56 +++++++------ frontend/src/Register.tsx | 77 ++++++++++------- frontend/src/grant-info/components/Header.tsx | 50 ++++++----- frontend/tests/Mock.test.tsx | 42 +++++----- frontend/tsconfig.app.tsbuildinfo | 2 +- 11 files changed, 298 insertions(+), 206 deletions(-) create mode 100644 frontend/src/Account.tsx delete mode 100644 frontend/src/Dashboard.tsx diff --git a/backend/dist/auth/auth.controller.js b/backend/dist/auth/auth.controller.js index c922ad75..d6c91c61 100644 --- a/backend/dist/auth/auth.controller.js +++ b/backend/dist/auth/auth.controller.js @@ -1,5 +1,4 @@ "use strict"; -// src/auth/auth.controller.ts var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 87a4e68a..c0fd4aae 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -4695,6 +4695,32 @@ "url": "https://opencollective.com/mobx" } }, + "node_modules/mobx-react": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/mobx-react/-/mobx-react-9.2.0.tgz", + "integrity": "sha512-dkGWCx+S0/1mfiuFfHRH8D9cplmwhxOV5CkXMp38u6rQGG2Pv3FWYztS0M7ncR6TyPRQKaTG/pnitInoYE9Vrw==", + "license": "MIT", + "peer": true, + "dependencies": { + "mobx-react-lite": "^4.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mobx" + }, + "peerDependencies": { + "mobx": "^6.9.0", + "react": "^16.8.0 || ^17 || ^18 || ^19" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/mobx-react-lite": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/mobx-react-lite/-/mobx-react-lite-4.1.0.tgz", diff --git a/frontend/src/Account.tsx b/frontend/src/Account.tsx new file mode 100644 index 00000000..4fd81e56 --- /dev/null +++ b/frontend/src/Account.tsx @@ -0,0 +1,57 @@ +import { observer } from "mobx-react-lite"; +import { useAuthContext } from "./context/auth/authContext"; +import { logoutUser } from "./external/bcanSatchel/actions"; +import Profile from "./Profile"; +import { Link } from "react-router-dom"; + +const Account = observer(() => { + const { user } = useAuthContext(); + + const handleLogout = () => { + logoutUser(); + }; + + return ( +
+
+ + + + +
+

Welcome, {user?.userId}

+ + +
+ ); +}); + +export default Account; diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 2df83575..408fdb73 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,51 +1,56 @@ -import './App.css'; +import "./App.css"; // Components -import Login from './Login'; -import Register from './Register'; -import Dashboard from './Dashboard'; +import Login from "./Login"; +import Register from "./Register"; +import Account from "./Account.tsx"; import GrantPage from "./grant-info/components/GrantPage.tsx"; // Libraries -import { ChakraProvider, defaultSystem } from "@chakra-ui/react" -import { BrowserRouter as Router, Route, Routes, Navigate } from 'react-router-dom'; -import { observer } from 'mobx-react-lite'; +import { ChakraProvider, defaultSystem } from "@chakra-ui/react"; +import { + BrowserRouter as Router, + Route, + Routes, + Navigate, +} from "react-router-dom"; +import { observer } from "mobx-react-lite"; // Register store and mutators -import './external/bcanSatchel/mutators'; -import { useAuthContext } from './context/auth/authContext'; - +import "./external/bcanSatchel/mutators"; +import { useAuthContext } from "./context/auth/authContext"; const App = observer(() => { - const { isAuthenticated } = useAuthContext() + const { isAuthenticated } = useAuthContext(); return ( - < div className="app-container"> - - : } - /> - : } - /> - : } - /> - } - /> - } - /> - - +
+ + : } + /> + : + } + /> + : } + /> + } /> + + } + /> + +
); }); -export default App; \ No newline at end of file +export default App; diff --git a/frontend/src/Dashboard.tsx b/frontend/src/Dashboard.tsx deleted file mode 100644 index fe1af10a..00000000 --- a/frontend/src/Dashboard.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { observer } from 'mobx-react-lite'; -import { useAuthContext } from './context/auth/authContext'; -import { logoutUser } from './external/bcanSatchel/actions'; -import Profile from './Profile'; - -const Dashboard = observer(() => { - const { user } = useAuthContext(); - - const handleLogout = () => { - logoutUser(); - }; - - return ( -
- - -

Welcome, {user?.userId}

- - -
- ); -}); - -export default Dashboard; \ No newline at end of file diff --git a/frontend/src/Login.tsx b/frontend/src/Login.tsx index fc5aeac6..16a39552 100644 --- a/frontend/src/Login.tsx +++ b/frontend/src/Login.tsx @@ -1,70 +1,86 @@ -import React, { useState } from 'react'; -import { useAuthContext } from './context/auth/authContext' -import { observer } from 'mobx-react-lite'; -import { useNavigate } from 'react-router-dom'; -import './external/bcanSatchel/mutators'; +import React, { useState } from "react"; +import { useAuthContext } from "./context/auth/authContext"; +import { observer } from "mobx-react-lite"; +import { useNavigate } from "react-router-dom"; +import "./external/bcanSatchel/mutators"; const Login = observer(() => { - const [username, setUsername] = useState(''); - const [password, setPassword] = useState(''); + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); const navigate = useNavigate(); const { login } = useAuthContext(); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); try { - await login(username, password); - navigate('/dashboard'); + await login(username, password); + navigate("/account"); } catch (error) { - console.error('Error during login:', error); - alert('An error occurred while logging in. Please try again later.'); + console.error("Error during login:", error); + alert("An error occurred while logging in. Please try again later."); } }; return ( -
+

Login

- + setUsername(e.target.value)} required - style={{width: '90%', - padding: '8px', - margin: '8px 0'}} + style={{ width: "90%", padding: "8px", margin: "8px 0" }} />
- + setPassword(e.target.value)} required - style={{ width: '90%', padding: '8px', margin: '8px 0'}} + style={{ width: "90%", padding: "8px", margin: "8px 0" }} /> -
- -
); }); -export default Login; \ No newline at end of file +export default Login; diff --git a/frontend/src/Profile.tsx b/frontend/src/Profile.tsx index 2520b8bc..0e32243a 100644 --- a/frontend/src/Profile.tsx +++ b/frontend/src/Profile.tsx @@ -1,59 +1,63 @@ -import React, { useState } from 'react'; -import { observer } from 'mobx-react-lite'; -import { useAuthContext } from './context/auth/authContext'; -import { updateUserProfile } from './external/bcanSatchel/actions'; +import React, { useState } from "react"; +import { observer } from "mobx-react-lite"; +import { useAuthContext } from "./context/auth/authContext"; +import { updateUserProfile } from "./external/bcanSatchel/actions"; const Profile = observer(() => { const { user } = useAuthContext(); - const [email, setEmail] = useState(user?.email || ''); - const [biography, setBiography] = useState(user?.biography || ''); + const [email, setEmail] = useState(user?.email || ""); + const [biography, setBiography] = useState(user?.biography || ""); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); try { - const response = await fetch('http://localhost:3001/user/me', { - method: 'PUT', + const response = await fetch("http://localhost:3001/user/me", { + method: "PUT", headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${user?.accessToken ?? ''}`, + "Content-Type": "application/json", + Authorization: `Bearer ${user?.accessToken ?? ""}`, }, body: JSON.stringify({ email, biography }), }); if (!response.ok) { const errorData = await response.json(); - alert(errorData.message || 'Failed to update profile.'); + alert(errorData.message || "Failed to update profile."); return; } const data = await response.json(); updateUserProfile(data); - alert('Profile updated successfully.'); + alert("Profile updated successfully."); } catch (error) { - console.error('Error updating profile:', error); - alert('An error occurred while updating your profile.'); + console.error("Error updating profile:", error); + alert("An error occurred while updating your profile."); } }; return ( -
+

Profile

-
- - +
+ {user?.userId}
setEmail(e.target.value)} required @@ -62,16 +66,16 @@ const Profile = observer(() => {
- ); }); -export default Profile; \ No newline at end of file +export default Profile; diff --git a/frontend/src/Register.tsx b/frontend/src/Register.tsx index c29b8c73..7371104a 100644 --- a/frontend/src/Register.tsx +++ b/frontend/src/Register.tsx @@ -1,20 +1,20 @@ -import React, { useState } from 'react'; -import { setAuthState } from './external/bcanSatchel/actions'; -import { observer } from 'mobx-react-lite'; -import { useNavigate } from 'react-router-dom'; +import React, { useState } from "react"; +import { setAuthState } from "./external/bcanSatchel/actions"; +import { observer } from "mobx-react-lite"; +import { useNavigate } from "react-router-dom"; const Register = observer(() => { - const [username, setUsername] = useState(''); - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); + const [username, setUsername] = useState(""); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); const navigate = useNavigate(); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - const response = await fetch('http://localhost:3001/auth/register', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, + const response = await fetch("http://localhost:3001/auth/register", { + method: "POST", + headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username, password, email }), }); @@ -22,9 +22,9 @@ const Register = observer(() => { if (response.ok) { // Automatically log in the user - const loginResponse = await fetch('http://localhost:3001/auth/login', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, + const loginResponse = await fetch("http://localhost:3001/auth/login", { + method: "POST", + headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username, password }), }); @@ -32,60 +32,73 @@ const Register = observer(() => { if (loginResponse.ok && loginData.access_token) { setAuthState(true, loginData.user, loginData.access_token); - navigate('/dashboard'); + navigate("/account"); } else { - alert(loginData.message || 'Login after registration failed.'); + alert(loginData.message || "Login after registration failed."); } } else { - alert(data.message || 'Registration failed.'); + alert(data.message || "Registration failed."); } }; return ( -
+

Register

- + setUsername(e.target.value)} required - style={{ width: '90%', padding: '8px', margin: '8px 0' }} + style={{ width: "90%", padding: "8px", margin: "8px 0" }} />
- + setEmail(e.target.value)} required - style={{ width: '90%', padding: '8px', margin: '8px 0' }} + style={{ width: "90%", padding: "8px", margin: "8px 0" }} />
- + setPassword(e.target.value)} required - style={{ width: '90%', padding: '8px', margin: '8px 0' }} + style={{ width: "90%", padding: "8px", margin: "8px 0" }} />
-
); }); -export default Register; \ No newline at end of file +export default Register; diff --git a/frontend/src/grant-info/components/Header.tsx b/frontend/src/grant-info/components/Header.tsx index ddbc752a..082ef9d8 100644 --- a/frontend/src/grant-info/components/Header.tsx +++ b/frontend/src/grant-info/components/Header.tsx @@ -1,30 +1,34 @@ -import React from 'react'; -import './styles/Header.css' +import React from "react"; +import { Link } from "react-router-dom"; +import "./styles/Header.css"; /** * Header * @returns HTML for the header component */ const Header: React.FC = () => { - return ( -
-
-
  • - BCAN Logo -
  • + return ( +
    +
    +
  • + BCAN Logo +
  • +
    +
    +
      +
    • All Grants
    • +
    • My Grants
    • +
    • Active Grants
    • +
    • Inactive Grants
    • +
    • + + My Account + +
    • +
    +
    +
    + ); +}; -
    -
    -
      -
    • All Grants
    • -
    • My Grants
    • -
    • Active Grants
    • -
    • Inactive Grants
    • -
    • pfp
    • -
    -
    -
    - ) -} - -export default Header; \ No newline at end of file +export default Header; diff --git a/frontend/tests/Mock.test.tsx b/frontend/tests/Mock.test.tsx index db81f968..933c2f77 100644 --- a/frontend/tests/Mock.test.tsx +++ b/frontend/tests/Mock.test.tsx @@ -1,22 +1,21 @@ -import '@testing-library/jest-dom'; // Enables 'toBeInDocument()' -import React from 'react'; -import { describe, it, expect, vi, type Mock } from 'vitest'; -import { render, screen, fireEvent } from '@testing-library/react'; - -import Dashboard from '../src/Dashboard'; -import { AuthProvider } from '../src/context/auth/authContext'; +import "@testing-library/jest-dom"; // Enables 'toBeInDocument()' +import React from "react"; +import { describe, it, expect, vi, type Mock } from "vitest"; +import { render, screen, fireEvent } from "@testing-library/react"; +import Account from "../src/Account"; +import { AuthProvider } from "../src/context/auth/authContext"; // Import the modules we'll mock: -import * as storeModule from '../src/external/bcanSatchel/store'; -import * as actionsModule from '../src/external/bcanSatchel/actions'; +import * as storeModule from "../src/external/bcanSatchel/store"; +import * as actionsModule from "../src/external/bcanSatchel/actions"; // “flips the switch” and says “don’t use real code from this path.” -vi.mock('../src/external/bcanSatchel/store', () => ({ +vi.mock("../src/external/bcanSatchel/store", () => ({ getAppStore: vi.fn(), })); -vi.mock('../src/external/bcanSatchel/actions', () => ({ +vi.mock("../src/external/bcanSatchel/actions", () => ({ logoutUser: vi.fn(), })); @@ -33,28 +32,29 @@ vi.mock('../src/external/bcanSatchel/store', () => ({ This would remove the need to mock the return value again in the actual test. But, that means you have a single static return value for getStore in all tests. Often, you need different behaviors in different tests. */ -describe('Dashboard component', () => { - it('renders user info from store and calls logout on click', () => { - +describe("Account component", () => { + it("renders user info from store and calls logout on click", () => { // Setup const getStoreMock = storeModule.getAppStore as Mock; getStoreMock.mockReturnValue({ // simulate store content: isAuthenticated: true, - accessToken: 'fake-token', - user: { userId: 'TestUser123', email: 'test@example.com', biography: '' }, + accessToken: "fake-token", + user: { userId: "TestUser123", email: "test@example.com", biography: "" }, }); // Act 1 - render( - - ); + render( + + + + ); // Eval 1 - expect(screen.getByText('Welcome, TestUser123')).toBeInTheDocument(); + expect(screen.getByText("Welcome, TestUser123")).toBeInTheDocument(); // Act 2 - fireEvent.click(screen.getByRole('button', { name: /logout/i })); + fireEvent.click(screen.getByRole("button", { name: /logout/i })); // Eval 2 expect(actionsModule.logoutUser).toHaveBeenCalledTimes(1); diff --git a/frontend/tsconfig.app.tsbuildinfo b/frontend/tsconfig.app.tsbuildinfo index 48c6c21e..ef2c036e 100644 --- a/frontend/tsconfig.app.tsbuildinfo +++ b/frontend/tsconfig.app.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/app.tsx","./src/dashboard.tsx","./src/login.tsx","./src/newuser.tsx","./src/authcontext.tsx","./src/main.tsx","./src/vite-env.d.ts"],"version":"5.6.2"} \ No newline at end of file +{"root":["./src/app.tsx","./src/account.tsx","./src/login.tsx","./src/newuser.tsx","./src/authcontext.tsx","./src/main.tsx","./src/vite-env.d.ts"],"version":"5.6.2"} \ No newline at end of file