Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

User profile #118

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions gatewayservice/gateway-service.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,27 @@ app.get('/rankings/:filter', async (req, res) => {
}
});


app.get('/ranking/user', async (req, res) => {
const username = req.query.username;
const category = req.query.category;

try {
// Forward the request to the user service
const result = await axios.get(`${userServiceUrl}/ranking/user`, {
params: {
username: username,
category: category
}
});

res.json(result.data);

} catch (error) {
res.status(error.response.status).json({ error: error.response.data.error });
}
});

// Read the OpenAPI YAML file synchronously
openapiPath='./openapi.yaml'
if (fs.existsSync(openapiPath)) {
Expand Down
32 changes: 32 additions & 0 deletions users/userservice/user-service.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,38 @@ app.get('/rankings/:filter', async (req, res) => {
})


// Get the ranking info for a specified category and user
app.get('/ranking/user', async (req, res) => {
const username = req.query.username;
const category = req.query.category;

try {
// Fetch the user with the specified username
const user = await User.findOne({ username });

// If user not found, return error
if (!user) {
return res.status(400).json("Error: User not found");
}

// Extract ranking info for the specified category
const rankingInfo = {
username: user.username,
category,
points: user.ranking[category].points,
questions: user.ranking[category].questions,
correct: user.ranking[category].correct,
wrong: user.ranking[category].wrong
};

res.status(200).json(rankingInfo);
} catch (error) {
res.status(400).json({ error: error.message });
}
});



app.post("/addpoints", async (req, res) => {
const username = req.body.username;
const category = req.body.category;
Expand Down
2 changes: 1 addition & 1 deletion webapp/e2e/steps/register-form.steps.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ defineFeature(feature, test => {
beforeAll(async () => {
browser = process.env.GITHUB_ACTIONS
? await puppeteer.launch()
: await puppeteer.launch({ headless: false, slowMo:5 });
: await puppeteer.launch({ headless: false, slowMo:60 });
page = await browser.newPage();
//Way of setting up the timeout
setDefaultOptions({ timeout: 10000 })
Expand Down
2 changes: 2 additions & 0 deletions webapp/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import Login from './components/Login';
import RankingsLayout from './components/ranking/RankingLayout';
import Game from './components/Game';
import MainPage from './components/MainPage';
import UserProfile from './components/UserProfile';

function App() {
// const isAuthenticated = useIsAuthenticated() // True if user has logged in
Expand All @@ -23,6 +24,7 @@ function App() {
<Route path='/register' element={<AddUser />} />
<Route path='/rankings' element={<RankingsLayout />} />
<Route path='/play' element={<Game />} />
<Route path='/userprofile' element={<UserProfile />} />
</Routes>
</BrowserRouter>
)
Expand Down
9 changes: 7 additions & 2 deletions webapp/src/components/Navbar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,14 @@ function Navbar() {
<ul className="md:flex items-center justify-between text-base text-gray-700 pt-4 md:pt-0">
<li><Link className="md:p-4 py-3 px-0 block font-bold text-gray-600 hover:text-gray-900" to='/play' >Play</Link></li>
<li><Link className="md:p-4 py-3 px-0 block font-bold text-gray-600 hover:text-gray-900" to="/rankings">Rankings</Link></li>
{isAuthenticated() ? <button class="px-8 py-4 bg-gradient-to-r from-blue-500 to-purple-500 text-white font-bold rounded-full transition-transform transform-gpu hover:-translate-y-1 hover:shadow-lg" onClick={() => Logout()}>

{isAuthenticated() && (
<li><Link className="md:p-4 py-3 px-0 block font-bold text-gray-600 hover:text-gray-900" to="/userprofile">UserProfile</Link></li>
)}
{isAuthenticated() ? <button className="px-8 py-4 bg-gradient-to-r from-blue-500 to-purple-500 text-white font-bold rounded-full transition-transform transform-gpu hover:-translate-y-1 hover:shadow-lg" onClick={() => Logout()}>
Logout
</button> : <li><Link className="md:p-4 py-3 px-0 block font-bold text-sky-500 hover:text-sky-800" to="/login">Sign in</Link></li>}
</button>
: <li><Link className="md:p-4 py-3 px-0 block font-bold text-sky-500 hover:text-sky-800" to="/login">Sign in</Link></li>}
</ul>
</nav>
</div>
Expand Down
47 changes: 47 additions & 0 deletions webapp/src/components/Navbar.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import Navbar from './Navbar';
import useIsAuthenticated from 'react-auth-kit/hooks/useIsAuthenticated';
import useSignOut from 'react-auth-kit/hooks/useSignOut';

jest.mock('react-auth-kit/hooks/useIsAuthenticated');
jest.mock('react-auth-kit/hooks/useSignOut');

describe('Navbar', () => {

beforeEach(() => {
// Reset mock function calls before each test
jest.clearAllMocks();
});

it('renders authenticated user links and logout button', () => {
useIsAuthenticated.mockReturnValue(() => true);
const { getByText } = render(
<MemoryRouter>
<Navbar />
</MemoryRouter>
);

expect(getByText('WIQ')).toBeInTheDocument();
expect(getByText('Play')).toBeInTheDocument();
expect(getByText('Rankings')).toBeInTheDocument();
expect(getByText('UserProfile')).toBeInTheDocument();
expect(getByText('Logout')).toBeInTheDocument();
});

it('renders unauthenticated user links and sign-in link', () => {
useIsAuthenticated.mockReturnValue(() => false);
const { getByText } = render(
<MemoryRouter>
<Navbar />
</MemoryRouter>
);

expect(getByText('WIQ')).toBeInTheDocument();
expect(getByText('Play')).toBeInTheDocument();
expect(getByText('Rankings')).toBeInTheDocument();
expect(getByText('Sign in')).toBeInTheDocument();
});

});
75 changes: 75 additions & 0 deletions webapp/src/components/UserProfile.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import React, { useEffect, useState, useCallback } from "react";
import axios from "axios";
import useAuthUser from "react-auth-kit/hooks/useAuthUser";
import userAvatar from "./ranking/profile_img.webp";

const UserProfile = () => {
const auth = useAuthUser();
const [category, setCategory] = useState('global');
const [questions, setQuestions] = useState(0);
const [right, setRight] = useState(0);
const [wrong, setWrong] = useState(0);

const apiEndpoint = process.env.REACT_APP_API_ENDPOINT ||'http://localhost:8000';


const handleCategoryChange = useCallback(async (newCategory) => {
try {
const result = await axios.get(`${apiEndpoint}/ranking/user`, {
params: {
username: auth.username,
category: newCategory
}
});

setQuestions(result.data.questions);
setRight(result.data.correct);
setWrong(result.data.wrong);
setCategory(newCategory);
} catch (error) {
console.error('Error fetching question:', error);
}
}, [apiEndpoint, auth.username]);

useEffect(() => {
handleCategoryChange(category);
}, [handleCategoryChange, category]);


return (
<div className="user-profile">
<div className="user-details">
<div className="user-info">
<img src={userAvatar} alt="User Avatar" className="user-avatar" style={{ width: "300px", height: "300px" }} />
<h2 className="md:p-4 py-3 px-0 font-bold text-gray-600 hover:text-gray-900">Username: {auth.username}</h2>

<p>Email: {auth.email}</p>
<p>Joined: {new Date(auth.createdAt).toLocaleDateString()}</p>


{/* Buttons to change category */}
<div className="category-buttons" style={{ display: 'flex', justifyContent: 'center', marginTop: '10px' }}>
<button style={{ marginRight: '8px', color: 'white', backgroundColor: 'purple', border: 'none', borderRadius: '20px', padding: '10px 20px', cursor: 'pointer' }} onClick={() => handleCategoryChange('global')}>Global</button>
<button style={{ marginRight: '8px', color: 'white', backgroundColor: 'purple', border: 'none', borderRadius: '20px', padding: '10px 20px', cursor: 'pointer' }} onClick={() => handleCategoryChange('flags')}>Flags</button>
<button style={{ marginRight: '8px', color: 'white', backgroundColor: 'purple', border: 'none', borderRadius: '20px', padding: '10px 20px', cursor: 'pointer' }} onClick={() => handleCategoryChange('cities')}>Cities</button>
<button style={{ marginRight: '8px', color: 'white', backgroundColor: 'purple', border: 'none', borderRadius: '20px', padding: '10px 20px', cursor: 'pointer' }} onClick={() => handleCategoryChange('monuments')}>Monuments</button>
<button style={{ marginRight: '8px', color: 'white', backgroundColor: 'purple', border: 'none', borderRadius: '20px', padding: '10px 20px', cursor: 'pointer' }} onClick={() => handleCategoryChange('tourist_attractions')}>Tourist Attractions</button>
<button style={{ color: 'white', backgroundColor: 'purple', border: 'none', borderRadius: '20px', padding: '10px 20px', cursor: 'pointer' }} onClick={() => handleCategoryChange('foods')}>Foods</button>
</div>

<div className="ranking" style={{ textAlign: 'center' }}>
<h3 style={{ marginBottom: '10px', color: 'purple' }}>{category} Ranking</h3>
<div style={{ backgroundColor: '#f4f4f4', padding: '10px', borderRadius: '8px', display: 'inline-block' }}>
<p style={{ marginBottom: '5px' }}>Total Answered Questions: {questions}</p>
<p style={{ marginBottom: '5px', color: 'green' }}>Right Answers: {right}</p>
<p style={{ marginBottom: '5px', color: 'red' }}>Wrong Answers: {wrong}</p>
</div>
</div>

</div>
</div>
</div >
);
};

export default UserProfile;
59 changes: 59 additions & 0 deletions webapp/src/components/UserProfile.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import React from 'react';
import { render, waitFor, fireEvent } from '@testing-library/react';
import UserProfile from './UserProfile';
import axios from 'axios';
import useAuthUser from 'react-auth-kit/hooks/useAuthUser';

jest.mock('axios');
jest.mock('react-auth-kit/hooks/useAuthUser', () => jest.fn());

describe('UserProfile', () => {
beforeEach(() => {
useAuthUser.mockReturnValue({
username: 'testUser',
email: '[email protected]',
createdAt: '2024-01-01T00:00:00Z',
});
});

it('renders user details', async () => {
axios.get.mockResolvedValueOnce({
data: {
questions: 10,
correct: 7,
wrong: 3,
},
});

const { getByText } = render(<UserProfile />);

await waitFor(() => {
expect(getByText('Username: testUser')).toBeInTheDocument();
expect(getByText('Email: [email protected]')).toBeInTheDocument();
expect(getByText('Joined: 1/1/2024')).toBeInTheDocument();
expect(getByText('Total Answered Questions: 10')).toBeInTheDocument();
expect(getByText('Right Answers: 7')).toBeInTheDocument();
expect(getByText('Wrong Answers: 3')).toBeInTheDocument();
});
});

it('changes category when category button is clicked', async () => {
axios.get.mockResolvedValueOnce({
data: {
questions: 5,
correct: 3,
wrong: 2,
},
});

const { getByText } = render(<UserProfile />);

fireEvent.click(getByText('Flags'));

await waitFor(() => {
expect(getByText('Total Answered Questions: 5')).toBeInTheDocument();
expect(getByText('Right Answers: 3')).toBeInTheDocument();
expect(getByText('Wrong Answers: 2')).toBeInTheDocument();
});
});
});
Binary file added webapp/src/components/ranking/profile_img.webp
Binary file not shown.