renamed
This commit is contained in:
@@ -0,0 +1 @@
|
||||
missing the test exercises 5.12 -5.23
|
||||
Generated
+30028
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@reduxjs/toolkit": "^1.9.5",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"axios": "^1.2.3",
|
||||
"bootstrap": "^5.3.0",
|
||||
"react": "^18.2.0",
|
||||
"react-bootstrap": "^2.7.4",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-redux": "^8.1.1",
|
||||
"react-router-dom": "^6.13.0",
|
||||
"react-scripts": "5.0.1",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"proxy": "http://localhost:3003",
|
||||
"devDependencies": {
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^14.0.0"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
@@ -0,0 +1,26 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css"
|
||||
integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
<title>React App</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 5.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 9.4 KiB |
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
@@ -0,0 +1,31 @@
|
||||
import { useState } from "react";
|
||||
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
|
||||
import Login from "./components/Login";
|
||||
import Blogs from "./components/Blogs";
|
||||
import Users from "./components/Users";
|
||||
import User from "./components/User";
|
||||
import Blog from "./components/Blog";
|
||||
import Notification from "./components/Notification";
|
||||
import Navigation from "./components/Navigation";
|
||||
|
||||
const App = () => {
|
||||
const [user, setUser] = useState(null);
|
||||
return (
|
||||
<div className="container">
|
||||
<Router>
|
||||
<Navigation user={user} setUser={setUser} />
|
||||
<Notification />
|
||||
{!user && <Login setUser={setUser} />}
|
||||
{user && <Blogs />}
|
||||
<Routes>
|
||||
<Route path="/" element={<> </>} />
|
||||
<Route path="/users" element={<Users />} />
|
||||
<Route path="/user/:id" element={<User />} />
|
||||
<Route path="/blog/:id" element={<Blog />} />
|
||||
</Routes>
|
||||
</Router>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,57 @@
|
||||
import { useSelector } from "react-redux";
|
||||
import { useParams } from "react-router-dom";
|
||||
import blogService from "../services/blogs";
|
||||
import { likeBlog, addComment } from "../reducers/blogReducer";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { useState } from "react";
|
||||
|
||||
const Blog = () => {
|
||||
const id = useParams().id;
|
||||
const blogs = useSelector((state) => state.blogs);
|
||||
const blog = blogs.find((e) => e.id === id);
|
||||
const dispatch = useDispatch();
|
||||
const increseLikes = async () => {
|
||||
const blogResp = await blogService.changeBlog(blog.id, {
|
||||
...blog,
|
||||
likes: blog.likes + 1,
|
||||
user: blog.user.id,
|
||||
});
|
||||
|
||||
dispatch(likeBlog(blogResp));
|
||||
};
|
||||
const [comment, setComment] = useState("");
|
||||
const onSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
const blogResp = await blogService.changeBlog(blog.id, {
|
||||
...blog,
|
||||
comments: blog.comments.concat(comment),
|
||||
user: blog.user.id,
|
||||
})
|
||||
dispatch(addComment({ id: blogResp.id, comment }));
|
||||
setComment("");
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<h2>{blog.title}</h2>
|
||||
<div>{blog.url}</div>
|
||||
<div>
|
||||
{blog.likes}
|
||||
<button onClick={increseLikes}>Like</button>
|
||||
</div>
|
||||
<div>Added by {blog.author}</div>
|
||||
<h3>comments </h3>
|
||||
<form onSubmit={onSubmit}>
|
||||
<input onChange={(e) => setComment(e.target.value)} value={comment} />
|
||||
<button>add commnet</button>
|
||||
</form>
|
||||
{blog.comments?.length ? (
|
||||
<ul>
|
||||
{blog.comments?.map((el, indx) => {
|
||||
return <li key={indx}>{el}</li>;
|
||||
})}
|
||||
</ul>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default Blog;
|
||||
@@ -0,0 +1,58 @@
|
||||
import { useState } from "react";
|
||||
import { useDispatch} from "react-redux";
|
||||
import blogService from "../services/blogs";
|
||||
import {
|
||||
createNotification,
|
||||
removeNotification,
|
||||
} from "../reducers/notificationReducer";
|
||||
import { createBlog } from "../reducers/blogReducer";
|
||||
|
||||
const BlogFrom = () => {
|
||||
const [title, setTitle] = useState("");
|
||||
const [url, setUrl] = useState("");
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const createNewBlogSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
const blog = await blogService.create({ title, url });
|
||||
dispatch(createBlog(blog));
|
||||
setTitle("");
|
||||
setUrl("");
|
||||
dispatch(createNotification("new blog created"));
|
||||
|
||||
setTimeout(() => {
|
||||
dispatch(removeNotification());
|
||||
}, 5000);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<form onSubmit={createNewBlogSubmit}>
|
||||
<h2>create new</h2>
|
||||
<div>
|
||||
title
|
||||
<input
|
||||
name="Title"
|
||||
type="text"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
url
|
||||
<input
|
||||
name="Url"
|
||||
type="text"
|
||||
value={url}
|
||||
onChange={(e) => setUrl(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<button type="submit">create</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default BlogFrom;
|
||||
@@ -0,0 +1,22 @@
|
||||
import { useSelector } from "react-redux";
|
||||
import BlogForm from "./BlogForm";
|
||||
import { Link } from "react-router-dom";
|
||||
import Togglable from "./Togglable";
|
||||
|
||||
const Blogs = () => {
|
||||
const blogs = useSelector((state) => state.blogs);
|
||||
return (
|
||||
<>
|
||||
<Togglable buttonLabel1="create blog">
|
||||
<BlogForm blogs={blogs} />
|
||||
</Togglable>
|
||||
<div>
|
||||
{blogs.map((blog) => (
|
||||
<Link key={blog.id} to={`/blog/${blog.id}`}>{blog.title}</Link>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Blogs;
|
||||
@@ -0,0 +1,51 @@
|
||||
import loginService from "../services/login";
|
||||
import { useState } from "react";
|
||||
import blogService from "../services/blogs";
|
||||
import Form from "react-bootstrap/Form";
|
||||
import Button from "react-bootstrap/Button";
|
||||
|
||||
const Login = ({ setUser }) => {
|
||||
const [password, setPassword] = useState("");
|
||||
const [username, setUsername] = useState("");
|
||||
|
||||
const handleLogin = async (event) => {
|
||||
event.preventDefault();
|
||||
try {
|
||||
const user = await loginService.login({ username, password });
|
||||
blogService.setToken(user.token);
|
||||
localStorage.setItem("loggedBlogAppUser", JSON.stringify(user));
|
||||
setUsername("");
|
||||
setPassword("");
|
||||
setUser(user);
|
||||
} catch (err) {}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>Log in to application</div>
|
||||
<Form onSubmit={handleLogin}>
|
||||
<Form.Group>
|
||||
<Form.Label> username</Form.Label>
|
||||
<Form.Control
|
||||
type="text"
|
||||
value={username}
|
||||
name="Username"
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
<Form.Label>password</Form.Label>
|
||||
<Form.Control
|
||||
type="password"
|
||||
value={password}
|
||||
name="Password"
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
<Button variant="primary" type="submit">
|
||||
login
|
||||
</Button>
|
||||
</Form.Group>
|
||||
</Form>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
@@ -0,0 +1,57 @@
|
||||
import { Link } from "react-router-dom";
|
||||
import { setAllBlogs } from "../reducers/blogReducer";
|
||||
import blogService from "../services/blogs";
|
||||
import userService from "../services/user";
|
||||
import { setAllUsers } from "../reducers/userReducer";
|
||||
import { useEffect } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import Navbar from "react-bootstrap/Navbar";
|
||||
import Nav from "react-bootstrap/Nav";
|
||||
|
||||
const Navigation = ({ user, setUser }) => {
|
||||
const dispatch = useDispatch();
|
||||
const logOut = () => {
|
||||
localStorage.removeItem("loggedBlogAppUser");
|
||||
setUser(null);
|
||||
};
|
||||
useEffect(() => {
|
||||
const loggedUserJSON = localStorage.getItem("loggedBlogAppUser");
|
||||
if (loggedUserJSON) {
|
||||
const user = JSON.parse(loggedUserJSON);
|
||||
setUser(user);
|
||||
blogService.setToken(user.token);
|
||||
}
|
||||
}, [setUser]);
|
||||
useEffect(() => {
|
||||
blogService
|
||||
.getAll()
|
||||
.then((blogs) =>
|
||||
dispatch(setAllBlogs(blogs.sort((a, b) => b.likes - a.likes)))
|
||||
);
|
||||
}, [dispatch]);
|
||||
useEffect(() => {
|
||||
const getData = async () => {
|
||||
const users = await userService.getAll();
|
||||
dispatch(setAllUsers(users));
|
||||
};
|
||||
getData();
|
||||
}, [dispatch]);
|
||||
return (
|
||||
<Navbar collapseOnSelect expand="lg" bg="dark" variant="dark">
|
||||
<Nav className="me-auto">
|
||||
<Nav.Link as="span">
|
||||
<Link to={"/users"}>users</Link>
|
||||
</Nav.Link>
|
||||
<Nav.Link href="#" as="span">
|
||||
<Link to={"/"}>blogs</Link>
|
||||
</Nav.Link>
|
||||
<Nav.Link href="#" as="span">
|
||||
{user ? `logged in as ${user.name}` : ""}
|
||||
</Nav.Link>
|
||||
<button onClick={logOut}>logout</button>
|
||||
<h2>blog app</h2>
|
||||
</Nav>
|
||||
</Navbar>
|
||||
);
|
||||
};
|
||||
export default Navigation;
|
||||
@@ -0,0 +1,16 @@
|
||||
import React from "react";
|
||||
import "@tesing-library/jst-dom/extend-expect";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import Blog from "./Blog";
|
||||
|
||||
test("renders content", () => {
|
||||
const blog = {
|
||||
title: "Component testing is done with react-testing-library",
|
||||
};
|
||||
render(<Blog blog={blog} />);
|
||||
|
||||
const element = screen.getByText(
|
||||
"Component testing is done with react-testing-library"
|
||||
);
|
||||
expect(element).toBeDefined();
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import { useSelector} from "react-redux";
|
||||
|
||||
const Notification = () => {
|
||||
const notifications = useSelector((state) => state.notifications);
|
||||
|
||||
if (!notifications.length) return <></>;
|
||||
|
||||
const style = {listStyleType:'none'}
|
||||
return (
|
||||
<ul style={style}>
|
||||
{notifications.map((notification, index) => (
|
||||
<li key={index}>{notification}</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
|
||||
export default Notification;
|
||||
@@ -0,0 +1,23 @@
|
||||
import { useState } from "react";
|
||||
|
||||
const Togglable = ({ children, buttonLabel1, buttonLabel2 = "cancel" }) => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const hideWhenVisible = { display: visible ? "none" : "" };
|
||||
const showWhenVisible = { display: visible ? "" : "none" };
|
||||
const toggleVisibility = () => {
|
||||
setVisible(!visible);
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<div style={hideWhenVisible}>
|
||||
<button onClick={toggleVisibility}>{buttonLabel1}</button>
|
||||
</div>
|
||||
<div style={showWhenVisible}>
|
||||
{children}
|
||||
<button onClick={toggleVisibility}>{buttonLabel2}</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Togglable;
|
||||
@@ -0,0 +1,27 @@
|
||||
import { useEffect } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useParams, Link } from "react-router-dom";
|
||||
|
||||
const User = () => {
|
||||
const id = useParams().id;
|
||||
const users = useSelector((state) => state.users);
|
||||
const user = users.find((el) => el.id === id);
|
||||
const blogs = useSelector((state) => state.blogs);
|
||||
const userBlogs = blogs.filter(el=> el.user.id === user.id)
|
||||
|
||||
useEffect(() => {});
|
||||
return (
|
||||
<>
|
||||
<h2>{user.name}</h2>
|
||||
<h3>added blogs</h3>
|
||||
<ul>
|
||||
{userBlogs.map((blog, index) => (
|
||||
<li key={index}>
|
||||
<Link to={`/blog/${blog.id}`}>{blog.title}</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default User;
|
||||
@@ -0,0 +1,27 @@
|
||||
import { useSelector } from "react-redux";
|
||||
import { Link } from "react-router-dom";
|
||||
import Table from 'react-bootstrap/Table'
|
||||
|
||||
const Users = () => {
|
||||
const users = useSelector((state) => state.users);
|
||||
return (
|
||||
<Table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>blogs</th>
|
||||
</tr>
|
||||
<tr key={"top_level"}></tr>
|
||||
{users.map((user) => (
|
||||
<tr key={user.id}>
|
||||
<th>
|
||||
<Link to={`/user/${user.id}`}>{user.name}</Link>
|
||||
</th>
|
||||
<th>{user.blogs.length}</th>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
);
|
||||
};
|
||||
export default Users;
|
||||
@@ -0,0 +1,23 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "./App";
|
||||
import { configureStore } from "@reduxjs/toolkit";
|
||||
import userReducer from "./reducers/userReducer";
|
||||
import blogReducer from "./reducers/blogReducer";
|
||||
import { Provider } from "react-redux";
|
||||
import notificationReducer from "./reducers/notificationReducer";
|
||||
|
||||
const store = configureStore({
|
||||
reducer: {
|
||||
users: userReducer,
|
||||
blogs: blogReducer,
|
||||
notifications: notificationReducer,
|
||||
},
|
||||
});
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")).render(
|
||||
<Provider store={store}>
|
||||
<App />
|
||||
</Provider>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { createSlice } from "@reduxjs/toolkit";
|
||||
|
||||
const initialState = [];
|
||||
const blogReducer = createSlice({
|
||||
name: "blogs",
|
||||
initialState,
|
||||
reducers: {
|
||||
setAllBlogs: (state, action) => action.payload,
|
||||
createBlog: (state, action) => {
|
||||
state.push(action.payload);
|
||||
},
|
||||
removeBlog: (state, action) =>
|
||||
state.filter((el) => el.id !== action.payload.id),
|
||||
likeBlog: (state, action) => {
|
||||
const blog = state.find((el) => el.id === action.payload.id);
|
||||
const newblog = { ...blog, likes: blog.likes + 1 };
|
||||
state[state.indexOf(blog)] = newblog;
|
||||
},
|
||||
addComment: (state, action) => {
|
||||
const blog = state.find((el) => el.id === action.payload.id);
|
||||
console.log(action.payload);
|
||||
const newblog = {
|
||||
...blog,
|
||||
comments: [...blog.comments, action.payload.comment],
|
||||
};
|
||||
state[state.indexOf(blog)] = newblog;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default blogReducer.reducer;
|
||||
export const { addComment, setAllBlogs, createBlog, removeBlog, likeBlog } =
|
||||
blogReducer.actions;
|
||||
@@ -0,0 +1,18 @@
|
||||
import { createSlice } from "@reduxjs/toolkit";
|
||||
|
||||
const initialState = [];
|
||||
const NotificationReducer = createSlice({
|
||||
name: "notification",
|
||||
initialState,
|
||||
reducers: {
|
||||
createNotification: (state, action) => {
|
||||
state.push(action.payload);
|
||||
},
|
||||
removeNotification: (state, action) => {
|
||||
state.pop()
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default NotificationReducer.reducer;
|
||||
export const { removeNotification, createNotification } = NotificationReducer.actions;
|
||||
@@ -0,0 +1,14 @@
|
||||
import { createSlice } from "@reduxjs/toolkit";
|
||||
|
||||
const initialState = [];
|
||||
const userReducer = createSlice({
|
||||
name: "users",
|
||||
initialState,
|
||||
reducers: {
|
||||
createUser: (state, action) => {},
|
||||
setAllUsers: (state, action) => action.payload,
|
||||
},
|
||||
});
|
||||
|
||||
export default userReducer.reducer;
|
||||
export const { createUser ,setAllUsers} = userReducer.actions;
|
||||
@@ -0,0 +1,43 @@
|
||||
import axios from "axios";
|
||||
|
||||
const baseUrl = "/api/blogs";
|
||||
let token = null;
|
||||
const getBlog = async(id)=>{
|
||||
const res = await axios.get(`${baseUrl}/${id}`)
|
||||
return res.data
|
||||
}
|
||||
|
||||
const changeBlog = async (id, newObject) => {
|
||||
const config = {
|
||||
headers: { Authorization: token },
|
||||
};
|
||||
const res = await axios.put(baseUrl + "/" + id, newObject, config);
|
||||
return res.data;
|
||||
};
|
||||
|
||||
const getAll = async () => {
|
||||
const req = await axios.get(baseUrl);
|
||||
return req.data;
|
||||
};
|
||||
|
||||
const setToken = (newToken) => {
|
||||
token = `Bearer ${newToken}`;
|
||||
};
|
||||
|
||||
const create = async (newObject) => {
|
||||
const config = {
|
||||
headers: { Authorization: token },
|
||||
};
|
||||
const res = await axios.post(baseUrl, newObject, config);
|
||||
return res.data;
|
||||
};
|
||||
const deleteBlog = async (id) => {
|
||||
const config = {
|
||||
headers: { Authorization: token },
|
||||
};
|
||||
const res = await axios.delete(baseUrl + "/" + id, config);
|
||||
return res.data;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-anonymous-default-export
|
||||
export default { deleteBlog, changeBlog, getAll,getBlog, setToken, create };
|
||||
@@ -0,0 +1,9 @@
|
||||
import axios from 'axios'
|
||||
const baseUrl = '/api/login'
|
||||
|
||||
const login = async credentials => {
|
||||
const response = await axios.post(baseUrl, credentials)
|
||||
return response.data
|
||||
}
|
||||
|
||||
export default { login }
|
||||
@@ -0,0 +1,13 @@
|
||||
import axios from "axios";
|
||||
const baseUrl = "/api/users";
|
||||
|
||||
const getAll = async () => {
|
||||
const response = await axios.get(baseUrl);
|
||||
return response.data;
|
||||
};
|
||||
const getUser = async (id) => {
|
||||
const resp = await axios.get(`${baseUrl}/${id}`);
|
||||
return resp.data;
|
||||
};
|
||||
|
||||
export default { getAll, getUser };
|
||||
Reference in New Issue
Block a user