add react-apollo-todo with auth0 example (#1083)
1
community/examples/react-apollo-todo/.dockerignore
Normal file
@ -0,0 +1 @@
|
|||||||
|
node_modules
|
23
community/examples/react-apollo-todo/.eslintrc.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
15
community/examples/react-apollo-todo/CONTRIBUTING.md
Normal 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.
|
23
community/examples/react-apollo-todo/Dockerfile
Normal 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"]
|
1
community/examples/react-apollo-todo/Procfile
Normal file
@ -0,0 +1 @@
|
|||||||
|
web: npm start
|
14
community/examples/react-apollo-todo/README.md
Normal 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.
|
6
community/examples/react-apollo-todo/cypress.env.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"AUTH0_DOMAIN": "todo-hasura-test.auth0.com",
|
||||||
|
"AUTH0_CLIENT_ID": "lgKxyHzCDUWCALdAOkjg3QI2D6eglGes",
|
||||||
|
"AUTH0_USERNAME": "praveen@hasura.io",
|
||||||
|
"AUTH0_PASSWORD": "praveen123@"
|
||||||
|
}
|
12
community/examples/react-apollo-todo/cypress.json
Normal 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
|
||||||
|
}
|
@ -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"
|
||||||
|
}
|
16
community/examples/react-apollo-todo/cypress/helpers/common.js
vendored
Normal 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);
|
||||||
|
};
|
15
community/examples/react-apollo-todo/cypress/helpers/dataHelpers.js
vendored
Normal 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
|
||||||
|
});
|
43
community/examples/react-apollo-todo/cypress/integration/todos/private-todos/spec.js
vendored
Normal 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);
|
||||||
|
};
|
33
community/examples/react-apollo-todo/cypress/integration/todos/private-todos/test.js
vendored
Normal 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();
|
43
community/examples/react-apollo-todo/cypress/integration/todos/public-todos/spec.js
vendored
Normal 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);
|
||||||
|
};
|
33
community/examples/react-apollo-todo/cypress/integration/todos/public-todos/test.js
vendored
Normal 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();
|
22
community/examples/react-apollo-todo/cypress/integration/users/online/spec.js
vendored
Normal 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);
|
||||||
|
};
|
32
community/examples/react-apollo-todo/cypress/integration/users/online/test.js
vendored
Normal 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();
|
41
community/examples/react-apollo-todo/cypress/integration/validators/validators.js
vendored
Normal 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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
17
community/examples/react-apollo-todo/cypress/plugins/index.js
vendored
Normal 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
|
||||||
|
};
|
59
community/examples/react-apollo-todo/cypress/support/commands.js
vendored
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
20
community/examples/react-apollo-todo/cypress/support/index.js
vendored
Normal 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')
|
56
community/examples/react-apollo-todo/package.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
BIN
community/examples/react-apollo-todo/public/favicon.ico
Normal file
After Width: | Height: | Size: 3.8 KiB |
43
community/examples/react-apollo-todo/public/index.html
Normal 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>
|
15
community/examples/react-apollo-todo/public/manifest.json
Normal 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"
|
||||||
|
}
|
71
community/examples/react-apollo-todo/src/apollo.js
vendored
Normal 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;
|
67
community/examples/react-apollo-todo/src/components/App.js
vendored
Normal 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;
|
102
community/examples/react-apollo-todo/src/components/Auth/Auth.js
vendored
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
7
community/examples/react-apollo-todo/src/components/Auth/auth0-variables.js
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { authDomain, authClientId, callbackUrl } from "../../utils/constants";
|
||||||
|
|
||||||
|
export const AUTH_CONFIG = {
|
||||||
|
domain: authDomain,
|
||||||
|
clientId: authClientId,
|
||||||
|
callbackUrl: callbackUrl
|
||||||
|
};
|
27
community/examples/react-apollo-todo/src/components/Callback/Callback.js
vendored
Normal 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;
|
@ -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 |
142
community/examples/react-apollo-todo/src/components/Home/Home.js
vendored
Normal 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;
|
174
community/examples/react-apollo-todo/src/components/LandingPage/LandingPage.js
vendored
Normal 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;
|
52
community/examples/react-apollo-todo/src/components/OnlineUsers/OnlineUsers.js
vendored
Normal 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;
|
60
community/examples/react-apollo-todo/src/components/Todo/TodoFilters.js
vendored
Normal 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;
|
108
community/examples/react-apollo-todo/src/components/Todo/TodoInput.js
vendored
Normal 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;
|
201
community/examples/react-apollo-todo/src/components/Todo/TodoItem.js
vendored
Normal 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;
|
107
community/examples/react-apollo-todo/src/components/Todo/TodoPrivateList.js
vendored
Normal 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;
|
22
community/examples/react-apollo-todo/src/components/Todo/TodoPrivateWrapper.js
vendored
Normal 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;
|
201
community/examples/react-apollo-todo/src/components/Todo/TodoPublicList.js
vendored
Normal 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;
|
22
community/examples/react-apollo-todo/src/components/Todo/TodoPublicWrapper.js
vendored
Normal 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;
|
137
community/examples/react-apollo-todo/src/components/Todo/TodoQueries.js
vendored
Normal 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
|
||||||
|
};
|
BIN
community/examples/react-apollo-todo/src/images/React-logo.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
community/examples/react-apollo-todo/src/images/apollo.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
community/examples/react-apollo-todo/src/images/auth.png
Normal file
After Width: | Height: | Size: 4.0 KiB |
@ -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 |
BIN
community/examples/react-apollo-todo/src/images/graphql.png
Normal file
After Width: | Height: | Size: 36 KiB |
@ -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 |
7
community/examples/react-apollo-todo/src/images/logo.svg
Normal 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 |
BIN
community/examples/react-apollo-todo/src/images/right-img.png
Normal file
After Width: | Height: | Size: 249 KiB |
BIN
community/examples/react-apollo-todo/src/images/right-img1.png
Normal file
After Width: | Height: | Size: 260 KiB |
5
community/examples/react-apollo-todo/src/index.js
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import ReactDOM from "react-dom";
|
||||||
|
import { makeMainRoutes } from "./routes";
|
||||||
|
|
||||||
|
const routes = makeMainRoutes();
|
||||||
|
ReactDOM.render(routes, document.getElementById("root"));
|
58
community/examples/react-apollo-todo/src/routes.js
vendored
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
559
community/examples/react-apollo-todo/src/styles/App.css
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
7
community/examples/react-apollo-todo/src/utils/constants.js
vendored
Normal 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;
|
3
community/examples/react-apollo-todo/src/utils/history.js
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import createHistory from "history/createBrowserHistory";
|
||||||
|
|
||||||
|
export default createHistory();
|
9
community/examples/react-apollo-todo/src/utils/utils.js
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
const getHeaders = () => {
|
||||||
|
const token = localStorage.getItem("auth0:id_token");
|
||||||
|
const headers = {
|
||||||
|
authorization: token ? `Bearer ${token}` : ""
|
||||||
|
};
|
||||||
|
return headers;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { getHeaders };
|