This commit is contained in:
QkoSad
2023-08-08 16:02:54 +03:00
commit 0a7a469d56
315 changed files with 426907 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
missing the test exercises 5.12 -5.23
+30028
View File
File diff suppressed because it is too large Load Diff
+47
View File
@@ -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

+26
View File
@@ -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

+25
View File
@@ -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"
}
+3
View File
@@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:
+31
View File
@@ -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;
+57
View File
@@ -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;
+22
View File
@@ -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;
+51
View File
@@ -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;
+27
View File
@@ -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;
+27
View File
@@ -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;
+23
View File
@@ -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;
+43
View File
@@ -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 };
+9
View File
@@ -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 }
+13
View File
@@ -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 };