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