add react-apollo-todo with auth0 example (#1083)

This commit is contained in:
Praveen Durairaj 2018-11-21 19:19:40 +05:30 committed by Shahidh K Muhammed
parent f3f1f3e36a
commit 643042c2d5
57 changed files with 2790 additions and 0 deletions

View File

@ -0,0 +1 @@
node_modules

View File

@ -0,0 +1,23 @@
{
"env": {
"browser": true,
"es6": true
},
"extends": ["eslint:recommended", "plugin:react/recommended"],
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": 2018,
"sourceType": "module"
},
"plugins": ["react", "prettier"],
"rules": {
"indent": "off",
"linebreak-style": ["error", "unix"],
"quotes": ["error", "double"],
"semi": ["error", "always"],
"prettier/prettier": "error",
"no-console": "off"
}
}

View File

@ -0,0 +1,15 @@
This app uses environment variables.
```
REACT_APP_GRAPHQL_URL=https://hasura-todo-test.herokuapp.com/v1alpha1/graphql
REACT_APP_REALTIME_GRAPHQL_URL=wss://hasura-todo-test.herokuapp.com/v1alpha1/graphql
REACT_APP_CALLBACK_URL=http://localhost:3000/callback
REACT_APP_AUTH0_DOMAIN=todo-hasura-test.auth0.com
REACT_APP_AUTH0_CLIENT_ID=lgKxyHzCDUWCALdAOkjg3QI2D6eglGes
```
Create a `.env` file and apply these environment variables to work with your React app. Replace it with your Auth0 values appropriately.

View File

@ -0,0 +1,23 @@
FROM node:carbon
ENV NODE_ENV=PRODUCTION
ENV REACT_APP_CALLBACK_URL=https://react-apollo-todo-demo.hasura.app/callback
# Create app directory
WORKDIR /app
# Install app dependencies
RUN npm -g install serve
# A wildcard is used to ensure both package.json AND package-lock.json are copied
COPY package*.json ./
RUN npm install
# Bundle app source
COPY . .
#Build react/vue/angular bundle static files
RUN npm run build
EXPOSE 8080
# serve dist folder on port 8080
CMD ["serve", "-s", "build", "-p", "8080"]

View File

@ -0,0 +1 @@
web: npm start

View File

@ -0,0 +1,14 @@
Tech stack
----------
- Frontend
- React v0.16.3
- Apollo Client 2.1
- Backend
- Hasura GraphQL Engine
Run the React app
-----------------
Run `npm start` to start the todo app.

View File

@ -0,0 +1,6 @@
{
"AUTH0_DOMAIN": "todo-hasura-test.auth0.com",
"AUTH0_CLIENT_ID": "lgKxyHzCDUWCALdAOkjg3QI2D6eglGes",
"AUTH0_USERNAME": "praveen@hasura.io",
"AUTH0_PASSWORD": "praveen123@"
}

View File

@ -0,0 +1,12 @@
{
"baseUrl": "http://localhost:3000",
"env": {
"BASE_URL": "http://localhost:3000",
"TEST_MODE": "parallel"
},
"ignoreTestFiles": ["*spec.js", "validators.js"],
"viewportWidth": 1280,
"viewportHeight": 720,
"chromeWebSecurity": false,
"video": false
}

View File

@ -0,0 +1,5 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}

View File

@ -0,0 +1,16 @@
export const testMode = Cypress.env("TEST_MODE");
export const baseUrl = Cypress.config("baseUrl");
export const setAuthSession = authResult => {
let expiresAt = JSON.stringify(
authResult.expiresIn * 1000 + new Date().getTime()
);
var base64Url = authResult.idToken.split(".")[1];
var base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
const decodedJwt = JSON.parse(window.atob(base64));
const sub = decodedJwt.sub;
window.localStorage.setItem("auth0:access_token", authResult.accessToken);
window.localStorage.setItem("auth0:id_token", authResult.idToken);
window.localStorage.setItem("auth0:expires_at", expiresAt);
window.localStorage.setItem("auth0:id_token:sub", sub);
};

View File

@ -0,0 +1,15 @@
export const baseUrl = Cypress.config("baseUrl");
export const getElementFromAlias = alias => `[data-test=${alias}]`;
export const getTodoName = (i, todoName = "") =>
`tutorial_test_todo_${todoName}_${i}`;
export const getUserName = (i, userName = "") => `${i}_${userName}`;
export const makeDataAPIUrl = dataApiUrl => `${dataApiUrl}/v1/query`;
export const makeDataAPIOptions = (dataApiUrl, key, body) => ({
method: "POST",
url: makeDataAPIUrl(dataApiUrl),
headers: {
"x-hasura-access-key": key
},
body,
failOnStatusCode: false
});

View File

@ -0,0 +1,43 @@
import {
getElementFromAlias,
getTodoName,
baseUrl
} from "../../../helpers/dataHelpers";
import { validateTodo } from "../../validators/validators";
const testName = "privatetodo";
export const checkRoute = () => {
// Check landing page route
cy.visit("/home");
// wait for subscriptions to load
cy.wait(5000);
};
export const createTodo = () => {
cy.get(getElementFromAlias("input-private"))
.clear()
.type(getTodoName(0, testName))
.type("{enter}");
cy.url().should("eq", `${baseUrl}/home`);
// Check if the todo got created
cy.get(getElementFromAlias(`private_0_${getTodoName(0, testName)}`)).contains(
getTodoName(0, testName)
);
// Validate
validateTodo(getTodoName(0, testName), "success", false);
};
export const deleteTodo = () => {
cy.url().should("eq", `${baseUrl}/home`);
// Click on delete
cy.get(
getElementFromAlias(`remove_private_0_${getTodoName(0, testName)}`)
).click();
cy.wait(2000);
cy.get(getElementFromAlias(`private_0_${getTodoName(0, testName)}`)).should(
"not.exist"
);
// Validate
validateTodo(getTodoName(0, testName), "failure", false);
};

View File

@ -0,0 +1,33 @@
/* eslint no-unused-vars: 0 */
/* eslint import/prefer-default-export: 0 */
import { checkRoute, createTodo, deleteTodo } from "./spec";
import { setMetaData } from "../../validators/validators";
const setup = () => {
describe("Setup route", () => {
it("Visit the index route", () => {
// Visit the index route
cy.visit("/home");
cy.wait(5000);
setMetaData();
});
});
};
export const runCreateTodoTests = () => {
describe("Create Private Todo", () => {
beforeEach(function() {
// runs before each test in the block to set localstorage
cy.loginAsAdmin();
cy.wait(5000);
});
it("Opens the correct route", checkRoute);
it("Successfuly creates private todo", createTodo);
it("Delete off the private todo", deleteTodo);
});
};
setup();
runCreateTodoTests();

View File

@ -0,0 +1,43 @@
import {
getElementFromAlias,
getTodoName,
baseUrl
} from "../../../helpers/dataHelpers";
import { validateTodo } from "../../validators/validators";
const testName = "publictodo";
export const checkRoute = () => {
// Check landing page route
cy.visit("/home");
// wait for subscriptions to load
cy.wait(5000);
};
export const createTodo = () => {
cy.get(getElementFromAlias("input-public"))
.clear()
.type(getTodoName(0, testName))
.type("{enter}");
cy.url().should("eq", `${baseUrl}/home`);
// Check if the todo got created
cy.get(getElementFromAlias(`public_0_${getTodoName(0, testName)}`)).contains(
getTodoName(0, testName)
);
// Validate
validateTodo(getTodoName(0, testName), "success", true);
};
export const deleteTodo = () => {
cy.url().should("eq", `${baseUrl}/home`);
// Click on delete
cy.get(
getElementFromAlias(`remove_public_0_${getTodoName(0, testName)}`)
).click();
cy.wait(2000);
cy.get(getElementFromAlias(`public_0_${getTodoName(0, testName)}`)).should(
"not.exist"
);
// Validate
validateTodo(getTodoName(0, testName), "failure", true);
};

View File

@ -0,0 +1,33 @@
/* eslint no-unused-vars: 0 */
/* eslint import/prefer-default-export: 0 */
import { checkRoute, createTodo, deleteTodo } from "./spec";
import { setMetaData } from "../../validators/validators";
const setup = () => {
describe("Setup route", () => {
it("Visit the index route", () => {
// Visit the index route
cy.visit("/home");
cy.wait(5000);
setMetaData();
});
});
};
export const runCreateTodoTests = () => {
describe("Create Public Todo", () => {
beforeEach(function() {
// runs before each test in the block to set localstorage
cy.loginAsAdmin();
cy.wait(5000);
});
it("Opens the correct route", checkRoute);
it("Successfuly creates public todo", createTodo);
it("Delete off the public todo", deleteTodo);
});
};
setup();
runCreateTodoTests();

View File

@ -0,0 +1,22 @@
import {
getElementFromAlias,
getUserName,
baseUrl
} from "../../../helpers/dataHelpers";
import { validateTodo } from "../../validators/validators";
const userName = Cypress.env("AUTH0_USERNAME").split("@")[0];
export const checkRoute = () => {
// Check landing page route
cy.visit("/home");
// wait for subscriptions to load
cy.wait(5000);
};
export const checkOnlineUser = () => {
cy.get(getElementFromAlias(`0_${userName}`)).contains(userName);
cy.url().should("eq", `${baseUrl}/home`);
// Validate
// validateOnlineUser(getUserName(0, userName), "success", true);
};

View File

@ -0,0 +1,32 @@
/* eslint no-unused-vars: 0 */
/* eslint import/prefer-default-export: 0 */
import { checkRoute, checkOnlineUser } from "./spec";
import { setMetaData } from "../../validators/validators";
const setup = () => {
describe("Setup route", () => {
it("Visit the index route", () => {
// Visit the index route
cy.visit("/home");
cy.wait(5000);
setMetaData();
});
});
};
export const runCreateTodoTests = () => {
describe("Online Users Subscription", () => {
beforeEach(function() {
// runs before each test in the block to set localstorage
cy.loginAsAdmin();
cy.wait(5000);
});
it("Opens the correct route", checkRoute);
it("Check online user subscription", checkOnlineUser);
});
};
setup();
runCreateTodoTests();

View File

@ -0,0 +1,41 @@
import { makeDataAPIOptions } from "../../helpers/dataHelpers";
// ***************** UTIL FUNCTIONS **************************
let accessKey;
let dataApiUrl;
export const setMetaData = () => {
cy.window().then(win => {
// accessKey = win.__env.accessKey;
// dataApiUrl = win.__env.dataApiUrl;
accessKey = "abcd";
dataApiUrl = "https://hasura-todo-test.herokuapp.com";
});
};
// ******************* VALIDATION FUNCTIONS *******************************
// ****************** Todo Validator *********************
export const validateTodo = (todoName, result, is_public) => {
const userId = window.localStorage.getItem("auth0:id_token:sub");
const reqBody = {
type: "select",
args: {
table: "todos",
columns: ["*"],
where: { user_id: userId, text: todoName, is_public: is_public }
}
};
const requestOptions = makeDataAPIOptions(dataApiUrl, accessKey, reqBody);
cy.request(requestOptions).then(response => {
console.log(response);
if (result.status === "success") {
console.log("inside success");
expect(response.body.length === 1).to.be.true;
} else if (result.status === "failure") {
console.log("inside failure");
expect(response.body.length === 0).to.be.true;
}
});
};

View File

@ -0,0 +1,17 @@
// ***********************************************************
// This example plugins/index.js can be used to load plugins
//
// You can change the location of this file or turn off loading
// the plugins file with the 'pluginsFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/plugins-guide
// ***********************************************************
// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)
module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
};

View File

@ -0,0 +1,59 @@
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add("login", (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This is will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
const auth0 = require("auth0-js");
import { setAuthSession } from "../helpers/common";
Cypress.Commands.add("loginAsAdmin", (overrides = {}) => {
Cypress.log({
name: "loginAsAdminBySingleSignOn"
});
const webAuth = new auth0.WebAuth({
domain: Cypress.env("AUTH0_DOMAIN"), // Get this from https://manage.auth0.com/#/applications and your application
clientID: Cypress.env("AUTH0_CLIENT_ID"), // Get this from https://manage.auth0.com/#/applications and your application
responseType: "token id_token"
});
webAuth.client.login(
{
realm: "Username-Password-Authentication",
username: Cypress.env("AUTH0_USERNAME"),
password: Cypress.env("AUTH0_PASSWORD"),
audience: "https://todo-hasura-test.auth0.com/api/v2/", // Get this from https://manage.auth0.com/#/apis and your api, use the identifier property
scope: "openid email profile"
},
function(err, authResult) {
// Auth tokens in the result or an error
if (authResult && authResult.accessToken && authResult.idToken) {
window._authResult = authResult;
setAuthSession(authResult);
} else {
console.error("Problem logging into Auth0", err);
throw err;
}
}
);
});

View File

@ -0,0 +1,20 @@
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import "./commands";
// Alternatively you can use CommonJS syntax:
// require('./commands')

View File

@ -0,0 +1,56 @@
{
"name": "react-apollo-todo",
"version": "0.1.0",
"engines": {
"node": "8.9.1"
},
"private": true,
"dependencies": {
"apollo-cache-inmemory": "^1.2.10",
"apollo-client": "^2.4.2",
"apollo-link-context": "^1.0.9",
"apollo-link-http": "^1.5.5",
"apollo-link-ws": "^1.0.9",
"auth0-js": "^9.7.3",
"bootstrap": "^4.1.3",
"graphql": "^14.0.2",
"graphql-tag": "^2.9.2",
"graphqurl": "^0.3.2",
"moment": "^2.22.2",
"prop-types": "^15.6.2",
"react": "^16.5.1",
"react-apollo": "^2.1.11",
"react-bootstrap": "^0.32.4",
"react-dom": "^16.5.1",
"react-router": "^4.3.1",
"react-router-dom": "^4.3.1",
"subscriptions-transport-ws": "^0.9.15"
},
"devDependencies": {
"cypress": "^3.1.0",
"husky": "^1.0.0-rc.15",
"lint-staged": "^7.3.0",
"prettier": "1.14.3",
"react-scripts": "^1.1.5"
},
"scripts": {
"start": "REACT_APP_CALLBACK_URL=http://localhost:3000/callback react-scripts start",
"build": "react-scripts build",
"test": "cypress run --spec 'cypress/integration/**/**/test.js'",
"cypress": "cypress open",
"eject": "react-scripts eject",
"lint": "eslint src/**/*.js",
"lint:fix": "eslint src/**/*.js --fix"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"*.{js,json,css,md}": [
"prettier --write",
"git add"
]
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<!--
manifest.json provides metadata used when your web app is added to the
homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
<link href="https://afeld.github.io/emoji-css/emoji.css" rel="stylesheet">
<link href="https://use.fontawesome.com/releases/v5.0.7/css/all.css" rel="stylesheet" crossorigin="anonymous">
<!--
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 Apollo Todo 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>

View File

@ -0,0 +1,15 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
}
],
"start_url": "./index.html",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@ -0,0 +1,71 @@
import ApolloClient from "apollo-client";
import { HttpLink } from "apollo-link-http";
import { InMemoryCache } from "apollo-cache-inmemory";
import { WebSocketLink } from "apollo-link-ws";
import { split } from "apollo-link";
import { getMainDefinition } from "apollo-utilities";
import { SubscriptionClient } from "subscriptions-transport-ws";
import { setContext } from "apollo-link-context";
import { GRAPHQL_URL, REALTIME_GRAPHQL_URL } from "./utils/constants";
const getHeaders = () => {
const token = localStorage.getItem("auth0:id_token");
const headers = {
authorization: token ? `Bearer ${token}` : ""
};
return headers;
};
const makeApolloClient = () => {
const authLink = setContext((_, { headers }) => {
const token = localStorage.getItem("auth0:id_token");
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : ""
}
};
});
const token = localStorage.getItem("auth0:id_token");
// Create an http link:
const httpLink = new HttpLink({
uri: GRAPHQL_URL,
fetch,
headers: getHeaders(token)
});
// Create a WebSocket link:
const wsLink = new WebSocketLink(
new SubscriptionClient(REALTIME_GRAPHQL_URL, {
reconnect: true,
timeout: 30000,
connectionParams: {
headers: getHeaders(token)
}
})
);
// chose the link to use based on operation
const link = split(
// split based on operation type
({ query }) => {
const { kind, operation } = getMainDefinition(query);
return kind === "OperationDefinition" && operation === "subscription";
},
wsLink,
httpLink
);
const client = new ApolloClient({
link: authLink.concat(link),
cache: new InMemoryCache({
addTypename: true
})
});
return client;
};
export default makeApolloClient;

View File

@ -0,0 +1,67 @@
import React, { Component } from "react";
import PropTypes from "prop-types";
import { Navbar, Button } from "react-bootstrap";
import "../styles/App.css";
class App extends Component {
goTo(route) {
this.props.history.replace(`/${route}`);
}
login() {
this.props.auth.login();
}
logout() {
this.props.auth.logout();
}
componentDidMount() {
if (this.props.auth.isAuthenticated()) {
this.props.history.push("/home");
}
}
render() {
const { isAuthenticated } = this.props.auth;
return (
<div>
<Navbar fluid className="removeMarBottom">
<Navbar.Header className="navheader">
<Navbar.Brand className="navBrand">
GraphQL Tutorial App
</Navbar.Brand>
{!isAuthenticated() && (
<Button
id="qsLoginBtn"
bsStyle="primary"
className="btn-margin logoutBtn"
onClick={this.login.bind(this)}
>
Log In
</Button>
)}
{isAuthenticated() && (
<Button
id="qsLogoutBtn"
bsStyle="primary"
className="btn-margin logoutBtn"
onClick={this.logout.bind(this)}
>
Log Out
</Button>
)}
</Navbar.Header>
</Navbar>
</div>
);
}
}
App.propTypes = {
history: PropTypes.object,
auth: PropTypes.object
};
export default App;

View File

@ -0,0 +1,102 @@
import history from "../../utils/history";
import auth0 from "auth0-js";
import gql from "graphql-tag";
import { AUTH_CONFIG } from "./auth0-variables";
export default class Auth {
auth0 = new auth0.WebAuth({
domain: AUTH_CONFIG.domain,
clientID: AUTH_CONFIG.clientId,
redirectUri: AUTH_CONFIG.callbackUrl,
audience: `https://${AUTH_CONFIG.domain}/userinfo`,
responseType: "token id_token",
scope: "openid profile"
});
constructor() {
this.login = this.login.bind(this);
this.logout = this.logout.bind(this);
this.handleAuthentication = this.handleAuthentication.bind(this);
this.isAuthenticated = this.isAuthenticated.bind(this);
}
login() {
this.auth0.authorize();
}
handleAuthentication = client => {
this.auth0.parseHash((err, authResult) => {
if (authResult && authResult.accessToken && authResult.idToken) {
this.setSession(authResult);
// store in db
this.auth0.client.userInfo(authResult.accessToken, function(err, user) {
// Now you have the user's information
client
.mutate({
mutation: gql`
mutation($userId: String!, $nickname: String) {
insert_users(
objects: [{ auth0_id: $userId, name: $nickname }]
on_conflict: {
constraint: users_pkey
update_columns: [last_seen, name]
}
) {
affected_rows
}
}
`,
variables: {
userId: user.sub,
nickname: user.nickname
}
})
.then(() => {
// history.replace("/home");
window.location.href = "/home";
})
.catch(error => {
console.error(error);
// alert(JSON.stringify(error));
});
});
} else if (err) {
history.replace("/home");
// window.location.href="/home";
console.error(err);
alert(`Error: ${err.error}. Check the console for further details.`);
}
});
};
setSession(authResult) {
// Set the time that the access token will expire at
let expiresAt = JSON.stringify(
authResult.expiresIn * 1000 + new Date().getTime()
);
localStorage.setItem("auth0:access_token", authResult.accessToken);
localStorage.setItem("auth0:id_token", authResult.idToken);
localStorage.setItem("auth0:expires_at", expiresAt);
localStorage.setItem("auth0:id_token:sub", authResult.idTokenPayload.sub);
// navigate to the home route
history.replace("/home");
// window.location.href="/home";
}
logout() {
// Clear access token and ID token from local storage
localStorage.removeItem("auth0:access_token");
localStorage.removeItem("auth0:id_token");
localStorage.removeItem("auth0:expires_at");
localStorage.removeItem("auth0:id_token:sub");
// navigate to the home route
history.replace("/home");
// window.location.href="/home";
}
isAuthenticated() {
// Check whether the current time is past the
// access token's expiry time
let expiresAt = JSON.parse(localStorage.getItem("auth0:expires_at"));
return new Date().getTime() < expiresAt;
}
}

View File

@ -0,0 +1,7 @@
import { authDomain, authClientId, callbackUrl } from "../../utils/constants";
export const AUTH_CONFIG = {
domain: authDomain,
clientId: authClientId,
callbackUrl: callbackUrl
};

View File

@ -0,0 +1,27 @@
import React, { Component } from "react";
import loading from "./loading.svg";
class Callback extends Component {
render() {
const style = {
position: "absolute",
display: "flex",
justifyContent: "center",
height: "100vh",
width: "100vw",
top: 0,
bottom: 0,
left: 0,
right: 0,
backgroundColor: "white"
};
return (
<div style={style}>
<img src={loading} alt="loading" />
</div>
);
}
}
export default Callback;

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="utf-8"?><svg width='120px' height='120px' xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid" class="uil-ring"><rect x="0" y="0" width="100" height="100" fill="none" class="bk"></rect><defs><filter id="uil-ring-shadow" x="-100%" y="-100%" width="300%" height="300%"><feOffset result="offOut" in="SourceGraphic" dx="0" dy="0"></feOffset><feGaussianBlur result="blurOut" in="offOut" stdDeviation="0"></feGaussianBlur><feBlend in="SourceGraphic" in2="blurOut" mode="normal"></feBlend></filter></defs><path d="M10,50c0,0,0,0.5,0.1,1.4c0,0.5,0.1,1,0.2,1.7c0,0.3,0.1,0.7,0.1,1.1c0.1,0.4,0.1,0.8,0.2,1.2c0.2,0.8,0.3,1.8,0.5,2.8 c0.3,1,0.6,2.1,0.9,3.2c0.3,1.1,0.9,2.3,1.4,3.5c0.5,1.2,1.2,2.4,1.8,3.7c0.3,0.6,0.8,1.2,1.2,1.9c0.4,0.6,0.8,1.3,1.3,1.9 c1,1.2,1.9,2.6,3.1,3.7c2.2,2.5,5,4.7,7.9,6.7c3,2,6.5,3.4,10.1,4.6c3.6,1.1,7.5,1.5,11.2,1.6c4-0.1,7.7-0.6,11.3-1.6 c3.6-1.2,7-2.6,10-4.6c3-2,5.8-4.2,7.9-6.7c1.2-1.2,2.1-2.5,3.1-3.7c0.5-0.6,0.9-1.3,1.3-1.9c0.4-0.6,0.8-1.3,1.2-1.9 c0.6-1.3,1.3-2.5,1.8-3.7c0.5-1.2,1-2.4,1.4-3.5c0.3-1.1,0.6-2.2,0.9-3.2c0.2-1,0.4-1.9,0.5-2.8c0.1-0.4,0.1-0.8,0.2-1.2 c0-0.4,0.1-0.7,0.1-1.1c0.1-0.7,0.1-1.2,0.2-1.7C90,50.5,90,50,90,50s0,0.5,0,1.4c0,0.5,0,1,0,1.7c0,0.3,0,0.7,0,1.1 c0,0.4-0.1,0.8-0.1,1.2c-0.1,0.9-0.2,1.8-0.4,2.8c-0.2,1-0.5,2.1-0.7,3.3c-0.3,1.2-0.8,2.4-1.2,3.7c-0.2,0.7-0.5,1.3-0.8,1.9 c-0.3,0.7-0.6,1.3-0.9,2c-0.3,0.7-0.7,1.3-1.1,2c-0.4,0.7-0.7,1.4-1.2,2c-1,1.3-1.9,2.7-3.1,4c-2.2,2.7-5,5-8.1,7.1 c-0.8,0.5-1.6,1-2.4,1.5c-0.8,0.5-1.7,0.9-2.6,1.3L66,87.7l-1.4,0.5c-0.9,0.3-1.8,0.7-2.8,1c-3.8,1.1-7.9,1.7-11.8,1.8L47,90.8 c-1,0-2-0.2-3-0.3l-1.5-0.2l-0.7-0.1L41.1,90c-1-0.3-1.9-0.5-2.9-0.7c-0.9-0.3-1.9-0.7-2.8-1L34,87.7l-1.3-0.6 c-0.9-0.4-1.8-0.8-2.6-1.3c-0.8-0.5-1.6-1-2.4-1.5c-3.1-2.1-5.9-4.5-8.1-7.1c-1.2-1.2-2.1-2.7-3.1-4c-0.5-0.6-0.8-1.4-1.2-2 c-0.4-0.7-0.8-1.3-1.1-2c-0.3-0.7-0.6-1.3-0.9-2c-0.3-0.7-0.6-1.3-0.8-1.9c-0.4-1.3-0.9-2.5-1.2-3.7c-0.3-1.2-0.5-2.3-0.7-3.3 c-0.2-1-0.3-2-0.4-2.8c-0.1-0.4-0.1-0.8-0.1-1.2c0-0.4,0-0.7,0-1.1c0-0.7,0-1.2,0-1.7C10,50.5,10,50,10,50z" fill="#337ab7" filter="url(#uil-ring-shadow)"><animateTransform attributeName="transform" type="rotate" from="0 50 50" to="360 50 50" repeatCount="indefinite" dur="1s"></animateTransform></path></svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -0,0 +1,142 @@
import React, { Component } from "react";
import PropTypes from "prop-types";
import moment from "moment";
import gql from "graphql-tag";
import "../../styles/App.css";
import TodoPublicWrapper from "../Todo/TodoPublicWrapper";
import TodoPrivateWrapper from "../Todo/TodoPrivateWrapper";
import OnlineUsers from "../OnlineUsers/OnlineUsers";
import { Navbar, Button } from "react-bootstrap";
class App extends Component {
login() {
this.props.auth.login();
}
logout() {
this.props.auth.logout();
}
updateLastSeen = () => {
const userId = localStorage.getItem("auth0:id_token:sub");
const timestamp = moment().format();
if (this.props.client) {
this.props.client
.mutate({
mutation: gql`
mutation($userId: String!, $timestamp: timestamptz!) {
update_users(
where: { auth0_id: { _eq: $userId } }
_set: { auth0_id: $userId, last_seen: $timestamp }
) {
affected_rows
}
}
`,
variables: {
userId: userId,
timestamp: timestamp
}
})
.then(() => {
// handle response if required
})
.catch(error => {
console.error(error);
});
}
};
componentDidMount() {
// eslint-disable-next-line
const lastSeenMutation = setInterval(this.updateLastSeen.bind(this), 5000);
}
render() {
const { isAuthenticated } = this.props.auth;
if (!isAuthenticated()) {
window.location.href = "/";
}
return (
<div>
<Navbar fluid className="removeMarBottom">
<Navbar.Header className="navheader">
<Navbar.Brand className="navBrand">
React Apollo Todo App
</Navbar.Brand>
{!isAuthenticated() && (
<Button
id="qsLoginBtn"
bsStyle="primary"
className="btn-margin logoutBtn"
onClick={this.login.bind(this)}
>
Log In
</Button>
)}
{isAuthenticated() && (
<Button
id="qsLogoutBtn"
bsStyle="primary"
className="btn-margin logoutBtn"
onClick={this.logout.bind(this)}
>
Log Out
</Button>
)}
</Navbar.Header>
</Navbar>
<div>
<div className="col-xs-12 col-md-12 col-lg-9 col-sm-12 noPadd">
<div>
<div className="col-md-6 col-sm-12">
<div className="wd95 addPaddTopBottom">
<div className="sectionHeader">Personal todos</div>
<TodoPrivateWrapper client={this.props.client} />
</div>
</div>
<div className="col-xs-12 col-md-6 col-sm-12 grayBgColor todoMainWrapper commonBorRight">
<div className="wd95 addPaddTopBottom">
<div className="sectionHeader">Public todos</div>
<TodoPublicWrapper client={this.props.client} />
</div>
</div>
</div>
</div>
<div className="col-xs-12 col-lg-3 col-md-12 col-sm-12 noPadd">
<OnlineUsers />
</div>
</div>
<div className="footerWrapper">
<span>
<a
href="https://react-apollo-todo-demo.hasura.app/console"
target="_blank"
rel="noopener noreferrer"
>
Backend
<i className="fa fa-angle-double-right" />
</a>
</span>
<span className="footerLinkPadd accessKey">
<button>
Access Key: hasurademoapp
</button>
</span>
<span className="footerLinkPadd">
<a
href="https://github.com/hasura/graphql-engine/tree/master/community/examples/react-apollo-todo"
target="_blank"
rel="noopener noreferrer"
>
Github
<i className="fa fa-angle-double-right" />
</a>
</span>
</div>
</div>
);
}
}
App.propTypes = {
auth: PropTypes.object,
isAuthenticated: PropTypes.bool
};
export default App;

View File

@ -0,0 +1,174 @@
import React, { Component } from "react";
import PropTypes from "prop-types";
import "../../styles/App.css";
import { Link } from "react-router-dom";
class LandingPage extends Component {
login() {
this.props.auth.login();
}
logout() {
this.props.auth.logout();
}
render() {
const { isAuthenticated } = this.props.auth;
const reactLogo = require("../../images/React-logo.png");
const authLogo = require("../../images/auth.png");
const graphql = require("../../images/graphql.png");
const hasuraLogo = require("../../images/green-logo-white.svg");
const apolloLogo = require("../../images/apollo.png");
const rightImg = require("../../images/right-img.png");
return (
<div className="container-fluid gradientBgColor minHeight">
<div>
<div className="headerWrapper">
<div className="headerDescription">
{isAuthenticated() && (
<Link to="/home">Realtime React Todo App Demo</Link>
)}
{!isAuthenticated() && <span>Realtime React Todo App Demo</span>}
</div>
<div className="loginBtn">
{!isAuthenticated() && (
<button
id="qsLoginBtn"
bsStyle="primary"
className="btn-margin logoutBtn"
onClick={this.login.bind(this)}
>
Log In
</button>
)}
{isAuthenticated() && (
<button
id="qsLogoutBtn"
bsStyle="primary"
className="btn-margin logoutBtn"
onClick={this.logout.bind(this)}
>
Log Out
</button>
)}
</div>
</div>
<div className="mainWrapper">
<div className="col-md-5 col-sm-6 col-xs-12 noPadd">
<div className="appstackWrapper">
<div className="appStack">
<div className="col-md-1 col-sm-1 col-xs-2 removePaddLeft flexWidth">
<i className="em em---1" />
</div>
<div className="col-md-11 col-sm-11 col-xs-10 noPadd">
<div className="description">
Try out a realtime app that uses
</div>
<div className="appStackIconWrapper">
<div className="col-md-4 col-sm-4 col-xs-4 noPadd">
<div className="appStackIcon">
<img
className="img-responsive"
src={reactLogo}
alt="React logo"
/>
</div>
</div>
<div className="col-md-4 col-sm-4 col-xs-4 noPadd">
<div className="appStackIcon">
<img
className="img-responsive"
src={authLogo}
alt="Auth0 logo"
/>
</div>
</div>
<div className="col-md-4 col-sm-4 col-xs-4 noPadd">
<div className="appStackIcon">
<img
className="img-responsive"
src={graphql}
alt="GraphQL logo"
/>
</div>
</div>
</div>
</div>
</div>
<div className="appStack">
<div className="col-md-1 col-sm-1 col-xs-2 removePaddLeft flexWidth">
<i className="em em-rocket" />
</div>
<div className="col-md-11 col-sm-11 col-xs-10 noPadd">
<div className="description">Powered by</div>
<div className="appStackIconWrapper">
<div className="col-md-4 col-sm-4 col-xs-4 noPadd">
<div className="appStackIcon">
<img
className="img-responsive"
src={apolloLogo}
alt="apollo logo"
/>
</div>
</div>
<div className="col-md-4 col-sm-4 col-xs-4 noPadd">
<div className="appStackIcon">
<img
className="img-responsive"
src={hasuraLogo}
alt="Hasura logo"
/>
</div>
</div>
</div>
</div>
</div>
<div className="appStack">
<div className="col-md-1 col-sm-1 col-xs-2 removePaddLeft flexWidth">
<i className="em em-sunglasses" />
</div>
<div className="col-md-11 col-sm-11 col-xs-10 noPadd">
<div className="description removePaddBottom">
Explore the Hasura console and try out some queries &
mutations
</div>
</div>
</div>
<div className="appStack removePaddBottom">
<div className="col-md-1 col-sm-1 col-xs-2 removePaddLeft flexWidth">
<i className="em em-zap" />
</div>
<div className="col-md-11 col-sm-11 col-xs-10 noPadd">
<div className="description removePaddBottom">
Full tutorial coming soon!
</div>
</div>
</div>
</div>
<div className="footer">
Built with
<i className="fas fa-heart" />
by{" "}
<a
href="https://hasura.io/"
target="_blank"
rel="noopener noreferrer"
>
Hasura
</a>
</div>
</div>
<div className="tutorialImg col-md-6 col-sm-6 col-xs-12 hidden-xs noPadd">
<img className="img-responsive" src={rightImg} alt="View" />
</div>
</div>
</div>
</div>
);
}
}
LandingPage.propTypes = {
auth: PropTypes.object,
isAuthenticated: PropTypes.bool
};
export default LandingPage;

View File

@ -0,0 +1,52 @@
import React, { Component } from "react";
import { Subscription } from "react-apollo";
import gql from "graphql-tag";
const SUBSCRIPTION_ONLINE_USERS = gql`
subscription {
online_users(order_by: { name: asc }) {
name
}
}
`;
class OnlineUsers extends Component {
render() {
return (
<Subscription subscription={SUBSCRIPTION_ONLINE_USERS}>
{({ loading, error, data }) => {
if (loading) {
return <div>Loading. Please wait...</div>;
}
if (error) {
return <div>Error loading users</div>;
}
return (
<div className="sliderMenu grayBgColor">
<div className="sliderHeader">
Online users - {data.online_users.length}
</div>
{data.online_users.map((user, index) => {
return (
<div key={user.name} className="userInfo">
<div className="userImg">
<i className="far fa-user" />
</div>
<div
data-test={index + "_" + user.name}
className="userName"
>
{user.name}
</div>
</div>
);
})}
</div>
);
}}
</Subscription>
);
}
}
export default OnlineUsers;

View File

@ -0,0 +1,60 @@
import React from "react";
import PropTypes from "prop-types";
const TodoFilters = ({
todos,
currentFilter,
type,
filterResults,
clearCompleted,
clearInProgress
}) => {
const activeTodos = todos.filter(todo => todo.is_completed !== true);
return (
<div className="footerList">
<span> {activeTodos.length} items left </span>
<ul>
<li onClick={() => filterResults("all")}>
<a className={currentFilter === "all" ? "selected" : ""}>All</a>
</li>
<li onClick={() => filterResults("active")}>
<a
className={
currentFilter === "active"
? "selected removePaddLeft"
: "removePaddLeft"
}
>
Active
</a>
</li>
<li onClick={() => filterResults("completed")}>
<a
className={
currentFilter === "completed"
? "selected removePaddLeft"
: "removePaddLeft"
}
>
Completed
</a>
</li>
</ul>
{type === "private" ? (
<button onClick={() => clearCompleted(type)} className="clearComp">
{clearInProgress ? "Clearing" : "Clear completed"}
</button>
) : null}
</div>
);
};
TodoFilters.propTypes = {
todos: PropTypes.array.isRequired,
userId: PropTypes.string,
type: PropTypes.string,
currentFilter: PropTypes.string,
filterResults: PropTypes.func
};
export default TodoFilters;

View File

@ -0,0 +1,108 @@
import React from "react";
import PropTypes from "prop-types";
import { Mutation } from "react-apollo";
import "../../styles/App.css";
import { QUERY_PRIVATE_TODO, MUTATION_TODO_ADD } from "./TodoQueries";
class TodoInput extends React.Component {
constructor() {
super();
this.state = {
textboxValue: ""
};
this.handleTextboxValueChange = this.handleTextboxValueChange.bind(this);
this.handleTextboxKeyPress = this.handleTextboxKeyPress.bind(this);
}
handleTextboxValueChange(e) {
this.setState({
...this.state,
textboxValue: e.target.value
});
}
handleTextboxKeyPress(e, addTodo) {
if (e.key === "Enter") {
const newTodo = this.state.textboxValue;
const userId = this.props.userId;
const isPublic = this.props.type === "public" ? true : false;
addTodo({
variables: {
objects: [
{
text: newTodo,
user_id: userId,
is_completed: false,
is_public: isPublic
}
]
},
update: (store, { data: { insert_todos } }) => {
const query = QUERY_PRIVATE_TODO;
try {
if (this.props.type === "private") {
const data = store.readQuery({
query: query,
variables: { userId: this.props.userId }
});
const insertedTodo = insert_todos.returning;
data.todos.splice(0, 0, insertedTodo[0]);
store.writeQuery({
query: query,
variables: {
userId: this.props.userId
},
data
});
}
} catch (e) {
console.error(e);
}
this.setState({
...this.state,
textboxValue: ""
});
}
});
}
}
render() {
return (
<Mutation mutation={MUTATION_TODO_ADD}>
{(addTodo, { error }) => {
if (error) {
alert("Something went wrong");
}
return (
<div className="formInput">
<input
className="input"
data-test={
this.props.type === "private"
? "input-private"
: "input-public"
}
placeholder="What needs to be done?"
value={this.state.textboxValue}
onChange={this.handleTextboxValueChange}
onKeyPress={e => {
this.handleTextboxKeyPress(e, addTodo);
}}
/>
<i className="downArrow fa fa-angle-down" />
</div>
);
}}
</Mutation>
);
}
}
TodoInput.propTypes = {
userId: PropTypes.string,
type: PropTypes.string
};
export default TodoInput;

View File

@ -0,0 +1,201 @@
import React from "react";
import PropTypes from "prop-types";
import { Mutation } from "react-apollo";
import "../../styles/App.css";
import {
QUERY_PRIVATE_TODO,
QUERY_PUBLIC_TODO,
MUTATION_TODO_UPDATE,
MUTATION_TODO_DELETE
} from "./TodoQueries";
const handleTodoToggle = (
toggleTodo,
todo,
type,
userId,
completePublicTodoClicked
) => {
toggleTodo({
variables: {
todoId: todo.id,
set: {
is_completed: !todo.is_completed
}
},
update: (cache, { data: { update_todo } }) => {
// eslint-disable-line
const query = type === "private" ? QUERY_PRIVATE_TODO : QUERY_PUBLIC_TODO;
if (type === "private") {
const data = cache.readQuery({
query: query,
variables: { userId: userId }
});
const toggledTodo = data.todos.find(t => t.id === todo.id);
toggledTodo.is_completed = !todo.is_completed;
cache.writeQuery({
query: query,
variables: {
userId: userId
},
data
});
} else if (type === "public") {
completePublicTodoClicked(todo);
}
}
});
};
const handleTodoDelete = (
deleteTodo,
todo,
type,
userId,
deletePublicTodoClicked
) => {
deleteTodo({
variables: {
todoId: todo.id
},
update: (cache, { data: { update_todo } }) => {
// eslint-disable-line
const query = type === "private" ? QUERY_PRIVATE_TODO : QUERY_PUBLIC_TODO;
if (type === "private") {
const data = cache.readQuery({
query: query,
variables: { userId: userId }
});
data.todos = data.todos.filter(t => {
return t.id !== todo.id;
});
cache.writeQuery({
query: query,
variables: {
userId: userId
},
data
});
} else if (type === "public") {
deletePublicTodoClicked(todo);
}
}
});
};
const TodoItem = ({
index,
todo,
type,
userId,
completePublicTodoClicked,
deletePublicTodoClicked
}) => (
<Mutation mutation={MUTATION_TODO_UPDATE}>
{updateTodo => {
return (
<Mutation mutation={MUTATION_TODO_DELETE}>
{deleteTodo => {
return (
<li
onClick={() => {
handleTodoToggle(
updateTodo,
todo,
type,
userId,
completePublicTodoClicked
);
}}
>
{todo.is_public ? (
<div className="userInfoPublic" title={todo.user.name}>
{todo.user.name.charAt(0).toUpperCase()}
</div>
) : null}
<div className="view">
{todo.is_completed ? (
<div className="round">
<input
checked={true}
type="checkbox"
id={todo.id}
onChange={() => {
handleTodoToggle(
updateTodo,
todo,
type,
userId,
completePublicTodoClicked
);
}}
/>
<label htmlFor={todo.id} />
</div>
) : (
<div className="round">
<input
type="checkbox"
checked={false}
id={todo.id}
onChange={() => {
handleTodoToggle(
updateTodo,
todo,
type,
userId,
completePublicTodoClicked
);
}}
/>
<label htmlFor={todo.id} />)
</div>
)}
</div>
<div className="labelContent">
{todo.is_completed ? (
<strike className="todoLabel">
<div data-test={type + "_" + index + "_" + todo.text}>
{todo.text}
</div>
</strike>
) : (
<div data-test={type + "_" + index + "_" + todo.text}>
{todo.text}
</div>
)}
</div>
<button
className="closeBtn"
data-test={"remove_" + type + "_" + index + "_" + todo.text}
onClick={e => {
e.preventDefault();
e.stopPropagation();
handleTodoDelete(
deleteTodo,
todo,
type,
userId,
deletePublicTodoClicked
);
}}
>
x
</button>
</li>
);
}}
</Mutation>
);
}}
</Mutation>
);
TodoItem.propTypes = {
todo: PropTypes.object.isRequired,
type: PropTypes.string,
userId: PropTypes.string
};
export default TodoItem;

View File

@ -0,0 +1,107 @@
import React, { Component, Fragment } from "react";
import PropTypes from "prop-types";
import { Query } from "react-apollo";
import { GRAPHQL_URL } from "../../utils/constants";
import TodoItem from "./TodoItem";
import TodoFilters from "./TodoFilters";
import { QUERY_PRIVATE_TODO } from "./TodoQueries";
class TodoPrivateList extends Component {
constructor() {
super();
this.state = { filter: "all", clearInProgress: false };
}
filterResults(type) {
this.setState({ filter: type });
}
clearCompleted(type) {
// mutation to delete all is_completed with is_public clause
const isOk = window.confirm("Are you sure?");
if (isOk) {
this.setState({ clearInProgress: true });
const isPublic = type === "public" ? true : false;
this.props.client
.query({
query: `
mutation ($isPublic: Boolean!) {
delete_todos (
where: { is_completed: {_eq: true}, is_public: {_eq: $isPublic}}
) {
affected_rows
}
}
`,
endpoint: GRAPHQL_URL,
variables: {
isPublic: isPublic
}
})
.then(() => {
// handle response
this.setState({ clearInProgress: false });
})
.catch(error => {
this.setState({ clearInProgress: false });
console.error(error);
});
}
}
render() {
const { userId, type } = this.props;
return (
<Query query={QUERY_PRIVATE_TODO} variables={{ userId: userId }}>
{({ loading, error, data, refetch }) => {
if (loading) {
return <div>Loading. Please wait...</div>;
}
if (error) {
return <div>{""}</div>;
}
refetch();
// apply filters for displaying todos
let finalData = data.todos;
if (this.state.filter === "active") {
finalData = data.todos.filter(todo => todo.is_completed !== true);
} else if (this.state.filter === "completed") {
finalData = data.todos.filter(todo => todo.is_completed === true);
}
return (
<Fragment>
<div className="todoListwrapper">
<ul>
{finalData.map((todo, index) => {
return (
<TodoItem
key={index}
index={index}
todo={todo}
type={type}
userId={userId}
/>
);
})}
</ul>
</div>
<TodoFilters
todos={data.todos}
userId={userId}
type={type}
currentFilter={this.state.filter}
filterResults={this.filterResults.bind(this)}
clearCompleted={this.clearCompleted.bind(this)}
clearInProgress={this.state.clearInProgress}
/>
</Fragment>
);
}}
</Query>
);
}
}
TodoPrivateList.propTypes = {
userId: PropTypes.string.isRequired,
type: PropTypes.string.isRequired
};
export default TodoPrivateList;

View File

@ -0,0 +1,22 @@
import React, { Component } from "react";
import TodoPrivateList from "./TodoPrivateList";
import TodoInput from "./TodoInput";
import "../../styles/App.css";
class TodoPrivateWrapper extends Component {
render() {
const userId = localStorage.getItem("auth0:id_token:sub");
return (
<div className="todoWrapper">
<TodoInput userId={userId} type="private" />
<TodoPrivateList
userId={userId}
client={this.props.client}
type="private"
/>
</div>
);
}
}
export default TodoPrivateWrapper;

View File

@ -0,0 +1,201 @@
import React, { Component, Fragment } from "react";
import PropTypes from "prop-types";
import TodoItem from "./TodoItem";
import TodoFilters from "./TodoFilters";
import {
SUBSCRIPTION_TODO_PUBLIC_LIST,
QUERY_PUBLIC_TODO,
QUERY_FEED_PUBLIC_TODO,
QUERY_FEED_PUBLIC_OLD_TODO
} from "./TodoQueries";
class TodoPublicList extends Component {
constructor() {
super();
this.state = {
filter: "all",
dataLength: 0,
showNew: false,
showOlder: true,
newTodosLength: 0,
limit: 5,
todos: []
};
this.deletePublicTodoClicked = this.deletePublicTodoClicked.bind(this);
this.completePublicTodoClicked = this.completePublicTodoClicked.bind(this);
this.loadMoreClicked = this.loadMoreClicked.bind(this);
this.loadOlderClicked = this.loadOlderClicked.bind(this);
this.filterResults = this.filterResults.bind(this);
}
componentDidMount() {
const { client } = this.props;
const _this = this;
// query for public todos
client
.query({
query: QUERY_PUBLIC_TODO,
variables: { todoLimit: this.state.limit }
})
.then(data => {
this.setState({ todos: data.data.todos });
const latestTodoId = data.data.todos.length
? data.data.todos[0].id
: null;
// start a subscription
client
.subscribe({
query: SUBSCRIPTION_TODO_PUBLIC_LIST,
variables: { todoId: latestTodoId } // update subscription when todoId changes
})
.subscribe({
next(data) {
if (data.data.todos.length) {
_this.setState({
...this.state,
showNew: true,
newTodosLength:
_this.state.newTodosLength + data.data.todos.length
});
}
},
error(err) {
console.error("err", err);
}
});
});
}
filterResults(type) {
this.setState({ filter: type });
}
loadMoreClicked() {
const { client } = this.props;
this.setState({ ...this.state, showNew: false, newTodosLength: 0 });
client
.query({
query: QUERY_FEED_PUBLIC_TODO,
variables: {
todoId: this.state.todos.length ? this.state.todos[0].id : null
}
})
.then(data => {
if (data.data.todos.length) {
const mergedTodos = data.data.todos.concat(this.state.todos);
// update state with new todos
this.setState({ ...this.state, todos: mergedTodos });
}
});
}
loadOlderClicked() {
const { client } = this.props;
client
.query({
query: QUERY_FEED_PUBLIC_OLD_TODO,
variables: {
todoId: this.state.todos.length
? this.state.todos[this.state.todos.length - 1].id
: null
}
})
.then(data => {
if (data.data.todos.length) {
const mergedTodos = this.state.todos.concat(data.data.todos);
// update state with new todos
this.setState({ ...this.state, todos: mergedTodos });
} else {
this.setState({ ...this.state, showOlder: false });
}
});
}
deletePublicTodoClicked(deletedTodo) {
const finalTodos = this.state.todos.filter(t => {
return t.id !== deletedTodo.id;
});
this.setState({ ...this.state, todos: finalTodos });
}
completePublicTodoClicked(completedTodo) {
const finalTodos = this.state.todos.filter(t => {
if (t.id === completedTodo.id) {
t.is_completed = !t.is_completed;
return t;
}
return t;
});
this.setState({ ...this.state, todos: finalTodos });
}
render() {
const { userId, type } = this.props;
// apply client side filters for displaying todos
let finalTodos = this.state.todos;
if (this.state.filter === "active") {
finalTodos = this.state.todos.filter(todo => todo.is_completed !== true);
} else if (this.state.filter === "completed") {
finalTodos = this.state.todos.filter(todo => todo.is_completed === true);
}
// show new todo feed logic
let showNewTodos = null;
if (this.state.showNew && this.state.newTodosLength) {
showNewTodos = (
<div className={"loadMoreSection"} onClick={this.loadMoreClicked}>
You have {this.state.newTodosLength} new{" "}
{this.state.newTodosLength > 1 ? "todos" : "todo"}
</div>
);
}
// show old todo history logic
let showOlderTodos = (
<div className={"loadMoreSection"} onClick={this.loadOlderClicked}>
Load Older Todos
</div>
);
if (!this.state.showOlder && this.state.todos.length) {
showOlderTodos = (
<div className={"loadMoreSection"} onClick={this.loadOlderClicked}>
No more todos available
</div>
);
}
return (
<Fragment>
<div className="todoListwrapper">
{showNewTodos}
<ul>
{finalTodos.map((todo, index) => {
return (
<TodoItem
key={index}
index={index}
todo={todo}
type={type}
userId={userId}
client={this.props.client}
deletePublicTodoClicked={this.deletePublicTodoClicked}
completePublicTodoClicked={this.completePublicTodoClicked}
/>
);
})}
</ul>
{showOlderTodos}
</div>
<TodoFilters
todos={this.state.todos}
userId={userId}
type={type}
currentFilter={this.state.filter}
filterResults={this.filterResults}
/>
</Fragment>
);
}
}
TodoPublicList.propTypes = {
userId: PropTypes.string,
client: PropTypes.object,
type: PropTypes.string
};
export default TodoPublicList;

View File

@ -0,0 +1,22 @@
import React, { Component } from "react";
import TodoPublicList from "./TodoPublicList";
import TodoInput from "./TodoInput";
import "../../styles/App.css";
class TodoPublicWrapper extends Component {
render() {
const userId = localStorage.getItem("auth0:id_token:sub");
return (
<div className="todoWrapper">
<TodoInput userId={userId} type="public" />
<TodoPublicList
userId={userId}
type="public"
client={this.props.client}
/>
</div>
);
}
}
export default TodoPublicWrapper;

View File

@ -0,0 +1,137 @@
import gql from "graphql-tag";
const TODO_FRAGMENT = gql`
fragment TodoFragment on todos {
id
text
is_completed
created_at
is_public
}
`;
const USER_FRAGMENT = gql`
fragment UserFragment on users {
name
}
`;
const QUERY_PRIVATE_TODO = gql`
query fetch_todos($userId: String!) {
todos(
where: { is_public: { _eq: false }, user_id: { _eq: $userId } }
order_by: { created_at: desc }
) {
...TodoFragment
}
}
${TODO_FRAGMENT}
`;
const QUERY_PUBLIC_TODO = gql`
query fetch_todos($todoLimit: Int, $todoId: Int) {
todos(
where: { is_public: { _eq: true }, id: { _gt: $todoId } }
order_by: { created_at: desc }
limit: $todoLimit
) {
...TodoFragment
user {
...UserFragment
}
}
}
${TODO_FRAGMENT}
${USER_FRAGMENT}
`;
const QUERY_FEED_PUBLIC_TODO = gql`
query fetch_todos($todoId: Int) {
todos(
where: { is_public: { _eq: true }, id: { _gt: $todoId } }
order_by: { created_at: desc }
) {
...TodoFragment
user {
...UserFragment
}
}
}
${TODO_FRAGMENT}
${USER_FRAGMENT}
`;
const QUERY_FEED_PUBLIC_OLD_TODO = gql`
query fetch_todos($todoId: Int) {
todos(
where: { is_public: { _eq: true }, id: { _lt: $todoId } }
limit: 5
order_by: { created_at: desc }
) {
...TodoFragment
user {
...UserFragment
}
}
}
${TODO_FRAGMENT}
${USER_FRAGMENT}
`;
const MUTATION_TODO_ADD = gql`
mutation insert_todos($objects: [todos_insert_input!]) {
insert_todos(objects: $objects) {
affected_rows
returning {
id
text
is_completed
created_at
is_public
}
}
}
`;
const MUTATION_TODO_UPDATE = gql`
mutation update_todos($todoId: Int, $set: todos_set_input!) {
update_todos(where: { id: { _eq: $todoId } }, _set: $set) {
affected_rows
}
}
`;
const MUTATION_TODO_DELETE = gql`
mutation delete_todos($todoId: Int) {
delete_todos(where: { id: { _eq: $todoId } }) {
affected_rows
}
}
`;
const SUBSCRIPTION_TODO_PUBLIC_LIST = gql`
subscription($todoId: Int) {
todos(
where: { is_public: { _eq: true }, id: { _gt: $todoId } }
order_by: { created_at: desc }
limit: 1
) {
id
text
is_completed
created_at
is_public
}
}
`;
export {
QUERY_PRIVATE_TODO,
QUERY_PUBLIC_TODO,
QUERY_FEED_PUBLIC_TODO,
QUERY_FEED_PUBLIC_OLD_TODO,
MUTATION_TODO_ADD,
MUTATION_TODO_UPDATE,
MUTATION_TODO_DELETE,
SUBSCRIPTION_TODO_PUBLIC_LIST
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" viewBox="0 0 26 26" enable-background="new 0 0 26 26" width="512px" height="512px">
<path d="m.3,14c-0.2-0.2-0.3-0.5-0.3-0.7s0.1-0.5 0.3-0.7l1.4-1.4c0.4-0.4 1-0.4 1.4,0l.1,.1 5.5,5.9c0.2,0.2 0.5,0.2 0.7,0l13.4-13.9h0.1v-8.88178e-16c0.4-0.4 1-0.4 1.4,0l1.4,1.4c0.4,0.4 0.4,1 0,1.4l0,0-16,16.6c-0.2,0.2-0.4,0.3-0.7,0.3-0.3,0-0.5-0.1-0.7-0.3l-7.8-8.4-.2-.3z" fill="#91DC5A"/>
</svg>

After

Width:  |  Height:  |  Size: 523 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View File

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 220 80" style="enable-background:new 0 0 220 80;" xml:space="preserve">
<style type="text/css">
.st0{fill:#102954;}
.st1{fill:#FFFFFF;}
</style>
<g>
<g>
<g>
<path class="st0" d="M67.4,26.8c2.1-5.2,2.2-16-0.7-24.4l0,0c-0.7-1.5-3-1.1-3.1,0.7v0.6c-0.5,7.9-3.4,12.2-7.6,14.2
c-0.7,0.3-1.8,0.2-2.5-0.2c-5.1-3.2-11-5.1-17.5-5.1s-12.4,1.9-17.5,5.1c-0.7,0.4-1.5,0.5-2.2,0.2C12,16.3,8.9,11.5,8.4,3.6V3.1
c0-1.6-2.3-2.1-3.1-0.7c-3,8.3-2.9,19.1-0.7,24.4c1.1,2.6,1.1,5.6,0.2,8.3c-1.2,3.4-1.8,7.2-1.7,11c0.3,17.4,15.1,32.2,32.4,32.4
c18.3,0.2,33.3-14.6,33.3-32.9c0-3.7-0.6-7.2-1.7-10.5C66.3,32.4,66.4,29.4,67.4,26.8z"/>
</g>
<ellipse class="st1" cx="36" cy="45.5" rx="25" ry="25"/>
<path class="st0" d="M39.9,42.9L34,33.8c-1-1.5-2.9-1.9-4.4-1c-0.9,0.6-1.5,1.6-1.5,2.7c0,0.6,0.2,1.2,0.6,1.7l4,6.2
c0.3,0.5,0.2,1.1-0.1,1.5l-6.2,6.8c-1.1,1.3-1.1,3.3,0.2,4.5c0.6,0.6,1.4,0.8,2.2,0.8c0.9,0,1.7-0.4,2.3-1.1l4.6-5.4
c0.3-0.4,1-0.4,1.3,0.1l3.3,4.7c0.2,0.3,0.5,0.7,0.9,1c1.1,0.8,2.5,0.7,3.5,0.1l0,0c0.9-0.6,1.5-1.6,1.5-2.7
c0-0.6-0.2-1.2-0.5-1.7L39.9,42.9z"/>
</g>
<g>
<path class="st0" d="M96.8,45.6h-3.9c-0.6,0-1.1-0.5-1.1-1.1V32.2c0-0.6-0.5-1.1-1.1-1.1h-3.4c-0.6,0-1.1,0.5-1.1,1.1v31.3
c0,0.6,0.5,1.1,1.1,1.1h3.4c0.6,0,1.1-0.5,1.1-1.1V51.3c0-0.6,0.5-1.1,1.1-1.1h3.9c0.6,0,1.1,0.5,1.1,1.1v12.2
c0,0.6,0.5,1.1,1.1,1.1h3.4c0.6,0,1.1-0.5,1.1-1.1V32.2c0-0.6-0.5-1.1-1.1-1.1H99c-0.6,0-1.1,0.5-1.1,1.1v12.3
C97.9,45.1,97.4,45.6,96.8,45.6z"/>
<path class="st0" d="M114.2,32l-5.5,31.3c-0.1,0.6,0.4,1.2,1,1.2h3.4c0.5,0,1-0.4,1-0.9l0.9-5.7c0.1-0.5,0.5-0.9,1-0.9h4.3
c0.5,0,1,0.4,1,0.9l1,5.8c0.1,0.5,0.5,0.9,1,0.9h3.5c0.7,0,1.2-0.6,1-1.3L122,32c-0.1-0.5-0.5-0.9-1-0.9h-5.8
C114.7,31.1,114.3,31.5,114.2,32z M119.3,52.3h-2.2c-0.7,0-1.1-0.6-1-1.2l1-11.5c0.2-1.2,1.9-1.2,2.1,0l1.1,11.5
C120.4,51.7,119.9,52.3,119.3,52.3z"/>
<path class="st0" d="M143,45.2h-3.8c-0.7,0-1.1-0.3-1.1-1.1v-7.2c0-0.7,0.4-1.1,1.1-1.1h2.2c0.7,0,1.1,0.3,1.1,1.1v3.4
c0,0.6,0.5,1.1,1.1,1.1h3.5c0.6,0,1.1-0.5,1.1-1.1v-4.2c0-3.3-1.8-5-5.3-5h-5.1c-3.5,0-5.3,1.7-5.3,5v8.6c0,3.3,1.8,5.1,5.2,5.1
h3.8c0.7,0,1.1,0.3,1.1,1.1v8c0,0.7-0.3,1.1-1.1,1.1h-2.2c-0.7,0-1.1-0.3-1.1-1.1v-3.4c0-0.6-0.5-1.1-1.1-1.1h-3.5
c-0.6,0-1.1,0.5-1.1,1.1v4.2c0,3.3,1.8,5,5.3,5h5c3.5,0,5.3-1.7,5.3-5v-9.4C148.2,46.9,146.4,45.2,143,45.2z"/>
<path class="st0" d="M164,58.8c0,0.7-0.3,1.1-1.1,1.1h-3c-0.7,0-1.1-0.3-1.1-1.1V32.2c0-0.6-0.5-1.1-1.1-1.1h-3.5
c-0.6,0-1.1,0.5-1.1,1.1v27.3c0,3.3,1.8,5,5.3,5h5.8c3.5,0,5.3-1.7,5.3-5V32.2c0-0.6-0.5-1.1-1.1-1.1h-3.3c-0.6,0-1.1,0.5-1.1,1.1
L164,58.8L164,58.8z"/>
<path class="st0" d="M191.8,46.3V36.1c0-3.3-1.8-5-5.3-5h-10c-0.6,0-1.1,0.5-1.1,1.1v31.3c0,0.6,0.5,1.1,1.1,1.1h3.4
c0.6,0,1.1-0.5,1.1-1.1v-11c0-0.6,0.5-1.1,1.1-1.1l0,0c0.4,0,0.8,0.3,1,0.7l4.4,11.8c0.2,0.4,0.6,0.7,1,0.7h3.7
c0.7,0,1.3-0.7,1-1.4L189,52.3c-0.2-0.5,0.1-1.1,0.6-1.4C190.9,50.1,191.8,48.5,191.8,46.3z M186.2,36.9v8.8
c0,0.7-0.4,1.1-1.1,1.1H182c-0.6,0-1.1-0.5-1.1-1.1v-8.8c0-0.6,0.5-1.1,1.1-1.1h3.1C185.8,35.8,186.2,36.2,186.2,36.9z"/>
<path class="st0" d="M210.2,31.1h-5.8c-0.5,0-1,0.4-1,0.9l-5.5,31.3c-0.1,0.6,0.4,1.2,1,1.2h3.4c0.5,0,1-0.4,1-0.9l0.9-5.7
c0.1-0.5,0.5-0.9,1-0.9h4.3c0.5,0,1,0.4,1,0.9l1,5.8c0.1,0.5,0.5,0.9,1,0.9h3.5c0.7,0,1.2-0.6,1-1.3L211.2,32
C211.1,31.5,210.7,31.1,210.2,31.1z M208.4,52.3h-2.1c-0.7,0-1.1-0.6-1-1.2l1-10.5c0.2-1.2,1.9-1.2,2.1,0l1.1,10.5
C209.6,51.7,209.1,52.3,208.4,52.3z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@ -0,0 +1,7 @@
<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>

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 KiB

View File

@ -0,0 +1,5 @@
import ReactDOM from "react-dom";
import { makeMainRoutes } from "./routes";
const routes = makeMainRoutes();
ReactDOM.render(routes, document.getElementById("root"));

View File

@ -0,0 +1,58 @@
import React from "react";
import { Route, Router } from "react-router-dom";
import Home from "./components/Home/Home";
import Callback from "./components/Callback/Callback";
import Auth from "./components/Auth/Auth";
import LandingPage from "./components/LandingPage/LandingPage";
import history from "./utils/history";
import { ApolloProvider } from "react-apollo";
import makeApolloClient from "./apollo";
const client = makeApolloClient();
const provideClient = component => {
return <ApolloProvider client={client}>{component}</ApolloProvider>;
};
const auth = new Auth();
const handleAuthentication = ({ location }) => {
if (/access_token|id_token|error/.test(location.hash)) {
auth.handleAuthentication(client);
}
};
export const makeMainRoutes = () => {
return (
<Router history={history}>
<div>
<Route
exact
path="/"
render={props =>
provideClient(
<LandingPage auth={auth} client={client} {...props} />
)
}
/>
<Route
exact
path="/home"
render={props =>
provideClient(<Home auth={auth} client={client} {...props} />)
}
/>
<Route
exact
path="/callback"
render={props => {
handleAuthentication(props);
return <Callback {...props} />;
}}
/>
</div>
</Router>
);
};

View File

@ -0,0 +1,559 @@
@import url("https://fonts.googleapis.com/css?family=Open+Sans:400,600,700");
@import url("https://fonts.googleapis.com/css?family=Raleway:400,600,700");
body {
background-color: #f7f7f7;
font-family: "Open Sans";
font-weight: 400;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-size: 10px;
font-family: sans-serif;
}
/* Landing section */
.wd10 {
width: 10%;
display: inline-block;
}
.wd90 {
width: 90%;
display: inline-block;
}
.removePaddBottom {
padding-bottom: 0 !important;
}
.gradientBgColor {
background-color: #a0b4cc;
/* Safari 4-5, Chrome 1-9 */
background: -webkit-gradient(
linear,
0% 0%,
0% 100%,
from(#a0b4cc),
to(#c2a899)
);
/* Safari 5.1, Chrome 10+ */
background: -webkit-linear-gradient(top, #a0b4cc, #c2a899);
/* Firefox 3.6+ */
background: -moz-linear-gradient(top, #a0b4cc, #c2a899);
/* IE 10 */
background: -ms-linear-gradient(top, #a0b4cc, #c2a899);
/* Opera 11.10+ */
background: -o-linear-gradient(top, #a0b4cc, #c2a899);
}
.minHeight {
width: 100%;
height: 100vh;
}
.headerWrapper {
padding: 30px 0;
display: flex;
align-items: center;
height: 15vh;
padding-left: 75px;
}
.headerDescription {
font-size: 20px;
color: #fff;
font-family: "Raleway";
font-weight: 700;
flex: 1;
}
.headerDescription a {
color: #fff;
}
.headerDescription a:hover {
text-decoration: none;
border-bottom: 1px solid #fff;
}
.loginBtn {
text-align: right;
padding-right: 75px;
}
.loginBtn button {
background-color: #8da5c1;
border: 1px solid #fff;
border-radius: 20px;
padding: 4px 30px;
font-family: "Raleway";
font-size: 16px;
color: #fff;
font-weight: 600;
cursor: pointer;
}
.loginBtn button:hover {
background-color: #7792b2;
}
.loginBtn button:focus {
outline: none;
}
.mainWrapper {
padding-left: 75px;
width: 100%;
float: left;
height: 85vh;
display: flex;
align-items: center;
}
.description {
font-size: 16px;
padding-bottom: 10px;
}
.appstackWrapper {
background-color: #fff;
border-radius: 5px;
padding: 30px;
-webkit-box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.1);
-moz-box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.1);
box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.1);
width: 100%;
float: left;
color: #606060;
}
.arrow {
position: absolute;
right: -100px;
top: 10px;
z-index: 1;
}
.arrow img {
width: 120px;
display: inline-block;
}
.appStack {
width: 100%;
float: left;
padding-bottom: 20px;
}
.appStack i {
font-size: 16px;
}
.appStackIconWrapper {
width: 100%;
float: left;
display: flex;
align-items: center;
}
.appStackIcon img {
width: 80%;
}
.footer {
padding-top: 10px;
text-align: center;
clear: both;
font-size: 14px;
color: #3c3737;
}
.footer a {
color: #3c3737;
}
.footer a:hover {
text-decoration: none;
border-bottom: 1px solid #3c3737;
}
.footer i {
color: #b51d04;
margin: 0 5px;
}
.tutorialImg {
position: absolute;
right: 0;
text-align: right;
}
.tutorialImg img {
width: 95%;
display: inline-block;
}
/* Landing section */
.wd95 {
width: 95%;
margin: 0 auto;
}
.navheader {
width: 100%;
}
.navbar {
padding: 5px 10px;
}
.navBrand {
padding-top: 14px;
font-size: 12.5px;
margin-right: 10px;
padding-bottom: 3.125px;
padding-left: 15px;
padding-right: 15px;
}
.logoutBtn {
margin-top: 12px;
float: right;
font-size: 10px;
padding: 3.75px 7.5px;
}
.header {
background-color: #4f5050;
padding: 20px;
font-size: 20px;
text-align: left;
font-weight: 600;
color: #cbcbcb;
max-height: 50px;
height: 50px;
display: flex;
align-items: center;
padding: 0 20px;
}
.noPadd {
padding-left: 0;
padding-right: 0;
}
.grayBgColor {
background-color: #efeded;
}
.removePaddRight {
padding-right: 0;
}
.todoWrapper {
width: 100%;
position: relative;
}
.sectionHeader {
font-size: 18px;
font-weight: 400;
padding-bottom: 20px;
}
.addPaddTopBottom {
padding: 30px 0;
}
.commonBorRight {
border-right: 1px solid #ccc;
}
.formInput input {
height: 60px;
padding: 16px 16px 16px 60px;
border: none;
background-color: #fff;
width: 100%;
-webkit-box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.1);
-moz-box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.1);
box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.1);
opacity: 0.6;
}
.formInput input:focus {
outline: none;
}
.formInput {
position: relative;
display: flex;
align-items: center;
}
.downArrow {
position: absolute;
left: 15px;
font-size: 25px;
opacity: 0.6;
}
.todoListwrapper {
border-top: 1px solid #e6e6e6;
-webkit-box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.1);
-moz-box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.1);
box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.1);
}
.todoListwrapper ul {
-webkit-padding-start: 0px;
-moz-padding-start: 0px;
margin-bottom: 0;
}
.todoListwrapper ul li {
list-style-type: none;
min-height: 60px;
max-height: 60px;
/* overflow: auto; */
display: flex;
font-size: 14px;
align-items: center;
border-bottom: 1px solid #ededed;
background-color: #fff;
position: relative;
}
.closeBtn {
background-color: transparent;
border: none;
color: #cc9a9a;
position: absolute;
right: 15px;
padding: 0;
font-size: 25px;
}
.closeBtn i {
font-size: 20px;
}
.closeBtn:hover {
color: #af5b5e;
}
.closeBtn:focus {
outline: none;
}
.labelContent {
padding-left: 1px;
color: #777;
width: calc(100% - 128px);
max-height: 60px;
overflow: auto;
display: flex;
align-items: center;
}
.view {
width: 28px;
height: 28px;
margin: 0 15px;
}
.footerList {
height: 60px;
background-color: #fff;
border-bottom: 1px solid #ededed;
display: flex;
align-items: center;
padding: 0 15px;
margin-bottom: 30px;
}
.footerList:before {
content: "";
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: 50px;
overflow: hidden;
box-shadow: 0 1px 0px rgba(0, 0, 0, 0.2), 0 8px 0 -3px #f6f6f6,
0 9px 1px -3px rgba(0, 0, 0, 0.2), 0 16px 0 -6px #f6f6f6,
0 17px 2px -6px rgba(0, 0, 0, 0.2);
-moz-box-shadow: 0 1px 0px rgba(0, 0, 0, 0.2), 0 8px 0 -3px #f6f6f6,
0 9px 1px -3px rgba(0, 0, 0, 0.2), 0 16px 0 -6px #f6f6f6,
0 17px 2px -6px rgba(0, 0, 0, 0.2);
-webkit-box-shadow: 0 1px 0px rgba(0, 0, 0, 0.2), 0 8px 0 -3px #f6f6f6,
0 9px 1px -3px rgba(0, 0, 0, 0.2), 0 16px 0 -6px #f6f6f6,
0 17px 2px -6px rgba(0, 0, 0, 0.2);
}
.footerList ul {
-webkit-padding-start: 0px;
-moz-padding-start: 0px;
margin-bottom: 0;
padding-left: 20px;
z-index: 1;
}
.footerList ul li {
list-style-type: none;
display: inline-block;
}
.footerList ul li a {
padding: 3px 7px;
border: 1px solid transparent;
border-radius: 3px;
color: #777;
cursor: pointer;
}
.footerList ul li a.selected {
border-color: rgba(175, 47, 47, 0.2);
}
.clearComp {
background-color: transparent;
border: none;
color: #777;
position: absolute;
right: 15px;
padding: 0;
}
.clearComp:focus {
outline: none;
}
.todoMainWrapper {
height: calc(100vh - 50px);
overflow-y: auto;
}
.displayBlock {
display: block;
}
.removeMarBottom {
margin-bottom: 0;
}
.sliderMenu {
height: calc(100vh - 50px);
overflow-y: auto;
}
.sliderHeader {
padding: 10px 0;
padding-left: 15px;
padding-top: 30px;
text-transform: uppercase;
font-weight: 600;
font-size: 10px;
}
.userInfo {
padding: 10px 0;
padding-left: 15px;
display: flex;
align-items: center;
cursor: pointer;
}
.userInfo:hover {
background-color: #d1d0d0;
}
.userImg {
text-align: center;
padding-right: 10px;
}
.userImg i {
font-size: 20px;
}
.userImg img {
width: 30px;
}
/*Custom Checkbox */
.round {
position: relative;
}
.round label {
background-color: #fff;
border: 1px solid #ccc;
border-radius: 50%;
cursor: pointer;
height: 28px;
left: 0;
position: absolute;
top: 0;
width: 28px;
min-width: 28px;
}
.round label:after {
border: 2px solid #66bb6a;
border-top: none;
border-right: none;
content: "";
height: 6px;
left: 7px;
opacity: 0;
position: absolute;
top: 8px;
transform: rotate(-45deg);
width: 12px;
}
.round input[type="checkbox"] {
visibility: hidden;
}
.round input[type="checkbox"]:checked + label {
background-color: #fff;
border-color: #66bb6a;
}
.removePaddLeft {
padding-left: 0;
}
.removePaddLeft {
}
.round input[type="checkbox"]:checked + label:after {
opacity: 1;
}
.loadMoreSection {
list-style-type: none;
height: 30px;
display: -ms-flexbox;
display: flex;
font-size: 14px;
-ms-flex-align: center;
align-items: center;
border-bottom: 1px solid #ededed;
background-color: #e6ecf0;
position: relative;
justify-content: center;
cursor: pointer;
}
.userInfoPublic {
width: 28px;
min-width: 28px;
height: 28px;
min-height: 28px;
border-radius: 50%;
background-color: #dfe3e6;
display: flex;
align-items: center;
justify-content: center;
margin-left: 10px;
font-size: 12px;
font-weight: bold;
cursor: pointer;
}
.footerWrapper {
background-color: #a5b9cc;
padding: 10px 0;
text-align: center;
position: fixed;
bottom: 0;
width: 100%;
z-index: 1;
font-size: 14px;
font-weight: bold;
}
.footerWrapper a {
color: #1d4060;
}
.footerWrapper a:hover {
color: #406282;
}
.footerWrapper a i {
margin-left: 5px;
}
.footerWrapper .footerLinkPadd {
margin-left: 20px;
}
.footerWrapper .accessKey button {
background-color: #a5b9cc;
border: 0;
}
@media (max-width: 991px) {
.sliderMenu {
height: auto;
}
.labelContent {
display: block;
}
.addPaddTopBottom {
padding: 0px 0;
}
.sliderHeader,
.userInfo {
width: 95%;
margin: 0 auto;
}
.headerWrapper {
padding-left: 0;
height: auto;
}
.mainWrapper {
min-height: 80vh;
padding-left: 0;
}
.minHeight {
height: auto;
min-height: 100vh;
}
.loginBtn {
padding-right: 0;
}
.appstackWrapper {
padding: 20px;
}
.appStack {
display: flex;
}
.flexWidth {
flex: 1;
}
.description {
padding-left: 10px;
}
.appStackIconWrapper {
padding-left: 10px;
}
}

View File

@ -0,0 +1,7 @@
export const GRAPHQL_URL =
"https://react-apollo-todo-demo.hasura.app/v1alpha1/graphql";
export const REALTIME_GRAPHQL_URL =
"wss://react-apollo-todo-demo.hasura.app/v1alpha1/graphql";
export const authClientId = "Fl-hdc6xdYIkok9ynbcL6zoUZPAIdOZN";
export const authDomain = "hasura-react-apollo-todo.auth0.com";
export const callbackUrl = process.env.REACT_APP_CALLBACK_URL;

View File

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

View File

@ -0,0 +1,9 @@
const getHeaders = () => {
const token = localStorage.getItem("auth0:id_token");
const headers = {
authorization: token ? `Bearer ${token}` : ""
};
return headers;
};
export { getHeaders };