renamed
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
missing 8.23, on 8.24 useSubsriptions seems to not work no errors are thrown
|
||||
|
||||
Generated
+29637
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.7.16",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"graphql": "^16.7.1",
|
||||
"graphql-ws": "^5.14.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"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"
|
||||
]
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
@@ -0,0 +1,43 @@
|
||||
<!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" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>React App</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</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,76 @@
|
||||
import { Route, Routes, BrowserRouter, Link } from "react-router-dom";
|
||||
import { useApolloClient, useQuery, useSubscription } from "@apollo/client";
|
||||
import { useEffect, useState } from "react";
|
||||
import { ALL_AUTHORS, ALL_BOOKS, BOOK_ADDED, ME } from "./queries";
|
||||
import Authors from "./components/Authors";
|
||||
import Books from "./components/Books";
|
||||
import NewBook from "./components/NewBook";
|
||||
import Login from "./components/Login";
|
||||
import Recommended from "./components/Recommended";
|
||||
|
||||
const App = () => {
|
||||
const [token, setToken] = useState(null);
|
||||
const authorsRes = useQuery(ALL_AUTHORS);
|
||||
const booksRes = useQuery(ALL_BOOKS);
|
||||
const userRes = useQuery(ME);
|
||||
const client = useApolloClient();
|
||||
|
||||
useEffect(() => {
|
||||
if (localStorage.getItem("library-user-token"))
|
||||
setToken(localStorage.getItem("library-user-token"));
|
||||
}, [setToken]);
|
||||
|
||||
useSubscription(BOOK_ADDED, {
|
||||
onData: ({ data }) => {
|
||||
console.log(data);
|
||||
},
|
||||
});
|
||||
|
||||
const logOut = () => {
|
||||
setToken(null);
|
||||
localStorage.removeItem("library-user-token");
|
||||
client.resetStore();
|
||||
};
|
||||
|
||||
if (authorsRes.loading || booksRes.loading) return <div>loading...</div>;
|
||||
|
||||
if (!token) {
|
||||
return <Login token={token} setToken={setToken} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<Link to={"/authors"}>
|
||||
<button>authors</button>
|
||||
</Link>
|
||||
<Link to={"/books"}>
|
||||
<button>books</button>
|
||||
</Link>
|
||||
<Link to={"/add"}>
|
||||
<button>add-book</button>
|
||||
</Link>
|
||||
<Link to={"/recommended"}>
|
||||
<button>recommended</button>
|
||||
</Link>
|
||||
<button onClick={logOut}>logout</button>
|
||||
<Routes>
|
||||
<Route
|
||||
path="/authors"
|
||||
element={<Authors authors={authorsRes.data.authors} />}
|
||||
/>
|
||||
<Route path="/books" element={<Books books={booksRes.data.books} />} />
|
||||
<Route path="/add" element={<NewBook />} />
|
||||
<Route
|
||||
path="/login"
|
||||
element={<Login token={token} setToken={setToken} />}
|
||||
/>
|
||||
<Route
|
||||
path="/recommended"
|
||||
element={<Recommended user={userRes.data} />}
|
||||
/>
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,55 @@
|
||||
import { useState } from "react";
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { ADD_BIRTHYEAR } from "../queries";
|
||||
|
||||
const Authors = ({ authors }) => {
|
||||
const [addBirthyear] = useMutation(ADD_BIRTHYEAR);
|
||||
const [year, setYear] = useState("");
|
||||
const [name, setName] = useState("");
|
||||
|
||||
const setBirthyear = (e) => {
|
||||
e.preventDefault();
|
||||
addBirthyear({ variables: { birthyear: parseInt(year), name } });
|
||||
setYear("");
|
||||
setName("");
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>authors</h2>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>born</th>
|
||||
<th>books</th>
|
||||
</tr>
|
||||
{authors.map((a) => (
|
||||
<tr key={a.name}>
|
||||
<td>{a.name}</td>
|
||||
<td>{a.born}</td>
|
||||
<td>{a.bookCount}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<h2>set birthyear</h2>
|
||||
<form onSubmit={setBirthyear}>
|
||||
<label>name</label>
|
||||
<select value={name} onChange={(e) => setName(e.target.value)}>
|
||||
{authors.map((author) => (
|
||||
<option value={author.name} key={author.id}>
|
||||
{author.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<label>birthyear</label>
|
||||
<input value={year} onChange={(e) => setYear(e.target.value)} />
|
||||
<br />
|
||||
<button>set</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Authors;
|
||||
@@ -0,0 +1,51 @@
|
||||
import { useState } from "react";
|
||||
import { useQuery } from "@apollo/client";
|
||||
import { BOOKS_BY_GENRE } from "../queries";
|
||||
|
||||
const Books = ({ books }) => {
|
||||
const genres = ["all", "refactoring", "agile", "design", "crime", "classic"];
|
||||
const [filter, setFilter] = useState('');
|
||||
const booksRes = useQuery(BOOKS_BY_GENRE, {
|
||||
variables: { genre: filter },
|
||||
});
|
||||
if (booksRes.loading) return <>loading ....</>;
|
||||
return (
|
||||
<div>
|
||||
<h2>books</h2>
|
||||
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>author</th>
|
||||
<th>published</th>
|
||||
</tr>
|
||||
{booksRes.data.books.map((a, index) => (
|
||||
<tr key={index}>
|
||||
<td>{a.title}</td>
|
||||
<td>{a.author.name}</td>
|
||||
<td>{a.published}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<select value={filter} onChange={(e) => setFilter(e.target.value)}>
|
||||
{genres.map((genre, index) => {
|
||||
if (genre === "all")
|
||||
return (
|
||||
<option value={''} key={index}>
|
||||
{genre}
|
||||
</option>
|
||||
);
|
||||
return (
|
||||
<option value={genre} key={index}>
|
||||
{genre}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Books;
|
||||
@@ -0,0 +1,32 @@
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { useEffect, useState } from "react";
|
||||
import { LOGIN } from "../queries";
|
||||
|
||||
const Login = ({ token, setToken }) => {
|
||||
const [name, setName] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [login, result] = useMutation(LOGIN);
|
||||
const loginUser = async (e) => {
|
||||
e.preventDefault();
|
||||
await login({ variables: { username: name, password } });
|
||||
};
|
||||
useEffect(() => {
|
||||
if (result.data) {
|
||||
setToken(result.data.login.value);
|
||||
localStorage.setItem("library-user-token", result.data.login.value);
|
||||
}
|
||||
}, [result.data]);
|
||||
return (
|
||||
<form onSubmit={loginUser}>
|
||||
name <input onChange={(e) => setName(e.target.value)} value={name} />
|
||||
password{" "}
|
||||
<input
|
||||
type="password"
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
value={password}
|
||||
/>
|
||||
<button>login</button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
export default Login;
|
||||
@@ -0,0 +1,77 @@
|
||||
import { useState } from "react";
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { ALL_AUTHORS, ALL_BOOKS, ADD_BOOK } from "../queries";
|
||||
|
||||
const NewBook = () => {
|
||||
const [title, setTitle] = useState("");
|
||||
const [author, setAuthor] = useState("");
|
||||
const [published, setPublished] = useState("");
|
||||
const [genre, setGenre] = useState("");
|
||||
const [genres, setGenres] = useState([]);
|
||||
|
||||
const [addBook] = useMutation(ADD_BOOK, {
|
||||
refetchQueries: [{ query: ALL_BOOKS }, { query: ALL_AUTHORS }],
|
||||
});
|
||||
|
||||
const submit = async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
console.log("add book...");
|
||||
addBook({
|
||||
variables: { title, author, published: parseInt(published), genres },
|
||||
});
|
||||
|
||||
setTitle("");
|
||||
setPublished("");
|
||||
setAuthor("");
|
||||
setGenres([]);
|
||||
setGenre("");
|
||||
};
|
||||
|
||||
const addGenre = () => {
|
||||
setGenres(genres.concat(genre));
|
||||
setGenre("");
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<form onSubmit={submit}>
|
||||
<div>
|
||||
title
|
||||
<input
|
||||
value={title}
|
||||
onChange={({ target }) => setTitle(target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
author
|
||||
<input
|
||||
value={author}
|
||||
onChange={({ target }) => setAuthor(target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
published
|
||||
<input
|
||||
type="number"
|
||||
value={published}
|
||||
onChange={({ target }) => setPublished(target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
value={genre}
|
||||
onChange={({ target }) => setGenre(target.value)}
|
||||
/>
|
||||
<button onClick={addGenre} type="button">
|
||||
add genre
|
||||
</button>
|
||||
</div>
|
||||
<div>genres: {genres.join(" ")}</div>
|
||||
<button type="submit">create book</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NewBook;
|
||||
@@ -0,0 +1,32 @@
|
||||
import { useQuery } from "@apollo/client";
|
||||
import { BOOKS_BY_GENRE } from "../queries";
|
||||
|
||||
const Recommended = ({ user }) => {
|
||||
const booksRes = useQuery(BOOKS_BY_GENRE, {
|
||||
variables: { genre: user.me.favoriteGenre },
|
||||
});
|
||||
if (booksRes.loading) return <>loading ....</>
|
||||
return (
|
||||
<>
|
||||
<h2>recommendations</h2>
|
||||
<div>books in your favotite genre</div>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>author</th>
|
||||
<th>published</th>
|
||||
</tr>
|
||||
{booksRes.data.books.map((a, index) => (
|
||||
<tr key={index}>
|
||||
<td>{a.title}</td>
|
||||
<td>{a.author.name}</td>
|
||||
<td>{a.published}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default Recommended;
|
||||
@@ -0,0 +1,52 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "./App";
|
||||
import {
|
||||
ApolloClient,
|
||||
InMemoryCache,
|
||||
ApolloProvider,
|
||||
createHttpLink,
|
||||
split,
|
||||
} from "@apollo/client";
|
||||
import { setContext } from "@apollo/client/link/context";
|
||||
|
||||
import { getMainDefinition } from "@apollo/client/utilities";
|
||||
import { GraphQLWsLink } from "@apollo/client/link/subscriptions";
|
||||
import { createClient } from "graphql-ws";
|
||||
|
||||
const authLink = setContext((_, { headers }) => {
|
||||
const token = localStorage.getItem("library-user-token");
|
||||
return {
|
||||
headers: {
|
||||
...headers,
|
||||
authorization: token ? `Bearer ${token}` : null,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const httpLink = createHttpLink({
|
||||
uri: "http://localhost:4000",
|
||||
});
|
||||
const wsLink = new GraphQLWsLink(createClient({ url: "ws://localhost:4000" }));
|
||||
|
||||
const splitLink = split(
|
||||
({ query }) => {
|
||||
const definition = getMainDefinition(query);
|
||||
return (
|
||||
definition.kind === "OperationDefinition" &&
|
||||
definition.operation === "subscription"
|
||||
);
|
||||
},
|
||||
wsLink,
|
||||
authLink.concat(httpLink)
|
||||
);
|
||||
const client = new ApolloClient({
|
||||
cache: new InMemoryCache(),
|
||||
link: splitLink,
|
||||
});
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")).render(
|
||||
<ApolloProvider client={client}>
|
||||
<App />
|
||||
</ApolloProvider>
|
||||
);
|
||||
@@ -0,0 +1,117 @@
|
||||
import { gql } from "@apollo/client";
|
||||
|
||||
export const ALL_BOOKS = gql`
|
||||
query {
|
||||
books {
|
||||
title
|
||||
published
|
||||
id
|
||||
genres
|
||||
author {
|
||||
bookCount
|
||||
born
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const ALL_AUTHORS = gql`
|
||||
query {
|
||||
authors {
|
||||
name
|
||||
id
|
||||
born
|
||||
bookCount
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const ADD_BOOK = gql`
|
||||
mutation Mutation(
|
||||
$title: String!
|
||||
$author: String!
|
||||
$published: Int!
|
||||
$genres: [String!]!
|
||||
) {
|
||||
addBook(
|
||||
title: $title
|
||||
author: $author
|
||||
published: $published
|
||||
genres: $genres
|
||||
) {
|
||||
author {
|
||||
bookCount
|
||||
born
|
||||
id
|
||||
name
|
||||
}
|
||||
genres
|
||||
id
|
||||
published
|
||||
title
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const ADD_BIRTHYEAR = gql`
|
||||
mutation Mutation($birthyear: Int!, $name: String!) {
|
||||
editAuthor(birthyear: $birthyear, name: $name) {
|
||||
bookCount
|
||||
id
|
||||
born
|
||||
name
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const LOGIN = gql`
|
||||
mutation Mutation($username: String!, $password: String!) {
|
||||
login(username: $username, password: $password) {
|
||||
value
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const ME = gql`
|
||||
query ExampleQuery {
|
||||
me {
|
||||
favoriteGenre
|
||||
id
|
||||
username
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const BOOKS_BY_GENRE = gql`
|
||||
query Query($genre: String) {
|
||||
books(genre: $genre) {
|
||||
author {
|
||||
name
|
||||
}
|
||||
genres
|
||||
id
|
||||
published
|
||||
title
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const BOOK_DETAILS = gql`
|
||||
fragment BookDetails on Book {
|
||||
title
|
||||
published
|
||||
id
|
||||
genres
|
||||
author {
|
||||
name
|
||||
id
|
||||
born
|
||||
bookCount
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const BOOK_ADDED = gql`
|
||||
subscription {
|
||||
bookAdded {
|
||||
title
|
||||
}
|
||||
}
|
||||
${BOOK_DETAILS}
|
||||
`;
|
||||
Reference in New Issue
Block a user