++
- Can now join games - Can continue games using table - Can create accounts - Can create Games
This commit is contained in:
Binary file not shown.
+27
-8
@@ -68,7 +68,7 @@ app.post('/login', (req, res) => {
|
|||||||
if (!row || !(await bcrypt.compare(password, row.password))) {
|
if (!row || !(await bcrypt.compare(password, row.password))) {
|
||||||
return res.status(400).json({ error: 'Invalid username or password.' });
|
return res.status(400).json({ error: 'Invalid username or password.' });
|
||||||
}
|
}
|
||||||
res.json({ message: 'Login successful!', userId: row.id });
|
res.json({ success: true, message: 'Login successful!', userId: row.id });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -87,20 +87,40 @@ app.get('/games/:userId', (req, res) => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// Join Game
|
// Join Game
|
||||||
app.post('/joinGame/:gameId', (req, res) => {
|
app.post('/joinGame/:gameId', (req, res) => {
|
||||||
const gameId = req.params.gameId;
|
const gameId = req.params.gameId;
|
||||||
const userId = req.body.userId;
|
const userId = req.body.userId;
|
||||||
|
|
||||||
const stmt = db.prepare('INSERT INTO games (participants) VALUES (?) WHERE game_id IS (?)');
|
db.get('SELECT * FROM games WHERE game_id = ?', [gameId], (err, game) => {
|
||||||
stmt.run([gameId, userId], function(err) {
|
|
||||||
if (err) {
|
if (err) {
|
||||||
return res.status(400).json({ error: 'Failed to create game.' });
|
return res.status(500).json({ error: 'Internal server error' });
|
||||||
}
|
}
|
||||||
res.status(201).json({message: "User joined Game!", gameId: this.gameId})
|
if (!game) {
|
||||||
})
|
return res.status(404).json({ error: 'Game not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const participants = JSON.parse(game.participants);
|
||||||
|
const gameMasterId = game.game_master_id;
|
||||||
|
|
||||||
|
// Check if the user is already a participant or the game master
|
||||||
|
if (gameMasterId === userId || participants.includes(userId)) {
|
||||||
|
return res.status(400).json({ error: 'User is already a participant or the game master.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
participants.push(userId);
|
||||||
|
|
||||||
|
const stmt = db.prepare('UPDATE games SET participants = ? WHERE game_id = ?');
|
||||||
|
stmt.run([JSON.stringify(participants), gameId], function (err) {
|
||||||
|
if (err) {
|
||||||
|
return res.status(400).json({ error: 'Failed to join game.' });
|
||||||
|
}
|
||||||
|
res.status(201).json({ message: 'User joined game successfully!', gameId: gameId });
|
||||||
|
});
|
||||||
stmt.finalize();
|
stmt.finalize();
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
// Create a new game
|
// Create a new game
|
||||||
@@ -119,8 +139,7 @@ app.post('/games', (req, res) => {
|
|||||||
app.post('/createGame', (req, res) => {
|
app.post('/createGame', (req, res) => {
|
||||||
const { name, description, gameMasterId, participants } = req.body;
|
const { name, description, gameMasterId, participants } = req.body;
|
||||||
|
|
||||||
// Generate an 8-character alphanumeric ID
|
const gameId = crypto.randomBytes(4).toString('hex');
|
||||||
const gameId = crypto.randomBytes(4).toString('hex'); // 8 characters (4 bytes * 2 hex chars per byte)
|
|
||||||
|
|
||||||
const stmt = db.prepare('INSERT INTO games (game_id, name, description, game_master_id, participants) VALUES (?, ?, ?, ?, ?)');
|
const stmt = db.prepare('INSERT INTO games (game_id, name, description, game_master_id, participants) VALUES (?, ?, ?, ?, ?)');
|
||||||
stmt.run([gameId, name, description, gameMasterId, JSON.stringify(participants)], function (err) {
|
stmt.run([gameId, name, description, gameMasterId, JSON.stringify(participants)], function (err) {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
|
"react-helmet": "^6.1.0",
|
||||||
"react-router-dom": "^7.0.2"
|
"react-router-dom": "^7.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ body {
|
|||||||
margin: 20px;
|
margin: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
background-color: #764ACB;
|
background-color: #764ACB;
|
||||||
color: #f0f0f5;
|
color: #f0f0f5;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
|
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
|
||||||
|
import { Helmet } from 'react-helmet';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
import Header from './components/header';
|
import Header from './components/header';
|
||||||
import Home from './pages/home';
|
import Home from './pages/home';
|
||||||
@@ -7,6 +8,7 @@ import Login from './pages/login';
|
|||||||
import Register from './pages/register';
|
import Register from './pages/register';
|
||||||
import JoinGame from './pages/joinGame';
|
import JoinGame from './pages/joinGame';
|
||||||
import StartGame from './pages/startGame';
|
import StartGame from './pages/startGame';
|
||||||
|
import { UserProvider } from './context/UserContext.jsx';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
||||||
@@ -18,11 +20,14 @@ function App() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<UserProvider>
|
||||||
|
<meta name="DND Master" content="WoW. A description."/>
|
||||||
<Router>
|
<Router>
|
||||||
<div>
|
<div>
|
||||||
|
<Helmet>
|
||||||
|
<title>DND Master</title>
|
||||||
|
</Helmet>
|
||||||
{popupMessage && <div className="popup">{popupMessage}</div>}
|
{popupMessage && <div className="popup">{popupMessage}</div>}
|
||||||
|
|
||||||
{/* Header is placed at the top of the page */}
|
|
||||||
<Header isLoggedIn={isLoggedIn} />
|
<Header isLoggedIn={isLoggedIn} />
|
||||||
|
|
||||||
<Routes>
|
<Routes>
|
||||||
@@ -52,6 +57,7 @@ function App() {
|
|||||||
</Routes>
|
</Routes>
|
||||||
</div>
|
</div>
|
||||||
</Router>
|
</Router>
|
||||||
|
</UserProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import React, { createContext, useState } from 'react';
|
||||||
|
|
||||||
|
export const UserContext = createContext();
|
||||||
|
|
||||||
|
export const UserProvider = ({ children }) => {
|
||||||
|
const [userId, setUserId] = useState(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UserContext.Provider value={{ userId, setUserId }}>
|
||||||
|
{children}
|
||||||
|
</UserContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
.container {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
align-self: center;
|
||||||
|
flex-direction: row;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group .btn {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding: 10px 20px;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-container {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.games-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
background-color: #333;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.games-table th, .games-table td {
|
||||||
|
border: 1px solid #555;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.games-table th {
|
||||||
|
background-color: #444;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.games-table tr:nth-child(even) {
|
||||||
|
background-color: #3a3a3a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.games-table tr:hover {
|
||||||
|
background-color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-link {
|
||||||
|
color: #1e90ff;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
@@ -1,8 +1,25 @@
|
|||||||
import React from 'react';
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate, Link } from 'react-router-dom';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { UserContext } from '../context/UserContext';
|
||||||
|
import './home.css';
|
||||||
|
|
||||||
function Home({ isLoggedIn, setIsLoggedIn }) {
|
function Home({ isLoggedIn, setIsLoggedIn }) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const [games, setGames] = useState([]);
|
||||||
|
const { userId } = useContext(UserContext);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isLoggedIn && userId) {
|
||||||
|
axios.get(`http://localhost:5000/games/${userId}`)
|
||||||
|
.then(response => {
|
||||||
|
setGames(response.data);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error fetching games:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [isLoggedIn, userId]);
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
setIsLoggedIn(false);
|
setIsLoggedIn(false);
|
||||||
@@ -10,18 +27,45 @@ function Home({ isLoggedIn, setIsLoggedIn }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container">
|
<div className="container">
|
||||||
<h2>Welcome to the Game</h2>
|
<h2>Welcome to the Site</h2>
|
||||||
{!isLoggedIn ? (
|
{!isLoggedIn ? (
|
||||||
<div className="button-group">
|
<div className="button-group">
|
||||||
<button className="btn" onClick={() => navigate('/login')}>Login</button>
|
<button className="btn" onClick={() => navigate('/login')}>Login</button>
|
||||||
<button className="btn" onClick={() => navigate('/register')}>Register</button>
|
<button className="btn" onClick={() => navigate('/register')}>Register</button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
<div className="content">
|
||||||
<div className="button-group">
|
<div className="button-group">
|
||||||
<button className="btn" onClick={() => navigate('/startGame')}>Start Game</button>
|
<button className="btn" onClick={() => navigate('/startGame')}>Start Game</button>
|
||||||
<button className="btn" onClick={() => navigate('/joinGame')}>Join Game</button>
|
<button className="btn" onClick={() => navigate('/joinGame')}>Join Game</button>
|
||||||
<button className="btn" onClick={handleLogout}>Logout</button>
|
<button className="btn" onClick={handleLogout}>Logout</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="table-container">
|
||||||
|
<h3>Your Games</h3>
|
||||||
|
<table className="games-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Game ID</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Description</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{games.map(game => (
|
||||||
|
<tr key={game.game_id}>
|
||||||
|
<td>
|
||||||
|
<Link to={`/games/${game.game_id}`} className="game-link">
|
||||||
|
{game.game_id}
|
||||||
|
</Link>
|
||||||
|
</td>
|
||||||
|
<td>{game.name}</td>
|
||||||
|
<td>{game.description}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,20 +1,16 @@
|
|||||||
|
import React, { useState, useContext } from 'react';
|
||||||
import React, { useState } from 'react';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { UserContext } from '../context/UserContext';
|
||||||
|
|
||||||
const JoinGamePage = () => {
|
const JoinGamePage = () => {
|
||||||
const [gameCode, setGameCode] = useState('');
|
const [gameCode, setGameCode] = useState('');
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { userId } = useContext(UserContext);
|
||||||
// Mock function to check if game exists (replace with your real logic)
|
|
||||||
const checkGameExists = (code) => {
|
|
||||||
const availableGames = ['game123', 'game456', 'game789']; // Example game codes
|
|
||||||
return availableGames.includes(code);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle form submission
|
// Handle form submission
|
||||||
const handleJoinGame = (e) => {
|
const handleJoinGame = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (gameCode.trim() === '') {
|
if (gameCode.trim() === '') {
|
||||||
@@ -22,14 +18,16 @@ const JoinGamePage = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (checkGameExists(gameCode)) {
|
try {
|
||||||
// Simulate adding user to the game (replace with your actual logic)
|
const response = await axios.post(`http://localhost:5000/joinGame/${gameCode}`, { userId });
|
||||||
|
if (response.status === 201) {
|
||||||
setError('');
|
setError('');
|
||||||
setTimeout(() => {
|
navigate(`/games/${gameCode}`); // Redirect to the games page
|
||||||
navigate('/games'); // Redirect to the games page
|
|
||||||
}, 1000);
|
|
||||||
} else {
|
} else {
|
||||||
setError('Game not found!');
|
setError('Failed to join the game.');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.response ? err.response.data.error : 'Error joining the game');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,29 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState, useContext } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import { UserContext } from '../context/UserContext';
|
||||||
|
|
||||||
function Login({ setIsLoggedIn }) {
|
function Login({ setIsLoggedIn }) {
|
||||||
const [username, setUsername] = useState('');
|
const [username, setUsername] = useState('');
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [message, setMessage] = useState('');
|
const [message, setMessage] = useState('');
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { setUserId } = useContext(UserContext);
|
||||||
|
|
||||||
const handleLogin = async (e) => {
|
const handleLogin = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
try {
|
try {
|
||||||
const res = await axios.post('http://localhost:5000/login', { username, password });
|
const res = await axios.post('http://localhost:5000/login', { username, password });
|
||||||
setMessage(res.data.message);
|
setMessage(res.data.message);
|
||||||
|
if (res.data.success) {
|
||||||
setIsLoggedIn(true);
|
setIsLoggedIn(true);
|
||||||
|
setUserId(res.data.userId);
|
||||||
navigate('/');
|
navigate('/');
|
||||||
|
} else {
|
||||||
|
setMessage(res.data.message);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setMessage(err.response.data.error);
|
setMessage(err.response ? err.response.data.error : 'Error logging in');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ const startGame = () => {
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
gameMasterId: 1, // Placeholder for game master ID; replace with dynamic value if needed
|
gameMasterId: 1,
|
||||||
participants: [], // Default empty participants array
|
participants: [],
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+21
-1
@@ -1858,7 +1858,7 @@ prelude-ls@^1.2.1:
|
|||||||
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
|
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
|
||||||
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
|
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
|
||||||
|
|
||||||
prop-types@^15.8.1:
|
prop-types@^15.7.2, prop-types@^15.8.1:
|
||||||
version "15.8.1"
|
version "15.8.1"
|
||||||
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
|
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
|
||||||
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
|
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
|
||||||
@@ -1885,6 +1885,21 @@ react-dom@^18.3.1:
|
|||||||
loose-envify "^1.1.0"
|
loose-envify "^1.1.0"
|
||||||
scheduler "^0.23.2"
|
scheduler "^0.23.2"
|
||||||
|
|
||||||
|
react-fast-compare@^3.1.1:
|
||||||
|
version "3.2.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.2.tgz#929a97a532304ce9fee4bcae44234f1ce2c21d49"
|
||||||
|
integrity sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==
|
||||||
|
|
||||||
|
react-helmet@^6.1.0:
|
||||||
|
version "6.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-helmet/-/react-helmet-6.1.0.tgz#a750d5165cb13cf213e44747502652e794468726"
|
||||||
|
integrity sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw==
|
||||||
|
dependencies:
|
||||||
|
object-assign "^4.1.1"
|
||||||
|
prop-types "^15.7.2"
|
||||||
|
react-fast-compare "^3.1.1"
|
||||||
|
react-side-effect "^2.1.0"
|
||||||
|
|
||||||
react-is@^16.13.1:
|
react-is@^16.13.1:
|
||||||
version "16.13.1"
|
version "16.13.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||||
@@ -1912,6 +1927,11 @@ react-router@7.0.2:
|
|||||||
set-cookie-parser "^2.6.0"
|
set-cookie-parser "^2.6.0"
|
||||||
turbo-stream "2.4.0"
|
turbo-stream "2.4.0"
|
||||||
|
|
||||||
|
react-side-effect@^2.1.0:
|
||||||
|
version "2.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-side-effect/-/react-side-effect-2.1.2.tgz#dc6345b9e8f9906dc2eeb68700b615e0b4fe752a"
|
||||||
|
integrity sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw==
|
||||||
|
|
||||||
react@^18.3.1:
|
react@^18.3.1:
|
||||||
version "18.3.1"
|
version "18.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891"
|
resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891"
|
||||||
|
|||||||
Reference in New Issue
Block a user