Move to the example from github because lazy
36
todo-app-react/.gitignore
vendored
Normal file
@ -0,0 +1,36 @@
|
||||
.DS_STORE
|
||||
node_modules
|
||||
scripts/flow/*/.flowconfig
|
||||
.flowconfig
|
||||
*~
|
||||
*.pyc
|
||||
.grunt
|
||||
_SpecRunner.html
|
||||
__benchmarks__
|
||||
build/
|
||||
remote-repo/
|
||||
coverage/
|
||||
.module-cache
|
||||
fixtures/dom/public/react-dom.js
|
||||
fixtures/dom/public/react.js
|
||||
test/the-files-to-test.generated.js
|
||||
*.log*
|
||||
chrome-user-data
|
||||
*.sublime-project
|
||||
*.sublime-workspace
|
||||
.idea
|
||||
*.iml
|
||||
.vscode
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
packages/react-devtools-core/dist
|
||||
packages/react-devtools-extensions/chrome/build
|
||||
packages/react-devtools-extensions/chrome/*.crx
|
||||
packages/react-devtools-extensions/chrome/*.pem
|
||||
packages/react-devtools-extensions/firefox/build
|
||||
packages/react-devtools-extensions/firefox/*.xpi
|
||||
packages/react-devtools-extensions/firefox/*.pem
|
||||
packages/react-devtools-extensions/shared/build
|
||||
packages/react-devtools-inline/dist
|
||||
packages/react-devtools-shell/dist
|
47
todo-app-react/README.md
Normal file
@ -0,0 +1,47 @@
|
||||
<div style="padding-top: 5px; padding-bottom: 10px;">
|
||||
<h1 align="center">ToDo React App</h1>
|
||||
<h2 align="center">
|
||||
A One-Click Deployable App with<br />
|
||||
<a href="https://dgraph.io/slash-graphql" target="_blank">
|
||||
<img src="https://dgraph.io/assets/images/slashgraphql-logo.svg" alt="Slash GraphQL" />
|
||||
</a><br />
|
||||
A fully-managed GraphQL backend service
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<h3 align="center"><a href="https://slash.dgraph.io/_/one-click?app=todo" target="_blank">Deploy Now</a> for free!</h3>
|
||||
|
||||
To-Do is a sample app that lets users manage the tasks on their personal to-do list. This app demonstrates how to use React hooks with an Apollo client to easily create, read, update, and delete to-do list items. Deploying this app on Slash GraphQL deploys both the back-end database service and a front-end React app in a single click, no credit card required. To learn more about this sample app, see: [Building a To-Do List React App with Dgraph](https://dgraph.io/blog/post/building-todo-list-react-dgraph/)
|
||||
|
||||
### Features
|
||||
- Add a new task
|
||||
- Update an existing task to mark tasks completed
|
||||
- Delete existing tasks
|
||||
|
||||
### Front-end
|
||||
- [React](https://reactjs.org/) (3.4.0)—a JavaScript library for building user interfaces.
|
||||
- [Mobx](https://mobx.js.org/README.html)— MobX is a battle tested library that makes state management simple and scalable by transparently applying functional reactive programming (TFRP).
|
||||
- [Apollo Client](https://www.npmjs.com/package/apollo-client) (2.6.8)—a comprehensive state management library for JavaScript that enables you to manage both local and remote data with GraphQL.
|
||||
- [ToDoMVC app CSS](https://github.com/tastejs/todomvc-app-css)—CSS for a ToDo App
|
||||
- [React Router](https://reactrouter.com/)—a collection of navigational components
|
||||
- [clipboard.js](https://clipboardjs.com/)—a modern approach to copy text to clipboard
|
||||
- [history](https://github.com/ReactTraining/history)—lets you easily manage session history
|
||||
|
||||
### Back-end
|
||||
- [Slash GraphQL](https://dgraph.io/slash-graphql)—a fully managed GraphQL backend service
|
||||
- [Auth0](https://auth0.com/)—Secure access for everyone.
|
||||
|
||||
|
||||
### Links
|
||||
- [Deploy Now](https://slash.dgraph.io/_/one-click?app=todo)
|
||||
- [Blog: Build a React app with Slash GraphQL](https://dgraph.io/blog/post/todo-slash-graphql/)
|
||||
- [Demo](https://slash-graphql-todos.netlify.app/)
|
||||
- [Community Support](https://discuss.dgraph.io/)
|
||||
|
||||
### Screenshots
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
---
|
22
todo-app-react/one-click/mutations.graphql
Normal file
@ -0,0 +1,22 @@
|
||||
mutation AddTasks {
|
||||
addTask(input: [
|
||||
{title: "Create a database", completed: false, user: {username: "your-email@example.com"}},
|
||||
{title: "Write A Schema", completed: false, user: {username: "your-email@example.com"}},
|
||||
{title: "Put Data In", completed: false, user: {username: "your-email@example.com"}},
|
||||
{title: "Complete Tasks with UI", completed: false, user: {username: "your-email@example.com"}},
|
||||
{title: "Profit!", completed: false, user: {username: "your-email@example.com"}},
|
||||
{title: "Walking", completed: false, user: {username: "frodo@dgraph.io"}},
|
||||
{title: "More Walking", completed: false, user: {username: "frodo@dgraph.io"}},
|
||||
{title: "Discard Jewelery", completed: false, user: {username: "frodo@dgraph.io"}},
|
||||
{title: "Meet Dad", completed: false, user: {username: "skywalker@dgraph.io"}},
|
||||
{title: "Dismantle Empire", completed: false, user: {username: "skywalker@dgraph.io"}}
|
||||
]) {
|
||||
numUids
|
||||
task {
|
||||
title
|
||||
user {
|
||||
username
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
32
todo-app-react/one-click/schema.graphql
Normal file
@ -0,0 +1,32 @@
|
||||
type Task {
|
||||
id: ID!
|
||||
title: String! @search(by: [fulltext])
|
||||
completed: Boolean! @search
|
||||
user: User!
|
||||
}
|
||||
type User {
|
||||
username: String! @id @search(by: [hash])
|
||||
name: String @search(by: [exact])
|
||||
tasks: [Task] @hasInverse(field: user)
|
||||
}
|
||||
------SCHEMA AUTH------
|
||||
type Task @auth(
|
||||
query: { rule: """
|
||||
query($USER: String!) {
|
||||
queryTask {
|
||||
user(filter: { username: { eq: $USER } }) {
|
||||
__typename
|
||||
}
|
||||
}
|
||||
}"""}), {
|
||||
id: ID!
|
||||
title: String! @search(by: [fulltext])
|
||||
completed: Boolean! @search
|
||||
user: User!
|
||||
}
|
||||
type User {
|
||||
username: String! @id @search(by: [hash])
|
||||
name: String @search(by: [exact])
|
||||
tasks: [Task] @hasInverse(field: user)
|
||||
}
|
||||
# Dgraph.Authorization {"Header":"X-Auth-Token","Namespace":"https://dgraph.io/jwt/claims","Algo":"RS256","Audience":["Q1nC2kLsN6KQTX1UPdiBS6AhXRx9KwKl"],"VerificationKey":"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp/qw/KXH23bpOuhXzsDp\ndo9bGNqjd/OkH2LkCT0PKFx5i/lmvFXdd04fhJD0Z0K3pUe7xHcRn1pIbZWlhwOR\n7siaCh9L729OQjnrxU/aPOKwsD19YmLWwTeVpE7vhDejhnRaJ7Pz8GImX/z/Xo50\nPFSYdX28Fb3kssfo+cMBz2+7h1prKeLZyDk30ItK9MMj9S5y+UKHDwfLV/ZHSd8m\nVVEYRXUNNzLsxD2XaEC5ym2gCjEP1QTgago0iw3Bm2rNAMBePgo4OMgYjH9wOOuS\nVnyvHhZdwiZAd1XtJSehORzpErgDuV2ym3mw1G9mrDXDzX9vr5l5CuBc3BjnvcFC\nFwIDAQAB\n-----END PUBLIC KEY-----"}
|
51
todo-app-react/package.json
Normal file
@ -0,0 +1,51 @@
|
||||
{
|
||||
"name": "dgraph-react-todoapp",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@auth0/auth0-spa-js": "^1.8.1",
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
"@testing-library/react": "^9.3.2",
|
||||
"@testing-library/user-event": "^7.1.2",
|
||||
"apollo-cache-inmemory": "^1.6.5",
|
||||
"apollo-cache-persist": "^0.1.1",
|
||||
"apollo-client": "^2.6.8",
|
||||
"apollo-link-context": "^1.0.20",
|
||||
"apollo-link-http": "^1.5.16",
|
||||
"classnames": "^2.2.6",
|
||||
"dgraph-js-http": "^20.07.0",
|
||||
"graphql": "^14.6.0",
|
||||
"graphql-tag": "^2.10.3",
|
||||
"history": "^4.10.1",
|
||||
"mobx": "^5.15.4",
|
||||
"mobx-react-lite": "^1.5.2",
|
||||
"react": "^16.12.0",
|
||||
"react-apollo": "^3.1.3",
|
||||
"react-clipboard.js": "^2.0.16",
|
||||
"react-dom": "^16.12.0",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-scripts": "3.4.0",
|
||||
"todomvc-app-css": "^2.3.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "react-app"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
BIN
todo-app-react/public/favicon.ico
Normal file
After Width: | Height: | Size: 2.6 KiB |
24
todo-app-react/public/index.html
Normal file
@ -0,0 +1,24 @@
|
||||
<!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="React + GraphQL + Auth0 - Todo App powered by Dgraph (Slash GraphQL)" />
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<title>React + GraphQL + Dgraph - ToDo App</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<section id="root"></section>
|
||||
<footer class="info">
|
||||
<p>Double-click to edit a todo</p>
|
||||
<p><a href="javascript:changeSlashGraphQLEndpoint()">Switch Slash GraphQL Endpoints</a></p>
|
||||
<p>Created by <a href="https://dgraph.io/graphql" target="_blank">Dgraph Labs</a></p>
|
||||
</footer>
|
||||
</body>
|
||||
|
||||
</html>
|
15
todo-app-react/public/manifest.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"short_name": "React Todo App",
|
||||
"name": "React Todo GraphQL App powered by Dgraph",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
27
todo-app-react/schema.graphql
Normal file
@ -0,0 +1,27 @@
|
||||
type Task
|
||||
@auth(
|
||||
query: {
|
||||
rule: """
|
||||
query($USER: String!) {
|
||||
queryTask {
|
||||
user(filter: { username: { eq: $USER } }) {
|
||||
__typename
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
}
|
||||
) {
|
||||
id: ID!
|
||||
title: String! @search(by: [fulltext])
|
||||
completed: Boolean! @search
|
||||
user: User!
|
||||
}
|
||||
|
||||
type User {
|
||||
username: String! @id @search(by: [hash])
|
||||
name: String @search(by: [exact])
|
||||
tasks: [Task] @hasInverse(field: user)
|
||||
}
|
||||
|
||||
# Dgraph.Authorization {"VerificationKey":"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArAtAGwFiRDKc5cGrMEGU\nmIRVunv1X0aLYTshT/BDNYEERi8oveB42+l5cBszXZLVtFab8Wz13LbAQ6lJyf9H\nvtHkizTe4Ta4JzFbGu6foNrzXH+5UbkCZdTt7Ww2irbtcbebGSnMksCx9/b/UNY5\nvzWnJuSokKMh2VFaz7oaZhKhJD55Tm1rqAiqvEnmI6G754s6KIPXFmagBCLdraSx\nOTDyWpv/AH9QI9V7ZW4xGW5noK8Xb97QjEk9s3Q6nizh6PMyHpDvLo3LLnJtQub7\nckyHCE12O7+sO0xOt0Fqrf2QVAQTtZUgsBi3rqFClRYtOGKnKJUkwX5dewQqIsQo\njQIDAQAB\n-----END PUBLIC KEY-----","Header":"X-Auth-Token","Namespace":"https://dgraph.io/jwt/claims","Algo":"RS256","Audience":["rdZUt2JRoPq6XutiYmt83SDOwiMTQzIf"]}
|
20
todo-app-react/src/App.css
Normal file
@ -0,0 +1,20 @@
|
||||
.navheader {
|
||||
margin: auto 0px;
|
||||
padding: 15px;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.token {
|
||||
line-height: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.token input {
|
||||
width: 80%;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.token img {
|
||||
cursor: pointer;
|
||||
height: 16px;
|
||||
}
|
71
todo-app-react/src/App.js
Normal file
@ -0,0 +1,71 @@
|
||||
import React from 'react'
|
||||
import { Router, Switch } from "react-router-dom";
|
||||
import ApolloClient from "apollo-client";
|
||||
import { InMemoryCache } from "apollo-cache-inmemory";
|
||||
import { ApolloProvider } from "@apollo/react-hooks";
|
||||
import { createHttpLink } from "apollo-link-http";
|
||||
import { useAuth0 } from "./react-auth0-spa";
|
||||
import { setContext } from "apollo-link-context";
|
||||
import { getSlashGraphQLEndpoint } from './slash_endpoint'
|
||||
import AuthToken from "./AuthToken";
|
||||
import TodoApp from './TodoApp';
|
||||
import NavBar from "./NavBar";
|
||||
import Profile from "./Profile";
|
||||
import history from "./history";
|
||||
import PrivateRoute from "./PrivateRoute";
|
||||
import './App.css';
|
||||
|
||||
const createApolloClient = token => {
|
||||
const httpLink = createHttpLink({
|
||||
uri: getSlashGraphQLEndpoint(),
|
||||
options: {
|
||||
reconnect: true,
|
||||
},
|
||||
});
|
||||
|
||||
const authLink = setContext((request, { headers }) => {
|
||||
// return the header to the context so httpLink can read them
|
||||
return {
|
||||
headers: {
|
||||
...headers,
|
||||
"X-Auth-Token": token,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return new ApolloClient({
|
||||
link: authLink.concat(httpLink),
|
||||
cache: new InMemoryCache()
|
||||
});
|
||||
}
|
||||
|
||||
const App = ({ idToken }) => {
|
||||
const { loading } = useAuth0();
|
||||
|
||||
if (loading) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
const client = createApolloClient(idToken);
|
||||
|
||||
return (
|
||||
<ApolloProvider client={client}>
|
||||
<div class="todoapp">
|
||||
<Router history={history}>
|
||||
<h1>todos</h1>
|
||||
<header className="navheader">
|
||||
<NavBar />
|
||||
</header>
|
||||
<Switch>
|
||||
<PrivateRoute path="/" component={TodoApp} exact />
|
||||
<PrivateRoute path="/profile" component={Profile} />
|
||||
</Switch>
|
||||
</Router>
|
||||
</div>
|
||||
|
||||
<AuthToken token={idToken} />
|
||||
</ApolloProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
18
todo-app-react/src/AuthToken.jsx
Normal file
@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import './App.css';
|
||||
import Clipboard from 'react-clipboard.js';
|
||||
|
||||
// FIXME: We should refactor this UI to have the copy button within the input box
|
||||
const AuthToken = ({ token }) => {
|
||||
return <div className="token">
|
||||
<span>X-Auth-Token: {" "}</span>
|
||||
|
||||
<input id="token" value={token || ""} readOnly />
|
||||
|
||||
<Clipboard data-clipboard-text={token}>
|
||||
<img src="https://clipboardjs.com/assets/images/clippy.svg" alt="Copy To Clipboard"/>
|
||||
</Clipboard>
|
||||
</div>;
|
||||
}
|
||||
|
||||
export default AuthToken;
|
111
todo-app-react/src/GraphQLData.js
Normal file
@ -0,0 +1,111 @@
|
||||
import gql from "graphql-tag";
|
||||
|
||||
export const GET_USER = gql`
|
||||
query getUser($username: String!){
|
||||
getUser(username: $username) {
|
||||
username
|
||||
name
|
||||
tasks {
|
||||
id
|
||||
title
|
||||
completed
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const ADD_USER = gql`
|
||||
mutation addUser($user: AddUserInput!) {
|
||||
addUser(input: [$user]) {
|
||||
user {
|
||||
username
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const GET_TODOS = gql`
|
||||
query {
|
||||
queryTask {
|
||||
id
|
||||
title
|
||||
completed
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const ADD_TODO = gql`
|
||||
mutation addTask($task: [AddTaskInput!]!) {
|
||||
addTask(input: $task) {
|
||||
task {
|
||||
id
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const TOGGLE_TODO = gql`
|
||||
mutation updateTask($taskID: ID!, $completed: Boolean!) {
|
||||
updateTask(input: {
|
||||
filter: { id: [$taskID] },
|
||||
set: {
|
||||
completed: $completed
|
||||
}
|
||||
}) {
|
||||
task {
|
||||
id
|
||||
title
|
||||
completed
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const TOGGLE_ALL_TODO = gql`
|
||||
mutation updateTask($completed: Boolean!) {
|
||||
updateTask(input: {
|
||||
filter: {},
|
||||
set: {
|
||||
completed: $completed
|
||||
}
|
||||
}) {
|
||||
task {
|
||||
id
|
||||
title
|
||||
completed
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const DELETE_TODO = gql`
|
||||
mutation deleteTask($taskID: [ID!]) {
|
||||
deleteTask(filter: { id: $taskID }) {
|
||||
msg
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const UPDATE_TODO = gql`
|
||||
mutation updateTask($taskID: ID!, $task: TaskPatch!) {
|
||||
updateTask(input: {
|
||||
filter: { id: [$taskID] },
|
||||
set: $task
|
||||
}) {
|
||||
task {
|
||||
id
|
||||
title
|
||||
completed
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const CLEAR_COMPLETED_TODO = gql`
|
||||
mutation updateTask($completed: Boolean) {
|
||||
deleteTask(filter: { completed: $completed }) {
|
||||
msg
|
||||
}
|
||||
}
|
||||
`;
|
28
todo-app-react/src/NavBar.css
Normal file
@ -0,0 +1,28 @@
|
||||
.navbar ul {
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 47px;
|
||||
}
|
||||
.navbar li {
|
||||
display: inline;
|
||||
float: left;
|
||||
}
|
||||
.navbar li:first-child {
|
||||
margin-right: 10px;
|
||||
}
|
||||
.navbar li a {
|
||||
display: block;
|
||||
color: white;
|
||||
background-color: #b83f45;
|
||||
text-align: center;
|
||||
padding: 14px 16px;
|
||||
font-size: large;
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
}
|
||||
/* Change the link color to #111 (black) on hover */
|
||||
.navbar li a:hover {
|
||||
background-color: silver;
|
||||
color: white;
|
||||
}
|
41
todo-app-react/src/NavBar.js
Normal file
@ -0,0 +1,41 @@
|
||||
import React from "react";
|
||||
import { useAuth0 } from "./react-auth0-spa";
|
||||
import { Link } from "react-router-dom";
|
||||
import { changeSlashGraphQLEndpoint } from './slash_endpoint'
|
||||
|
||||
import './NavBar.css';
|
||||
|
||||
const NavBar = () => {
|
||||
const { loading, isAuthenticated, logout } = useAuth0();
|
||||
|
||||
if (loading) {
|
||||
return <div>Loading...</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="navbar">
|
||||
<ul>
|
||||
{isAuthenticated && (
|
||||
<span>
|
||||
<li>
|
||||
<Link to="/">Home</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/profile" style={{ marginRight: 10 }}>Profile</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link onClick={changeSlashGraphQLEndpoint}>Endpoint</Link>
|
||||
</li>
|
||||
<li style={{ float: "right"}}>
|
||||
<Link onClick={() => logout({returnTo: global.window.location.href})}>
|
||||
Log out
|
||||
</Link>
|
||||
</li>
|
||||
</span>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NavBar;
|
26
todo-app-react/src/PrivateRoute.js
Normal file
@ -0,0 +1,26 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { Route } from "react-router-dom";
|
||||
import { useAuth0 } from "./react-auth0-spa";
|
||||
|
||||
const PrivateRoute = ({ component: Component, path, ...rest }) => {
|
||||
const { loading, isAuthenticated, loginWithRedirect } = useAuth0();
|
||||
|
||||
useEffect(() => {
|
||||
if (loading || isAuthenticated) {
|
||||
return;
|
||||
}
|
||||
const fn = async () => {
|
||||
await loginWithRedirect({
|
||||
appState: {targetUrl: window.location.pathname}
|
||||
});
|
||||
};
|
||||
fn();
|
||||
}, [loading, isAuthenticated, loginWithRedirect, path]);
|
||||
|
||||
const render = props =>
|
||||
isAuthenticated === true ? <Component {...props} /> : null;
|
||||
|
||||
return <Route path={path} render={render} {...rest} />;
|
||||
};
|
||||
|
||||
export default PrivateRoute;
|
8
todo-app-react/src/Profile.css
Normal file
@ -0,0 +1,8 @@
|
||||
.profile {
|
||||
padding: 15px;
|
||||
}
|
||||
.profile-img {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
border-radius: 50%;
|
||||
}
|
21
todo-app-react/src/Profile.js
Normal file
@ -0,0 +1,21 @@
|
||||
import React from "react";
|
||||
import { useAuth0 } from "./react-auth0-spa";
|
||||
import './Profile.css';
|
||||
|
||||
const Profile = () => {
|
||||
const { loading, user } = useAuth0();
|
||||
|
||||
if (loading || !user) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="profile">
|
||||
<img className="profile-img" src={user.picture} alt="Profile" />
|
||||
<p>Name: <strong>{user.nickname}</strong></p>
|
||||
<p>Email: <strong>{user.email}</strong></p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Profile;
|
10
todo-app-react/src/QueryHistory.jsx
Normal file
@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
import { observer } from "mobx-react-lite";
|
||||
import QueryStore from './QueryStore';
|
||||
import './App.css';
|
||||
|
||||
const QueryHistory = () => {
|
||||
return <div className="query">{QueryStore.query}</div>
|
||||
}
|
||||
|
||||
export default observer(QueryHistory);
|
16
todo-app-react/src/QueryStore.jsx
Normal file
@ -0,0 +1,16 @@
|
||||
import { decorate, observable, action } from "mobx";
|
||||
|
||||
class QueryStore {
|
||||
query = ""
|
||||
|
||||
setQuery(query) {
|
||||
this.query = query;
|
||||
}
|
||||
}
|
||||
|
||||
decorate(QueryStore, {
|
||||
query: observable,
|
||||
setQuery: action,
|
||||
});
|
||||
|
||||
export default new QueryStore();
|
262
todo-app-react/src/TodoApp.js
Normal file
@ -0,0 +1,262 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { useQuery, useMutation } from "@apollo/react-hooks";
|
||||
|
||||
import defs from './defs'
|
||||
import history from './history';
|
||||
import TodoFooter from './TodoFooter'
|
||||
import TodoItem from './TodoItem'
|
||||
import { GET_USER, GET_TODOS, ADD_USER, ADD_TODO, DELETE_TODO, TOGGLE_TODO, UPDATE_TODO, CLEAR_COMPLETED_TODO, TOGGLE_ALL_TODO } from "./GraphQLData";
|
||||
import { useAuth0 } from "./react-auth0-spa";
|
||||
|
||||
const ENTER_KEY = 13
|
||||
|
||||
const useImperativeQuery = (query) => {
|
||||
const { refetch } = useQuery(query, { skip: true });
|
||||
const imperativelyCallQuery = (variables) => {
|
||||
return refetch(variables);
|
||||
};
|
||||
return imperativelyCallQuery;
|
||||
};
|
||||
const TodoApp = () => {
|
||||
const [nowShowing, setNowShowing] = useState(defs.ALL_TODOS);
|
||||
const [getEditing, setEditing] = useState(null);
|
||||
const [newTodo, setNewTodo] = useState("");
|
||||
const [shownTodos, setShownTodos] = useState([]);
|
||||
|
||||
const [addUser] = useMutation(ADD_USER);
|
||||
const [addTodo] = useMutation(ADD_TODO);
|
||||
const [toggleTodo] = useMutation(TOGGLE_TODO);
|
||||
const [toggleAllTodo] = useMutation(TOGGLE_ALL_TODO);
|
||||
const [deleteTodo] = useMutation(DELETE_TODO);
|
||||
const [updateTodo] = useMutation(UPDATE_TODO);
|
||||
const [clearCompletedTodo] = useMutation(CLEAR_COMPLETED_TODO);
|
||||
const getUsers = useImperativeQuery(GET_USER)
|
||||
|
||||
const { user } = useAuth0();
|
||||
|
||||
const createUser = () => {
|
||||
if (user === undefined) {
|
||||
return null;
|
||||
}
|
||||
const { data: getUser } = getUsers({
|
||||
username: user.email
|
||||
});
|
||||
if (getUser && getUser.getUser === null) {
|
||||
const newUser = {
|
||||
username: user.email,
|
||||
name: user.nickname,
|
||||
};
|
||||
addUser({
|
||||
variables: {
|
||||
user: newUser
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const { loading, error, data } = useQuery(GET_TODOS);
|
||||
const getData = () => {
|
||||
if (loading) {
|
||||
return null;
|
||||
}
|
||||
if (error) {
|
||||
console.error(`GET_TODOS error: ${error}`);
|
||||
return `Error: ${error.message}`;
|
||||
}
|
||||
if (data.queryTask) {
|
||||
setShownTodos(data.queryTask)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const setNowShowingFn = nowShowing => () => setNowShowing(nowShowing)
|
||||
const routes = {
|
||||
'/': setNowShowingFn(defs.ALL_TODOS),
|
||||
'/active': setNowShowingFn(defs.ACTIVE_TODOS),
|
||||
'/completed': setNowShowingFn(defs.COMPLETED_TODOS),
|
||||
}
|
||||
const processLocationHash = hash => {
|
||||
if (hash) {
|
||||
hash = hash.substring(1)
|
||||
}
|
||||
const route = routes[hash] || routes['/']
|
||||
route()
|
||||
}
|
||||
processLocationHash(history.location.hash)
|
||||
history.listen((location, action) =>
|
||||
processLocationHash(location.hash)
|
||||
)
|
||||
createUser()
|
||||
getData()
|
||||
}, [user, data]) // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const handleChange = event => {
|
||||
setNewTodo(event.target.value)
|
||||
}
|
||||
|
||||
const handleNewTodoKeyDown = event => {
|
||||
if (event.keyCode !== ENTER_KEY) {
|
||||
return
|
||||
}
|
||||
event.preventDefault()
|
||||
const val = newTodo
|
||||
if (val) {
|
||||
add(val)
|
||||
setNewTodo('')
|
||||
}
|
||||
}
|
||||
|
||||
const add = (title) =>
|
||||
addTodo({
|
||||
variables: { task: [
|
||||
{ title: title, completed: false, user: { username: user.email } }
|
||||
]},
|
||||
refetchQueries: [{
|
||||
query: GET_TODOS
|
||||
}]
|
||||
});
|
||||
|
||||
const destroy = todo =>
|
||||
deleteTodo({
|
||||
variables: {
|
||||
taskID: [todo.id]
|
||||
},
|
||||
refetchQueries: [{
|
||||
query: GET_TODOS
|
||||
}]
|
||||
})
|
||||
|
||||
const toggleAll = event => {
|
||||
const checked = event.target.checked
|
||||
toggleAllTodo({
|
||||
variables: {
|
||||
completed: checked
|
||||
},
|
||||
refetchQueries: [{
|
||||
query: GET_TODOS
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
const toggle = todoToToggle => {
|
||||
toggleTodo({
|
||||
variables: {
|
||||
taskID: todoToToggle.id,
|
||||
completed: !todoToToggle.completed
|
||||
},
|
||||
refetchQueries: [{
|
||||
query: GET_TODOS
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
const edit = todo => setEditing(todo.id)
|
||||
|
||||
const save = (todoToSave, text) => {
|
||||
updateTodo({
|
||||
variables: {
|
||||
taskID: todoToSave.id,
|
||||
task: {
|
||||
title: text
|
||||
}
|
||||
},
|
||||
refetchQueries: [{
|
||||
query: GET_TODOS
|
||||
}]
|
||||
})
|
||||
setEditing(null)
|
||||
}
|
||||
|
||||
const cancel = () =>
|
||||
setEditing(null)
|
||||
|
||||
const clearCompleted = () =>
|
||||
clearCompletedTodo({
|
||||
variables: {
|
||||
completed: true
|
||||
},
|
||||
refetchQueries: [{
|
||||
query: GET_TODOS
|
||||
}]
|
||||
})
|
||||
|
||||
const newTodos = shownTodos.filter(todo => {
|
||||
switch (nowShowing) {
|
||||
case defs.ACTIVE_TODOS:
|
||||
return !todo.completed
|
||||
case defs.COMPLETED_TODOS:
|
||||
return todo.completed
|
||||
default:
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
const todoItems = newTodos.map(todo => {
|
||||
return (
|
||||
<TodoItem
|
||||
key={todo.id}
|
||||
todo={todo}
|
||||
onToggle={() => toggle(todo)}
|
||||
onDestroy={() => destroy(todo)}
|
||||
onEdit={() => edit(todo)}
|
||||
editing={getEditing === todo.id}
|
||||
onSave={text => save(todo, text)}
|
||||
onCancel={cancel}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
const activeTodoCount = shownTodos.reduce(function (accum, todo) {
|
||||
return todo.completed ? accum : accum + 1
|
||||
}, 0)
|
||||
|
||||
const completedCount = shownTodos.length - activeTodoCount
|
||||
|
||||
const footer = (activeTodoCount || completedCount)
|
||||
? <TodoFooter
|
||||
count={activeTodoCount}
|
||||
completedCount={completedCount}
|
||||
nowShowing={nowShowing}
|
||||
onClearCompleted={clearCompleted}
|
||||
/>
|
||||
: null
|
||||
|
||||
const main = !shownTodos.length
|
||||
? null
|
||||
: (
|
||||
<section className="main">
|
||||
<input
|
||||
id="toggle-all"
|
||||
className="toggle-all"
|
||||
type="checkbox"
|
||||
onChange={toggleAll}
|
||||
checked={activeTodoCount === 0}
|
||||
/>
|
||||
<label
|
||||
htmlFor="toggle-all"
|
||||
/>
|
||||
<ul className="todo-list">
|
||||
{todoItems}
|
||||
</ul>
|
||||
</section>
|
||||
)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<header className="header">
|
||||
<input
|
||||
className="new-todo"
|
||||
placeholder="What needs to be done?"
|
||||
value={newTodo}
|
||||
onKeyDown={handleNewTodoKeyDown}
|
||||
onChange={handleChange}
|
||||
autoFocus={true}
|
||||
/>
|
||||
</header>
|
||||
{main}
|
||||
{footer}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TodoApp
|
53
todo-app-react/src/TodoFooter.js
Normal file
@ -0,0 +1,53 @@
|
||||
import React from 'react'
|
||||
import classNames from 'classnames'
|
||||
|
||||
import defs from './defs'
|
||||
import Utils from './Utils'
|
||||
|
||||
export default class TodoFooter extends React.Component {
|
||||
render() {
|
||||
const { completedCount, count, nowShowing, onClearCompleted } = this.props
|
||||
const clearButton = completedCount === 0
|
||||
? null
|
||||
: (
|
||||
<button
|
||||
className="clear-completed"
|
||||
onClick={onClearCompleted}>
|
||||
Clear completed
|
||||
</button>
|
||||
)
|
||||
return (
|
||||
<footer className="footer">
|
||||
<span className="todo-count">
|
||||
<strong>{count}</strong> {Utils.pluralize(count, 'item')} left
|
||||
</span>
|
||||
<ul className="filters">
|
||||
<li>
|
||||
<a
|
||||
href="#/"
|
||||
className={classNames({selected: nowShowing === defs.ALL_TODOS})}>
|
||||
All
|
||||
</a>
|
||||
</li>
|
||||
{' '}
|
||||
<li>
|
||||
<a
|
||||
href="#/active"
|
||||
className={classNames({selected: nowShowing === defs.ACTIVE_TODOS})}>
|
||||
Active
|
||||
</a>
|
||||
</li>
|
||||
{' '}
|
||||
<li>
|
||||
<a
|
||||
href="#/completed"
|
||||
className={classNames({selected: nowShowing === defs.COMPLETED_TODOS})}>
|
||||
Completed
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
{clearButton}
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
}
|
81
todo-app-react/src/TodoItem.js
Normal file
@ -0,0 +1,81 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import classNames from 'classnames'
|
||||
|
||||
const ESCAPE_KEY = 27
|
||||
const ENTER_KEY = 13
|
||||
|
||||
const TodoItem = (props) => {
|
||||
const [editText, setEditText] = useState(props.todo.title);
|
||||
const editField = React.useRef(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!editField.current && props.editing) {
|
||||
const node = editField.current
|
||||
node.focus()
|
||||
node.setSelectionRange(node.value.length, node.value.length)
|
||||
}
|
||||
})
|
||||
|
||||
const handleSubmit = event => {
|
||||
const {onDestroy, onSave } = props
|
||||
var val = editText.trim()
|
||||
if (val) {
|
||||
onSave(val)
|
||||
setEditText(val)
|
||||
} else {
|
||||
onDestroy()
|
||||
}
|
||||
}
|
||||
|
||||
const handleEdit = () => {
|
||||
const { onEdit, todo } = props
|
||||
onEdit()
|
||||
setEditText(todo.title)
|
||||
}
|
||||
|
||||
const handleKeyDown = event => {
|
||||
const { onCancel, todo } = props
|
||||
if (event.which === ESCAPE_KEY) {
|
||||
setEditText(todo.title)
|
||||
onCancel(event)
|
||||
} else if (event.which === ENTER_KEY) {
|
||||
handleSubmit(event)
|
||||
}
|
||||
}
|
||||
|
||||
const handleChange = event => {
|
||||
if (props.editing) {
|
||||
setEditText(event.target.value)
|
||||
}
|
||||
}
|
||||
|
||||
const { editing, onDestroy, onToggle, todo } = props
|
||||
return (
|
||||
<li className={classNames({
|
||||
completed: todo.completed,
|
||||
editing: editing,
|
||||
})}>
|
||||
<div className="view">
|
||||
<input
|
||||
className="toggle"
|
||||
type="checkbox"
|
||||
checked={todo.completed}
|
||||
onChange={onToggle}
|
||||
/>
|
||||
<label onDoubleClick={handleEdit}>
|
||||
{todo.title}
|
||||
</label>
|
||||
<button className="destroy" onClick={onDestroy} />
|
||||
</div>
|
||||
<input
|
||||
ref={editField}
|
||||
className="edit"
|
||||
value={editText}
|
||||
onChange={handleChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
export default TodoItem
|
5
todo-app-react/src/Utils.js
Normal file
@ -0,0 +1,5 @@
|
||||
export default {
|
||||
pluralize: (count, word) => {
|
||||
return count === 1 ? word : word + 's';
|
||||
}
|
||||
}
|
5
todo-app-react/src/config.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"domain": "ianonavy.us.auth0.com",
|
||||
"clientId": "rdZUt2JRoPq6XutiYmt83SDOwiMTQzIf",
|
||||
"graphqlUrl": "http://localhost:8080/graphql"
|
||||
}
|
5
todo-app-react/src/defs.js
Normal file
@ -0,0 +1,5 @@
|
||||
export default {
|
||||
ALL_TODOS: 'all',
|
||||
ACTIVE_TODOS: 'active',
|
||||
COMPLETED_TODOS: 'completed',
|
||||
}
|
3
todo-app-react/src/history.js
Normal file
@ -0,0 +1,3 @@
|
||||
import { createBrowserHistory } from "history";
|
||||
|
||||
export default createBrowserHistory();
|
26
todo-app-react/src/index.js
Normal file
@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import 'todomvc-app-css/index.css'
|
||||
|
||||
import { Auth0Provider } from "./react-auth0-spa";
|
||||
import config from "./config.json";
|
||||
import history from "./history";
|
||||
|
||||
/* A function that routes the user to the right place after login */
|
||||
const onRedirectCallback = appState => {
|
||||
history.push(
|
||||
appState && appState.targetUrl
|
||||
? appState.targetUrl
|
||||
: window.location.pathname
|
||||
);
|
||||
};
|
||||
|
||||
ReactDOM.render(
|
||||
<Auth0Provider
|
||||
domain={process.env.REACT_APP_AUTH0_DOMAIN || config.domain}
|
||||
client_id={process.env.REACT_APP_AUTH0_CLIENT_ID || config.clientId}
|
||||
redirect_uri={window.location.origin}
|
||||
onRedirectCallback={onRedirectCallback}
|
||||
/>,
|
||||
document.getElementById("root")
|
||||
);
|
95
todo-app-react/src/react-auth0-spa.js
vendored
Normal file
@ -0,0 +1,95 @@
|
||||
import React, { useState, useEffect, useContext } from "react";
|
||||
import createAuth0Client from "@auth0/auth0-spa-js";
|
||||
import App from "./App";
|
||||
|
||||
const DEFAULT_REDIRECT_CALLBACK = () =>
|
||||
window.history.replaceState({}, document.title, window.location.pathname);
|
||||
|
||||
export const Auth0Context = React.createContext();
|
||||
export const useAuth0 = () => useContext(Auth0Context);
|
||||
export const Auth0Provider = ({
|
||||
children,
|
||||
onRedirectCallback = DEFAULT_REDIRECT_CALLBACK,
|
||||
...initOptions
|
||||
}) => {
|
||||
const [isAuthenticated, setIsAuthenticated] = useState();
|
||||
const [user, setUser] = useState();
|
||||
const [auth0Client, setAuth0] = useState();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [popupOpen, setPopupOpen] = useState(false);
|
||||
const [idToken, setIdToken] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
const initAuth0 = async () => {
|
||||
const auth0FromHook = await createAuth0Client(initOptions);
|
||||
setAuth0(auth0FromHook);
|
||||
|
||||
if (window.location.search.includes("code=") &&
|
||||
window.location.search.includes("state=")) {
|
||||
const { appState } = await auth0FromHook.handleRedirectCallback();
|
||||
onRedirectCallback(appState);
|
||||
}
|
||||
|
||||
const isAuthenticated = await auth0FromHook.isAuthenticated();
|
||||
|
||||
setIsAuthenticated(isAuthenticated);
|
||||
|
||||
if (isAuthenticated) {
|
||||
const user = await auth0FromHook.getUser();
|
||||
setUser(user);
|
||||
const idTokenClaims = await auth0FromHook.getIdTokenClaims();
|
||||
setIdToken(idTokenClaims.__raw);
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
};
|
||||
initAuth0();
|
||||
// eslint-disable-next-line
|
||||
}, []);
|
||||
|
||||
const loginWithPopup = async (params = {}) => {
|
||||
setPopupOpen(true);
|
||||
try {
|
||||
await auth0Client.loginWithPopup(params);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
setPopupOpen(false);
|
||||
}
|
||||
const user = await auth0Client.getUser();
|
||||
setUser(user);
|
||||
setIsAuthenticated(true);
|
||||
};
|
||||
|
||||
const handleRedirectCallback = async () => {
|
||||
setLoading(true);
|
||||
await auth0Client.handleRedirectCallback();
|
||||
const user = await auth0Client.getUser();
|
||||
const idTokenClaims = await auth0Client.getIdTokenClaims();
|
||||
|
||||
setIdToken(idTokenClaims.__raw);
|
||||
setLoading(false);
|
||||
setIsAuthenticated(true);
|
||||
setUser(user);
|
||||
};
|
||||
return (
|
||||
<Auth0Context.Provider
|
||||
value={{
|
||||
isAuthenticated,
|
||||
user,
|
||||
loading,
|
||||
popupOpen,
|
||||
loginWithPopup,
|
||||
handleRedirectCallback,
|
||||
getIdTokenClaims: (...p) => auth0Client.getIdTokenClaims(...p),
|
||||
loginWithRedirect: (...p) => auth0Client.loginWithRedirect(...p),
|
||||
getTokenSilently: (...p) => auth0Client.getTokenSilently(...p),
|
||||
getTokenWithPopup: (...p) => auth0Client.getTokenWithPopup(...p),
|
||||
logout: (...p) => auth0Client.logout(...p)
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
<App idToken={idToken} />
|
||||
</Auth0Context.Provider>
|
||||
);
|
||||
};
|
@ -2,4 +2,4 @@
|
||||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom';
|
||||
import '@testing-library/jest-dom/extend-expect';
|
32
todo-app-react/src/slash_endpoint.js
Normal file
@ -0,0 +1,32 @@
|
||||
const STORAGE_KEY = "slash-endpoint"
|
||||
|
||||
function askForEndpoint() {
|
||||
const endpoint = prompt("Please enter your Slash GraphQL Endpoint")
|
||||
console.log(endpoint)
|
||||
if (endpoint && global.localStorage && endpoint.endsWith("/graphql")) {
|
||||
global.localStorage.setItem(STORAGE_KEY, endpoint)
|
||||
}
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
export function getSlashGraphQLEndpoint() {
|
||||
const localStorageEndpoint = global.localStorage && global.localStorage.getItem(STORAGE_KEY);
|
||||
|
||||
if (localStorageEndpoint) {
|
||||
return localStorageEndpoint
|
||||
}
|
||||
|
||||
const defaultEndpoint = process.env.REACT_APP_GRAPHQL_ENDPOINT;
|
||||
if(defaultEndpoint) {
|
||||
return defaultEndpoint;
|
||||
}
|
||||
|
||||
return askForEndpoint();
|
||||
}
|
||||
|
||||
export function changeSlashGraphQLEndpoint() {
|
||||
global.localStorage && global.localStorage.removeItem(STORAGE_KEY)
|
||||
askForEndpoint();
|
||||
window.location.reload()
|
||||
}
|
||||
global.changeSlashGraphQLEndpoint = changeSlashGraphQLEndpoint;
|
BIN
todo-app-react/todo-1.png
Normal file
After Width: | Height: | Size: 61 KiB |
BIN
todo-app-react/todo-2.png
Normal file
After Width: | Height: | Size: 51 KiB |
BIN
todo-app-react/todo-graph-2.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
todo-app-react/todo-graph.png
Normal file
After Width: | Height: | Size: 14 KiB |
23
todo-react-app/.gitignore
vendored
@ -1,23 +0,0 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
@ -1,70 +0,0 @@
|
||||
# Getting Started with Create React App
|
||||
|
||||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||
|
||||
## Available Scripts
|
||||
|
||||
In the project directory, you can run:
|
||||
|
||||
### `yarn start`
|
||||
|
||||
Runs the app in the development mode.\
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
||||
|
||||
The page will reload if you make edits.\
|
||||
You will also see any lint errors in the console.
|
||||
|
||||
### `yarn test`
|
||||
|
||||
Launches the test runner in the interactive watch mode.\
|
||||
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||
|
||||
### `yarn build`
|
||||
|
||||
Builds the app for production to the `build` folder.\
|
||||
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.\
|
||||
Your app is ready to be deployed!
|
||||
|
||||
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||
|
||||
### `yarn eject`
|
||||
|
||||
**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
|
||||
|
||||
If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
||||
|
||||
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
|
||||
|
||||
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
|
||||
|
||||
## Learn More
|
||||
|
||||
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||
|
||||
To learn React, check out the [React documentation](https://reactjs.org/).
|
||||
|
||||
### Code Splitting
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
|
||||
|
||||
### Analyzing the Bundle Size
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
|
||||
|
||||
### Making a Progressive Web App
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
|
||||
|
||||
### Advanced Configuration
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
|
||||
|
||||
### Deployment
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
|
||||
|
||||
### `yarn build` fails to minify
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
|
@ -1,49 +0,0 @@
|
||||
{
|
||||
"name": "todo-react-app",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@apollo/react-hooks": "^4.0.0",
|
||||
"@testing-library/jest-dom": "^5.11.4",
|
||||
"@testing-library/react": "^11.1.0",
|
||||
"@testing-library/user-event": "^12.1.10",
|
||||
"apollo-cache-inmemory": "^1.6.6",
|
||||
"apollo-client": "^2.6.10",
|
||||
"apollo-link-context": "^1.0.20",
|
||||
"apollo-link-http": "^1.5.17",
|
||||
"classnames": "^2.3.1",
|
||||
"graphql": "^15.6.1",
|
||||
"graphql-tag": "^2.12.5",
|
||||
"history": "^5.0.1",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-router-dom": "^5.3.0",
|
||||
"react-scripts": "4.0.3",
|
||||
"todomvc-app-css": "^2.4.1",
|
||||
"web-vitals": "^1.0.1"
|
||||
},
|
||||
"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"
|
||||
]
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 3.8 KiB |
@ -1,43 +0,0 @@
|
||||
<!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>
|
Before Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 9.4 KiB |
@ -1,25 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
.App {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.App-logo {
|
||||
height: 40vmin;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.App-logo {
|
||||
animation: App-logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.App-header {
|
||||
background-color: #282c34;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: calc(10px + 2vmin);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.App-link {
|
||||
color: #61dafb;
|
||||
}
|
||||
|
||||
@keyframes App-logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
import ApolloClient from "apollo-client";
|
||||
import { InMemoryCache } from "apollo-cache-inmemory";
|
||||
import { ApolloProvider } from "@apollo/react-hooks";
|
||||
import { createHttpLink } from "apollo-link-http";
|
||||
|
||||
import "./App.css";
|
||||
|
||||
const createApolloClient = () => {
|
||||
const httpLink = createHttpLink({
|
||||
uri: "http://localhost:8080/graphql",
|
||||
options: {
|
||||
reconnect: true,
|
||||
},
|
||||
});
|
||||
|
||||
return new ApolloClient({
|
||||
link: httpLink,
|
||||
cache: new InMemoryCache(),
|
||||
});
|
||||
};
|
||||
|
||||
const App = () => {
|
||||
const client = createApolloClient();
|
||||
return (
|
||||
<ApolloProvider client={client}>
|
||||
<div>
|
||||
<h1>todos</h1>
|
||||
<input
|
||||
className="new-todo"
|
||||
placeholder="What needs to be done?"
|
||||
autoFocus={true}
|
||||
/>
|
||||
</div>
|
||||
</ApolloProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
@ -1,8 +0,0 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import App from './App';
|
||||
|
||||
test('renders learn react link', () => {
|
||||
render(<App />);
|
||||
const linkElement = screen.getByText(/learn react/i);
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
});
|
@ -1,13 +0,0 @@
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root')
|
||||
);
|
||||
|
||||
// If you want to start measuring performance in your app, pass a function
|
||||
// to log results (for example: reportWebVitals(console.log))
|
||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||
reportWebVitals();
|
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>
|
Before Width: | Height: | Size: 2.6 KiB |
@ -1,13 +0,0 @@
|
||||
const reportWebVitals = onPerfEntry => {
|
||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||
getCLS(onPerfEntry);
|
||||
getFID(onPerfEntry);
|
||||
getFCP(onPerfEntry);
|
||||
getLCP(onPerfEntry);
|
||||
getTTFB(onPerfEntry);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default reportWebVitals;
|