Move to the example from github because lazy

This commit is contained in:
Ian Adam Naval 2021-10-08 19:29:23 -04:00
parent e9d91ad31e
commit e7d63afee1
Signed by: potato
GPG Key ID: 7E65F1308E7028C4
54 changed files with 14942 additions and 27828 deletions

36
todo-app-react/.gitignore vendored Normal file
View 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
View 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
![Todo App 1](./todo-1.png)
![Todo App 2](./todo-2.png)
---

View 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
}
}
}
}

View 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-----"}

File diff suppressed because it is too large Load Diff

View 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"
]
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View 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>

View 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"
}

View 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"]}

View 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
View 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;

View 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;

View 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
}
}
`;

View 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;
}

View 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;

View 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;

View File

@ -0,0 +1,8 @@
.profile {
padding: 15px;
}
.profile-img {
display: block;
margin: 0 auto;
border-radius: 50%;
}

View 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;

View 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);

View 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();

View 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

View 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>
)
}
}

View 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

View File

@ -0,0 +1,5 @@
export default {
pluralize: (count, word) => {
return count === 1 ? word : word + 's';
}
}

View File

@ -0,0 +1,5 @@
{
"domain": "ianonavy.us.auth0.com",
"clientId": "rdZUt2JRoPq6XutiYmt83SDOwiMTQzIf",
"graphqlUrl": "http://localhost:8080/graphql"
}

View File

@ -0,0 +1,5 @@
export default {
ALL_TODOS: 'all',
ACTIVE_TODOS: 'active',
COMPLETED_TODOS: 'completed',
}

View File

@ -0,0 +1,3 @@
import { createBrowserHistory } from "history";
export default createBrowserHistory();

View 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
View 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>
);
};

View File

@ -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';

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

BIN
todo-app-react/todo-2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -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*

View File

@ -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 cant go back!**
If you arent 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 youre on your own.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt 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)

View File

@ -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"
]
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

View File

@ -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"
}

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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();
});

View File

@ -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;
}

View File

@ -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();

View File

@ -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

View File

@ -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;

File diff suppressed because it is too large Load Diff