├── .eslintrc
├── .gitignore
├── LICENSE
├── README.md
├── backend
├── .babelrc
├── .gitignore
├── config.json
├── package.json
├── scripts
│ └── updateSchema.js
└── src
│ ├── data
│ └── Todo.js
│ ├── graphql
│ ├── Todo
│ │ ├── index.js
│ │ └── mutations
│ │ │ ├── AddTodoMutation.js
│ │ │ ├── ChangeTodoStatusMutation.js
│ │ │ ├── MarkAllTodosMutation.js
│ │ │ ├── RemoveCompletedTodosMutation.js
│ │ │ ├── RemoveTodoMutation.js
│ │ │ └── RenameTodoMutation.js
│ ├── Viewer.js
│ ├── node.js
│ └── schema.js
│ ├── index.js
│ ├── middlewares
│ ├── errorHandler.js
│ └── graphql.js
│ └── utils
│ └── logger.js
├── frontend
├── .babelrc
├── .gitignore
├── config.json
├── package.json
├── scripts
│ └── babelRelayPlugin.js
├── src
│ ├── app.js
│ ├── routes.js
│ ├── screens
│ │ └── TodoApp
│ │ │ ├── components
│ │ │ └── TodoListFooter.js
│ │ │ ├── index.js
│ │ │ ├── mutations
│ │ │ ├── AddTodoMutation.js
│ │ │ └── RemoveCompletedTodosMutation.js
│ │ │ ├── screens
│ │ │ └── TodoList
│ │ │ │ ├── components
│ │ │ │ └── Todo.js
│ │ │ │ ├── images
│ │ │ │ ├── checked.svg
│ │ │ │ └── unchecked.svg
│ │ │ │ ├── index.js
│ │ │ │ ├── mutations
│ │ │ │ ├── ChangeTodoStatusMutation.js
│ │ │ │ ├── MarkAllTodosMutation.js
│ │ │ │ ├── RemoveTodoMutation.js
│ │ │ │ └── RenameTodoMutation.js
│ │ │ │ ├── styles
│ │ │ │ ├── Todo.css
│ │ │ │ └── TodoList.css
│ │ │ │ └── views
│ │ │ │ ├── TodoListView.js
│ │ │ │ └── TodoView.js
│ │ │ ├── shared
│ │ │ ├── components
│ │ │ │ └── TodoTextInput.js
│ │ │ └── styles
│ │ │ │ └── edit.css
│ │ │ ├── styles
│ │ │ ├── TodoApp.css
│ │ │ └── TodoListFooter.css
│ │ │ └── views
│ │ │ ├── TodoAppView.js
│ │ │ └── TodoListFooterView.js
│ ├── server
│ │ ├── components
│ │ │ └── Page.js
│ │ ├── index.js
│ │ ├── middlewares
│ │ │ ├── errorHandler.js
│ │ │ ├── page.js
│ │ │ ├── webpackDev.js
│ │ │ └── webpackHot.js
│ │ └── utils
│ │ │ ├── logger.js
│ │ │ └── webpackCompiler.js
│ ├── shared
│ │ └── utils
│ │ │ └── history.js
│ └── styles
│ │ └── app.css
├── webpack-client.config.js
└── webpack-server.config.js
├── package.json
├── proxy
├── .babelrc
├── .gitignore
├── config.json
├── package.json
└── src
│ ├── index.js
│ ├── middlewares
│ ├── errorHandler.js
│ └── proxy.js
│ └── utils
│ └── logger.js
└── server.js
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "node": true
4 | },
5 | "extends": "airbnb",
6 | "parser": "babel-eslint",
7 | "rules": {
8 | // TODO: remove that line when eslint-plugin-react is fixed (now it produces false positives)
9 | "react/prefer-stateless-function": 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | npm-debug.log
2 | node_modules/
3 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016-present Denis Nedelyaev
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Isomorphic Relay Boilerplate (Starter Kit)
2 | ==========================================
3 |
4 | Features
5 | --------
6 | - Single page app with **isomorphic rendering** (i.e. supports server-side rendering).
7 | - Uses GraphQL and Relay.
8 | - Search engine indexable.
9 | - CSS Modules.
10 | - Cache boosting.
11 | - Hot Module Replacement for browser side code (in development mode).
12 | - Clear separation between the back-end and front-end.
13 | - Example based on TodoMVC.
14 |
15 | The directory structure for the front-end follows recommendations by
16 | [@ryanflorence](https://github.com/ryanflorence) presented
17 | [here](https://gist.github.com/ryanflorence/daafb1e3cb8ad740b346).
18 |
19 | Todo
20 | ----
21 | - User sessions.
22 | - Flow annotations.
23 | - Infrastructure for unit testing.
24 | - Infrastructure for E2E testing.
25 |
--------------------------------------------------------------------------------
/backend/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "es2015",
4 | "stage-0"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/backend/.gitignore:
--------------------------------------------------------------------------------
1 | lib/
2 | npm-debug.log
3 | node_modules/
4 | schema.graphql
5 | schema.json
6 |
--------------------------------------------------------------------------------
/backend/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "port": 4444
3 | }
4 |
--------------------------------------------------------------------------------
/backend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "dependencies": {
4 | "express-graphql": "^0.4.9",
5 | "graphql": "^0.4.18",
6 | "graphql-relay": "^0.3.6"
7 | },
8 | "scripts": {
9 | "build": "babel src -d lib",
10 | "postinstall": "npm run update-schema && npm run build",
11 | "start": "node lib",
12 | "update-schema": "babel-node scripts/updateSchema.js"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/backend/scripts/updateSchema.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env babel-node
2 | import fs from 'fs';
3 | import path from 'path';
4 | import { graphql } from 'graphql';
5 | import { introspectionQuery, printSchema } from 'graphql/utilities';
6 |
7 | import logger from '../src/utils/logger';
8 | import schema from '../src/graphql/schema';
9 |
10 | // Save JSON of full schema introspection for Babel Relay Plugin to use
11 | (async () => {
12 | const result = await graphql(schema, introspectionQuery);
13 | if (result.errors) {
14 | logger.error(
15 | 'ERROR introspecting schema: ',
16 | JSON.stringify(result.errors, null, 2)
17 | );
18 | } else {
19 | fs.writeFileSync(
20 | path.resolve(__dirname, '..', 'schema.json'),
21 | JSON.stringify(result, null, 2)
22 | );
23 | }
24 | })();
25 |
26 | // Save user readable type system shorthand of schema
27 | fs.writeFileSync(
28 | path.resolve(__dirname, '..', 'schema.graphql'),
29 | printSchema(schema)
30 | );
31 |
--------------------------------------------------------------------------------
/backend/src/data/Todo.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This file provided by Facebook is for non-commercial testing and evaluation
3 | * purposes only. Facebook reserves all rights not expressly granted.
4 | *
5 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
6 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
7 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
8 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
9 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
10 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
11 | */
12 |
13 | export default class Todo {}
14 |
15 | const todosById = new Map;
16 | const todoIds = [];
17 | let nextTodoId = 0;
18 |
19 | export function addTodo(text, complete) {
20 | const todo = new Todo();
21 | todo.complete = !!complete;
22 | todo.id = String(nextTodoId++);
23 | todo.text = text;
24 | todosById.set(todo.id, todo);
25 | todoIds.push(todo.id);
26 | return todo.id;
27 | }
28 |
29 | export function getTodo(id) {
30 | return todosById.get(id);
31 | }
32 |
33 | export function changeTodoStatus(id, complete) {
34 | const todo = getTodo(id);
35 | todo.complete = complete;
36 | }
37 |
38 | export function getTodos(status = 'any') {
39 | const todos = todoIds.map(id => todosById.get(id));
40 | return status === 'any' ?
41 | todos :
42 | todos.filter(todo => todo.complete === (status === 'completed'));
43 | }
44 |
45 | export function markAllTodos(complete) {
46 | return getTodos().filter(todo => todo.complete !== complete).map(todo => {
47 | /* eslint-disable no-param-reassign */
48 | todo.complete = complete;
49 | /* eslint-enable no-param-reassign */
50 | return todo.id;
51 | });
52 | }
53 |
54 | export function removeTodo(id) {
55 | const todoIndex = todoIds.indexOf(id);
56 | if (todoIndex !== -1) {
57 | todoIds.splice(todoIndex, 1);
58 | }
59 |
60 | delete todosById[id];
61 | }
62 |
63 | export function removeCompletedTodos() {
64 | return getTodos().filter(todo => todo.complete).map(todo => {
65 | removeTodo(todo.id);
66 | return todo.id;
67 | });
68 | }
69 |
70 | export function renameTodo(id, text) {
71 | const todo = getTodo(id);
72 | todo.text = text;
73 | }
74 |
75 | // Mock data
76 | addTodo('Taste JavaScript', true);
77 | addTodo('Buy a unicorn', false);
78 |
--------------------------------------------------------------------------------
/backend/src/graphql/Todo/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This file provided by Facebook is for non-commercial testing and evaluation
3 | * purposes only. Facebook reserves all rights not expressly granted.
4 | *
5 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
6 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
7 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
8 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
9 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
10 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
11 | */
12 |
13 | import { GraphQLBoolean, GraphQLObjectType, GraphQLString } from 'graphql';
14 | import { connectionDefinitions, globalIdField } from 'graphql-relay';
15 | import Todo, { getTodo } from '../../data/Todo';
16 | import { nodeInterface, registerNodeFetcher } from '../node';
17 |
18 | const TODO_TYPE_NAME = 'Todo';
19 |
20 | registerNodeFetcher(TODO_TYPE_NAME, id => getTodo(id));
21 |
22 | const GraphQLTodo = new GraphQLObjectType({
23 | name: TODO_TYPE_NAME,
24 | fields: () => ({
25 | id: globalIdField(),
26 | text: {
27 | type: GraphQLString,
28 | resolve: obj => obj.text,
29 | },
30 | complete: {
31 | type: GraphQLBoolean,
32 | resolve: obj => obj.complete,
33 | },
34 | }),
35 | interfaces: () => [nodeInterface],
36 | isTypeOf: obj => obj instanceof Todo,
37 | });
38 |
39 | export const {
40 | connectionType: GraphQLTodoConnection,
41 | edgeType: GraphQLTodoEdge,
42 | } = connectionDefinitions({ nodeType: GraphQLTodo });
43 |
44 | export default GraphQLTodo;
45 |
--------------------------------------------------------------------------------
/backend/src/graphql/Todo/mutations/AddTodoMutation.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This file provided by Facebook is for non-commercial testing and evaluation
3 | * purposes only. Facebook reserves all rights not expressly granted.
4 | *
5 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
6 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
7 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
8 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
9 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
10 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
11 | */
12 |
13 | import { GraphQLNonNull, GraphQLString } from 'graphql';
14 | import { cursorForObjectInConnection, mutationWithClientMutationId } from 'graphql-relay';
15 |
16 | import { addTodo, getTodo, getTodos } from '../../../data/Todo';
17 | import { GraphQLTodoEdge } from '../';
18 | import GraphQLViewer, { viewer } from '../../Viewer';
19 |
20 | export default mutationWithClientMutationId({
21 | name: 'AddTodo',
22 | inputFields: () => ({
23 | text: { type: new GraphQLNonNull(GraphQLString) },
24 | }),
25 | outputFields: () => ({
26 | todoEdge: {
27 | type: GraphQLTodoEdge,
28 | resolve: ({ localTodoId }) => {
29 | const todo = getTodo(localTodoId);
30 | return {
31 | cursor: cursorForObjectInConnection(getTodos(), todo),
32 | node: todo,
33 | };
34 | },
35 | },
36 | viewer: {
37 | type: GraphQLViewer,
38 | resolve: () => viewer,
39 | },
40 | }),
41 | mutateAndGetPayload: ({ text }) => ({ localTodoId: addTodo(text) }),
42 | });
43 |
--------------------------------------------------------------------------------
/backend/src/graphql/Todo/mutations/ChangeTodoStatusMutation.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This file provided by Facebook is for non-commercial testing and evaluation
3 | * purposes only. Facebook reserves all rights not expressly granted.
4 | *
5 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
6 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
7 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
8 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
9 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
10 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
11 | */
12 |
13 | import { GraphQLBoolean, GraphQLID, GraphQLNonNull } from 'graphql';
14 | import { fromGlobalId, mutationWithClientMutationId } from 'graphql-relay';
15 |
16 | import { changeTodoStatus, getTodo } from '../../../data/Todo';
17 | import GraphQLTodo from '../';
18 | import GraphQLViewer, { viewer } from '../../Viewer';
19 |
20 | export default mutationWithClientMutationId({
21 | name: 'ChangeTodoStatus',
22 | inputFields: () => ({
23 | complete: { type: new GraphQLNonNull(GraphQLBoolean) },
24 | id: { type: new GraphQLNonNull(GraphQLID) },
25 | }),
26 | outputFields: () => ({
27 | todo: {
28 | type: GraphQLTodo,
29 | resolve: ({ localTodoId }) => getTodo(localTodoId),
30 | },
31 | viewer: {
32 | type: GraphQLViewer,
33 | resolve: () => viewer,
34 | },
35 | }),
36 |
37 | mutateAndGetPayload: ({ id, complete }) => {
38 | const localTodoId = fromGlobalId(id).id;
39 | changeTodoStatus(localTodoId, complete);
40 | return { localTodoId };
41 | },
42 | });
43 |
--------------------------------------------------------------------------------
/backend/src/graphql/Todo/mutations/MarkAllTodosMutation.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This file provided by Facebook is for non-commercial testing and evaluation
3 | * purposes only. Facebook reserves all rights not expressly granted.
4 | *
5 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
6 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
7 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
8 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
9 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
10 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
11 | */
12 |
13 | import { GraphQLBoolean, GraphQLList, GraphQLNonNull } from 'graphql';
14 | import { mutationWithClientMutationId } from 'graphql-relay';
15 |
16 | import { getTodo, markAllTodos } from '../../../data/Todo';
17 | import GraphQLTodo from '../';
18 | import GraphQLViewer, { viewer } from '../../Viewer';
19 |
20 | export default mutationWithClientMutationId({
21 | name: 'MarkAllTodos',
22 | inputFields: () => ({
23 | complete: { type: new GraphQLNonNull(GraphQLBoolean) },
24 | }),
25 | outputFields: () => ({
26 | changedTodos: {
27 | type: new GraphQLList(GraphQLTodo),
28 | resolve: ({ changedTodoLocalIds }) => changedTodoLocalIds.map(getTodo),
29 | },
30 | viewer: {
31 | type: GraphQLViewer,
32 | resolve: () => viewer,
33 | },
34 | }),
35 |
36 | mutateAndGetPayload: ({ complete }) => {
37 | const changedTodoLocalIds = markAllTodos(complete);
38 | return { changedTodoLocalIds };
39 | },
40 | });
41 |
--------------------------------------------------------------------------------
/backend/src/graphql/Todo/mutations/RemoveCompletedTodosMutation.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This file provided by Facebook is for non-commercial testing and evaluation
3 | * purposes only. Facebook reserves all rights not expressly granted.
4 | *
5 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
6 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
7 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
8 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
9 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
10 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
11 | */
12 |
13 | import { GraphQLList, GraphQLString } from 'graphql';
14 | import { mutationWithClientMutationId, toGlobalId } from 'graphql-relay';
15 |
16 | import { removeCompletedTodos } from '../../../data/Todo';
17 | import GraphQLViewer, { viewer } from '../../Viewer';
18 |
19 | // TODO: Support plural deletes
20 | export default mutationWithClientMutationId({
21 | name: 'RemoveCompletedTodos',
22 | outputFields: () => ({
23 | deletedTodoIds: {
24 | type: new GraphQLList(GraphQLString),
25 | resolve: ({ deletedTodoIds }) => deletedTodoIds,
26 | },
27 | viewer: {
28 | type: GraphQLViewer,
29 | resolve: () => viewer,
30 | },
31 | }),
32 |
33 | mutateAndGetPayload: () => {
34 | const deletedTodoLocalIds = removeCompletedTodos();
35 | const deletedTodoIds = deletedTodoLocalIds.map(toGlobalId.bind(null, 'Todo'));
36 | return { deletedTodoIds };
37 | },
38 | });
39 |
--------------------------------------------------------------------------------
/backend/src/graphql/Todo/mutations/RemoveTodoMutation.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This file provided by Facebook is for non-commercial testing and evaluation
3 | * purposes only. Facebook reserves all rights not expressly granted.
4 | *
5 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
6 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
7 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
8 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
9 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
10 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
11 | */
12 |
13 | import { GraphQLID, GraphQLNonNull } from 'graphql';
14 | import { fromGlobalId, mutationWithClientMutationId } from 'graphql-relay';
15 |
16 | import { removeTodo } from '../../../data/Todo';
17 | import GraphQLViewer, { viewer } from '../../Viewer';
18 |
19 | export default mutationWithClientMutationId({
20 | name: 'RemoveTodo',
21 | inputFields: () => ({
22 | id: { type: new GraphQLNonNull(GraphQLID) },
23 | }),
24 | outputFields: () => ({
25 | deletedTodoId: {
26 | type: GraphQLID,
27 | resolve: ({ id }) => id,
28 | },
29 | viewer: {
30 | type: GraphQLViewer,
31 | resolve: () => viewer,
32 | },
33 | }),
34 |
35 | mutateAndGetPayload: ({ id }) => {
36 | const localTodoId = fromGlobalId(id).id;
37 | removeTodo(localTodoId);
38 | return { id };
39 | },
40 | });
41 |
--------------------------------------------------------------------------------
/backend/src/graphql/Todo/mutations/RenameTodoMutation.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This file provided by Facebook is for non-commercial testing and evaluation
3 | * purposes only. Facebook reserves all rights not expressly granted.
4 | *
5 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
6 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
7 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
8 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
9 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
10 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
11 | */
12 |
13 | import { GraphQLID, GraphQLNonNull, GraphQLString } from 'graphql';
14 | import { fromGlobalId, mutationWithClientMutationId } from 'graphql-relay';
15 |
16 | import { getTodo, renameTodo } from '../../../data/Todo';
17 | import GraphQLTodo from '../';
18 |
19 | export default mutationWithClientMutationId({
20 | name: 'RenameTodo',
21 | inputFields: () => ({
22 | id: { type: new GraphQLNonNull(GraphQLID) },
23 | text: { type: new GraphQLNonNull(GraphQLString) },
24 | }),
25 | outputFields: () => ({
26 | todo: {
27 | type: GraphQLTodo,
28 | resolve: ({ localTodoId }) => getTodo(localTodoId),
29 | },
30 | }),
31 |
32 | mutateAndGetPayload: ({ id, text }) => {
33 | const localTodoId = fromGlobalId(id).id;
34 | renameTodo(localTodoId, text);
35 | return { localTodoId };
36 | },
37 | });
38 |
--------------------------------------------------------------------------------
/backend/src/graphql/Viewer.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This file provided by Facebook is for non-commercial testing and evaluation
3 | * purposes only. Facebook reserves all rights not expressly granted.
4 | *
5 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
6 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
7 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
8 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
9 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
10 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
11 | */
12 |
13 | import { GraphQLInt, GraphQLObjectType, GraphQLString } from 'graphql';
14 | import { connectionArgs, connectionFromArray, globalIdField } from 'graphql-relay';
15 |
16 | import { getTodos } from '../data/Todo';
17 | import { nodeInterface, registerNodeFetcher } from './node';
18 | import { GraphQLTodoConnection } from './Todo';
19 |
20 | const VIEWER_TYPE_NAME = 'Viewer';
21 |
22 | export const viewer = {};
23 |
24 | registerNodeFetcher(VIEWER_TYPE_NAME, () => viewer);
25 |
26 | export default new GraphQLObjectType({
27 | name: VIEWER_TYPE_NAME,
28 | fields: () => ({
29 | id: globalIdField(),
30 | todos: {
31 | type: GraphQLTodoConnection,
32 | args: {
33 | status: {
34 | type: GraphQLString,
35 | defaultValue: 'any',
36 | },
37 | ...connectionArgs,
38 | },
39 | resolve: (obj, { status, ...args }) => connectionFromArray(getTodos(status), args),
40 | },
41 | totalCount: {
42 | type: GraphQLInt,
43 | resolve: () => getTodos().length,
44 | },
45 | completedCount: {
46 | type: GraphQLInt,
47 | resolve: () => getTodos('completed').length,
48 | },
49 | }),
50 | interfaces: () => [nodeInterface],
51 | isTypeOf: obj => (obj === viewer),
52 | });
53 |
--------------------------------------------------------------------------------
/backend/src/graphql/node.js:
--------------------------------------------------------------------------------
1 | import { fromGlobalId, nodeDefinitions } from 'graphql-relay';
2 |
3 | const nodeFetchers = new Map;
4 |
5 | export function registerNodeFetcher(type, fetcher) {
6 | if (nodeFetchers.has(type)) {
7 | throw new Error(`Node fetcher for type "${type}" have already been registered.`);
8 | }
9 |
10 | nodeFetchers.set(type, fetcher);
11 | }
12 |
13 | const { nodeInterface, nodeField } = nodeDefinitions(
14 | (globalId, info) => {
15 | const { type, id } = fromGlobalId(globalId);
16 |
17 | const fetcher = nodeFetchers.get(type);
18 | if (!fetcher) {
19 | throw new Error(`Node fetcher for type "${type}" is not registered.`);
20 | }
21 |
22 | return fetcher(id, info);
23 | }
24 | );
25 |
26 | export { nodeInterface, nodeField };
27 |
--------------------------------------------------------------------------------
/backend/src/graphql/schema.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This file provided by Facebook is for non-commercial testing and evaluation
3 | * purposes only. Facebook reserves all rights not expressly granted.
4 | *
5 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
6 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
7 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
8 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
9 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
10 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
11 | */
12 |
13 | import { GraphQLObjectType, GraphQLSchema } from 'graphql';
14 |
15 | import { nodeField } from './node';
16 | import GraphQLAddTodoMutation from './Todo/mutations/AddTodoMutation';
17 | import GraphQLChangeTodoStatusMutation from './Todo/mutations/ChangeTodoStatusMutation';
18 | import GraphQLMarkAllTodosMutation from './Todo/mutations/MarkAllTodosMutation';
19 | import GraphQLRemoveCompletedTodosMutation from './Todo/mutations/RemoveCompletedTodosMutation';
20 | import GraphQLRemoveTodoMutation from './Todo/mutations/RemoveTodoMutation';
21 | import GraphQLRenameTodoMutation from './Todo/mutations/RenameTodoMutation';
22 | import GraphQLViewer, { viewer } from './Viewer';
23 |
24 | const Query = new GraphQLObjectType({
25 | name: 'Query',
26 | fields: () => ({
27 | viewer: {
28 | type: GraphQLViewer,
29 | resolve: () => viewer,
30 | },
31 | node: nodeField,
32 | }),
33 | });
34 |
35 | const Mutation = new GraphQLObjectType({
36 | name: 'Mutation',
37 | fields: {
38 | addTodo: GraphQLAddTodoMutation,
39 | changeTodoStatus: GraphQLChangeTodoStatusMutation,
40 | markAllTodos: GraphQLMarkAllTodosMutation,
41 | removeCompletedTodos: GraphQLRemoveCompletedTodosMutation,
42 | removeTodo: GraphQLRemoveTodoMutation,
43 | renameTodo: GraphQLRenameTodoMutation,
44 | },
45 | });
46 |
47 | export default new GraphQLSchema({
48 | query: Query,
49 | mutation: Mutation,
50 | });
51 |
--------------------------------------------------------------------------------
/backend/src/index.js:
--------------------------------------------------------------------------------
1 | import 'babel-polyfill';
2 | import express from 'express';
3 |
4 | import config from '../config.json';
5 | import errorHandler from './middlewares/errorHandler';
6 | import logger from './utils/logger';
7 | import graphql from './middlewares/graphql';
8 |
9 | const app = express();
10 |
11 | app.use('/', graphql);
12 | app.use(errorHandler);
13 |
14 | const server = app.listen(config.port, err => {
15 | if (err) {
16 | logger.error(err);
17 | } else {
18 | const { address, port } = server.address();
19 | logger.info(`Backend is listening at http://${address}:${port}`);
20 |
21 | if (process.send) {
22 | process.send('ready');
23 | }
24 | }
25 | });
26 |
--------------------------------------------------------------------------------
/backend/src/middlewares/errorHandler.js:
--------------------------------------------------------------------------------
1 | import logger from '../utils/logger';
2 |
3 | export default function errorHandler(err, {}, res, {}) {
4 | logger.error(err);
5 |
6 | if (res.headersSent) {
7 | res.end();
8 | } else {
9 | res.send(500, 'Internal server error.');
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/backend/src/middlewares/graphql.js:
--------------------------------------------------------------------------------
1 | import graphQLHTTP from 'express-graphql';
2 |
3 | import logger from '../utils/logger';
4 | import schema from '../graphql/schema';
5 |
6 | export default graphQLHTTP({
7 | schema,
8 | graphiql: process.env.NODE_ENV !== 'production',
9 | pretty: process.env.NODE_ENV !== 'production',
10 |
11 | formatError: error => {
12 | logger.error(error);
13 | return { message: 'Internal server error.' };
14 | },
15 | });
16 |
--------------------------------------------------------------------------------
/backend/src/utils/logger.js:
--------------------------------------------------------------------------------
1 | import winston from 'winston';
2 |
3 | export default new winston.Logger({
4 | exitOnError: false,
5 | transports: [
6 | new winston.transports.Console({
7 | handleExceptions: true,
8 | humanReadableUnhandledException: true,
9 | }),
10 | ],
11 | });
12 |
--------------------------------------------------------------------------------
/frontend/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "passPerPreset": true,
3 | "presets": [
4 | {
5 | "plugins": [
6 | "./scripts/babelRelayPlugin"
7 | ]
8 | },
9 | "react",
10 | "es2015",
11 | "stage-0"
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/frontend/.gitignore:
--------------------------------------------------------------------------------
1 | assets/
2 | assets.json
3 | lib/
4 | npm-debug.log
5 | node_modules/
6 |
--------------------------------------------------------------------------------
/frontend/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "graphQLAddress": "http://localhost:8080/graphql",
3 | "port": 8888
4 | }
5 |
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "devDependencies": {
4 | "autoprefixer": "^6.3.3",
5 | "babel-core": "^6.6.4",
6 | "babel-loader": "^6.2.4",
7 | "babel-preset-react": "^6.5.0",
8 | "babel-relay-plugin": "^0.7.2",
9 | "css-loader": "^0.23.1",
10 | "extract-text-webpack-plugin": "^1.0.1",
11 | "file-loader": "^0.8.5",
12 | "json-loader": "^0.5.4",
13 | "postcss-loader": "^0.8.1",
14 | "postcss-nested": "^1.0.0",
15 | "style-loader": "^0.13.0",
16 | "url-loader": "^0.5.7",
17 | "webpack": "^1.12.14",
18 | "webpack-dev-middleware": "^1.5.1",
19 | "webpack-hot-middleware": "^2.10.0"
20 | },
21 | "dependencies": {
22 | "classnames": "^2.2.3",
23 | "isomorphic-relay": "^0.5.4",
24 | "isomorphic-relay-router": "^0.6.2",
25 | "react": "^0.14.7",
26 | "react-dom": "^0.14.7",
27 | "react-relay": "^0.7.2",
28 | "react-router": "^2.0.0"
29 | },
30 | "scripts": {
31 | "build": "NODE_ENV=production npm run build-client && npm run build-server",
32 | "build-dev": "NODE_ENV=development npm run build-server",
33 | "build-client": "NODE_ENV=production webpack --config webpack-client.config.js",
34 | "build-server": "babel src/server -d lib/server && webpack --config webpack-server.config.js",
35 | "postinstall": "npm run build",
36 | "start": "NODE_ENV=production node lib/server",
37 | "start-dev": "node lib/server"
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/frontend/scripts/babelRelayPlugin.js:
--------------------------------------------------------------------------------
1 | const getBabelRelayPlugin = require('babel-relay-plugin');
2 | const schema = require('../../backend/schema.json');
3 |
4 | module.exports = getBabelRelayPlugin(schema.data);
5 |
--------------------------------------------------------------------------------
/frontend/src/app.js:
--------------------------------------------------------------------------------
1 | import history from 'shared/utils/history';
2 | import IsomorphicRelay from 'isomorphic-relay';
3 | import IsomorphicRouter from 'isomorphic-relay-router';
4 | import React from 'react';
5 | import ReactDOM from 'react-dom';
6 |
7 | import './styles/app.css';
8 |
9 | const data = JSON.parse(document.getElementById('preloadedData').textContent);
10 |
11 | IsomorphicRelay.injectPreparedData(data);
12 |
13 | const rootElement = document.getElementById('root');
14 |
15 | function render() {
16 | const routes = require('./routes').default;
17 |
18 | ReactDOM.render(
19 | ,
20 | rootElement
21 | );
22 | }
23 |
24 | render();
25 |
26 | if (module.hot) {
27 | module.hot.accept('./routes', () => {
28 | setTimeout(() => {
29 | ReactDOM.unmountComponentAtNode(rootElement);
30 | render();
31 | });
32 | });
33 | }
34 |
--------------------------------------------------------------------------------
/frontend/src/routes.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { createRoutes, IndexRoute, Route } from 'react-router';
3 | import Relay from 'react-relay';
4 |
5 | import TodoApp from './screens/TodoApp';
6 | import TodoList from './screens/TodoApp/screens/TodoList';
7 |
8 | const prepareParams = ({ status }) => ({
9 | status: ['active', 'completed'].includes(status) ? status : 'any',
10 | });
11 |
12 | const queries = {
13 | viewer: () => Relay.QL`query { viewer }`,
14 | };
15 |
16 | export default createRoutes(
17 |
18 |
19 |
20 |
21 | );
22 |
--------------------------------------------------------------------------------
/frontend/src/screens/TodoApp/components/TodoListFooter.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This file provided by Facebook is for non-commercial testing and evaluation
3 | * purposes only. Facebook reserves all rights not expressly granted.
4 | *
5 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
6 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
7 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
8 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
9 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
10 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
11 | */
12 |
13 | import React from 'react';
14 | import Relay from 'react-relay';
15 |
16 | import RemoveCompletedTodosMutation from '../mutations/RemoveCompletedTodosMutation';
17 | import TodoListFooterView from '../views/TodoListFooterView';
18 |
19 | class TodoListFooter extends React.Component {
20 | static propTypes = {
21 | viewer: React.PropTypes.shape({
22 | completedCount: React.PropTypes.number.isRequired,
23 | totalCount: React.PropTypes.number.isRequired,
24 | }).isRequired,
25 | };
26 |
27 | _handleRemoveCompletedTodosClick = () => {
28 | Relay.Store.commitUpdate(new RemoveCompletedTodosMutation({ viewer: this.props.viewer }));
29 | };
30 |
31 | render() {
32 | const { completedCount, totalCount } = this.props.viewer;
33 | return (
34 |
39 | );
40 | }
41 | }
42 |
43 | export default Relay.createContainer(TodoListFooter, {
44 | fragments: {
45 | viewer: () => Relay.QL`
46 | fragment on Viewer {
47 | completedCount,
48 | totalCount,
49 | ${RemoveCompletedTodosMutation.getFragment('viewer')},
50 | }
51 | `,
52 | },
53 | });
54 |
--------------------------------------------------------------------------------
/frontend/src/screens/TodoApp/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This file provided by Facebook is for non-commercial testing and evaluation
3 | * purposes only. Facebook reserves all rights not expressly granted.
4 | *
5 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
6 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
7 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
8 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
9 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
10 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
11 | */
12 |
13 | import React from 'react';
14 | import Relay from 'react-relay';
15 |
16 | import AddTodoMutation from './mutations/AddTodoMutation';
17 | import TodoAppView from './views/TodoAppView';
18 | import TodoListFooter from './components/TodoListFooter';
19 |
20 | class TodoApp extends React.Component {
21 | static propTypes = {
22 | children: React.PropTypes.node,
23 | viewer: React.PropTypes.shape({
24 | totalCount: React.PropTypes.number.isRequired,
25 | }).isRequired,
26 | };
27 |
28 | _handleTextInputSave = text => {
29 | Relay.Store.commitUpdate(
30 | new AddTodoMutation({ text, viewer: this.props.viewer })
31 | );
32 | };
33 |
34 | render() {
35 | return (
36 |
37 | {this.props.children}
38 | {this.props.viewer.totalCount > 0 && }
39 |
40 | );
41 | }
42 | }
43 |
44 | export default Relay.createContainer(TodoApp, {
45 | fragments: {
46 | viewer: () => Relay.QL`
47 | fragment on Viewer {
48 | totalCount,
49 | ${AddTodoMutation.getFragment('viewer')},
50 | ${TodoListFooter.getFragment('viewer')},
51 | }
52 | `,
53 | },
54 | });
55 |
--------------------------------------------------------------------------------
/frontend/src/screens/TodoApp/mutations/AddTodoMutation.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This file provided by Facebook is for non-commercial testing and evaluation
3 | * purposes only. Facebook reserves all rights not expressly granted.
4 | *
5 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
6 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
7 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
8 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
9 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
10 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
11 | */
12 |
13 | import Relay from 'react-relay';
14 |
15 | export default class AddTodoMutation extends Relay.Mutation {
16 | static fragments = {
17 | viewer: () => Relay.QL`
18 | fragment on Viewer {
19 | id,
20 | totalCount,
21 | }
22 | `,
23 | };
24 |
25 | getMutation() {
26 | return Relay.QL`mutation{addTodo}`;
27 | }
28 |
29 | getFatQuery() {
30 | return Relay.QL`
31 | fragment on AddTodoPayload @relay(pattern: true) {
32 | todoEdge,
33 | viewer {
34 | todos,
35 | totalCount,
36 | },
37 | }
38 | `;
39 | }
40 |
41 | getConfigs() {
42 | return [{
43 | type: 'RANGE_ADD',
44 | parentName: 'viewer',
45 | parentID: this.props.viewer.id,
46 | connectionName: 'todos',
47 | edgeName: 'todoEdge',
48 | rangeBehaviors: {
49 | '': 'append',
50 | 'status(any)': 'append',
51 | 'status(active)': 'append',
52 | 'status(completed)': null,
53 | },
54 | }];
55 | }
56 |
57 | getVariables() {
58 | return {
59 | text: this.props.text,
60 | };
61 | }
62 |
63 | getOptimisticResponse() {
64 | return {
65 | // FIXME: totalCount gets updated optimistically, but this edge does not
66 | // get added until the server responds
67 | todoEdge: {
68 | node: {
69 | complete: false,
70 | text: this.props.text,
71 | },
72 | },
73 | viewer: {
74 | id: this.props.viewer.id,
75 | totalCount: this.props.viewer.totalCount + 1,
76 | },
77 | };
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/frontend/src/screens/TodoApp/mutations/RemoveCompletedTodosMutation.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This file provided by Facebook is for non-commercial testing and evaluation
3 | * purposes only. Facebook reserves all rights not expressly granted.
4 | *
5 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
6 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
7 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
8 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
9 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
10 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
11 | */
12 |
13 | import Relay from 'react-relay';
14 |
15 | export default class RemoveCompletedTodosMutation extends Relay.Mutation {
16 | static fragments = {
17 | // TODO: Make completedCount, edges, and totalCount optional
18 | viewer: () => Relay.QL`
19 | fragment on Viewer {
20 | completedCount,
21 | id,
22 | todos(status: "completed", first: 2147483647) {
23 | edges {
24 | node {
25 | complete,
26 | id,
27 | },
28 | },
29 | },
30 | totalCount,
31 | }
32 | `,
33 | };
34 |
35 | getMutation() {
36 | return Relay.QL`mutation{removeCompletedTodos}`;
37 | }
38 |
39 | getFatQuery() {
40 | return Relay.QL`
41 | fragment on RemoveCompletedTodosPayload @relay(pattern: true) {
42 | deletedTodoIds,
43 | viewer {
44 | completedCount,
45 | totalCount,
46 | },
47 | }
48 | `;
49 | }
50 |
51 | getConfigs() {
52 | return [{
53 | type: 'NODE_DELETE',
54 | parentName: 'viewer',
55 | parentID: this.props.viewer.id,
56 | connectionName: 'todos',
57 | deletedIDFieldName: 'deletedTodoIds',
58 | }];
59 | }
60 |
61 | getVariables() {
62 | return {};
63 | }
64 |
65 | getOptimisticResponse() {
66 | const { todos } = this.props.viewer;
67 | let deletedTodoIds;
68 | if (todos && todos.edges) {
69 | deletedTodoIds = todos.edges.filter(edge => edge.node.complete).map(edge => edge.node.id);
70 | }
71 |
72 | const { completedCount, totalCount } = this.props.viewer;
73 | let newTotalCount;
74 | if (completedCount !== null && totalCount !== null) {
75 | newTotalCount = totalCount - completedCount;
76 | }
77 |
78 | return {
79 | deletedTodoIds,
80 | viewer: {
81 | completedCount: 0,
82 | id: this.props.viewer.id,
83 | totalCount: newTotalCount,
84 | },
85 | };
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/frontend/src/screens/TodoApp/screens/TodoList/components/Todo.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This file provided by Facebook is for non-commercial testing and evaluation
3 | * purposes only. Facebook reserves all rights not expressly granted.
4 | *
5 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
6 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
7 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
8 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
9 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
10 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
11 | */
12 |
13 | import React from 'react';
14 | import Relay from 'react-relay';
15 |
16 | import ChangeTodoStatusMutation from '../mutations/ChangeTodoStatusMutation';
17 | import RemoveTodoMutation from '../mutations/RemoveTodoMutation';
18 | import RenameTodoMutation from '../mutations/RenameTodoMutation';
19 | import TodoView from '../views/TodoView';
20 |
21 | class Todo extends React.Component {
22 | static propTypes = {
23 | todo: React.PropTypes.shape({
24 | complete: React.PropTypes.bool.isRequired,
25 | id: React.PropTypes.string.isRequired,
26 | text: React.PropTypes.string.isRequired,
27 | }).isRequired,
28 | viewer: React.PropTypes.object.isRequired,
29 | };
30 | state = {
31 | isEditing: false,
32 | };
33 |
34 | _handleCompleteChange = event => {
35 | Relay.Store.commitUpdate(
36 | new ChangeTodoStatusMutation({
37 | complete: event.target.checked,
38 | todo: this.props.todo,
39 | viewer: this.props.viewer,
40 | })
41 | );
42 | };
43 |
44 | _handleDestroyClick = () => {
45 | this._removeTodo();
46 | };
47 |
48 | _handleLabelDoubleClick = () => {
49 | this._setEditMode(true);
50 | };
51 |
52 | _handleTextInputCancel = () => {
53 | this._setEditMode(false);
54 | };
55 |
56 | _handleTextInputDelete = () => {
57 | this._setEditMode(false);
58 | this._removeTodo();
59 | };
60 |
61 | _handleTextInputSave = text => {
62 | this._setEditMode(false);
63 | Relay.Store.commitUpdate(
64 | new RenameTodoMutation({ todo: this.props.todo, text })
65 | );
66 | };
67 |
68 | _removeTodo() {
69 | Relay.Store.commitUpdate(
70 | new RemoveTodoMutation({ todo: this.props.todo, viewer: this.props.viewer })
71 | );
72 | }
73 |
74 | _setEditMode = shouldEdit => {
75 | this.setState({ isEditing: shouldEdit });
76 | };
77 |
78 | render() {
79 | return (
80 |
90 | );
91 | }
92 | }
93 |
94 | export default Relay.createContainer(Todo, {
95 | fragments: {
96 | todo: () => Relay.QL`
97 | fragment on Todo {
98 | complete,
99 | id,
100 | text,
101 | ${ChangeTodoStatusMutation.getFragment('todo')},
102 | ${RemoveTodoMutation.getFragment('todo')},
103 | ${RenameTodoMutation.getFragment('todo')},
104 | }
105 | `,
106 | viewer: () => Relay.QL`
107 | fragment on Viewer {
108 | ${ChangeTodoStatusMutation.getFragment('viewer')},
109 | ${RemoveTodoMutation.getFragment('viewer')},
110 | }
111 | `,
112 | },
113 | });
114 |
--------------------------------------------------------------------------------
/frontend/src/screens/TodoApp/screens/TodoList/images/checked.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/screens/TodoApp/screens/TodoList/images/unchecked.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/screens/TodoApp/screens/TodoList/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This file provided by Facebook is for non-commercial testing and evaluation
3 | * purposes only. Facebook reserves all rights not expressly granted.
4 | *
5 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
6 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
7 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
8 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
9 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
10 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
11 | */
12 |
13 | import React from 'react';
14 | import Relay from 'react-relay';
15 |
16 | import MarkAllTodosMutation from './mutations/MarkAllTodosMutation';
17 | import Todo from './components/Todo';
18 | import TodoListView from './views/TodoListView';
19 |
20 | class TodoList extends React.Component {
21 | static propTypes = {
22 | viewer: React.PropTypes.shape({
23 | completedCount: React.PropTypes.number.isRequired,
24 | todos: React.PropTypes.shape({
25 | edges: React.PropTypes.arrayOf(
26 | React.PropTypes.shape({
27 | node: React.PropTypes.shape({
28 | id: React.PropTypes.string.isRequired,
29 | }).isRequired,
30 | }).isRequired
31 | ).isRequired,
32 | }).isRequired,
33 | totalCount: React.PropTypes.number.isRequired,
34 | }),
35 | };
36 |
37 | _handleMarkAllChange = event => {
38 | Relay.Store.commitUpdate(
39 | new MarkAllTodosMutation({
40 | complete: event.target.checked,
41 | todos: this.props.viewer.todos,
42 | viewer: this.props.viewer,
43 | })
44 | );
45 | };
46 |
47 | render() {
48 | const { viewer } = this.props;
49 | return (
50 |
55 | {viewer.todos.edges.map(edge => edge.node).map(todo =>
56 |
57 | )}
58 |
59 | );
60 | }
61 | }
62 |
63 | export default Relay.createContainer(TodoList, {
64 | initialVariables: {
65 | status: null,
66 | },
67 | fragments: {
68 | viewer: () => Relay.QL`
69 | fragment on Viewer {
70 | completedCount,
71 | todos(status: $status, first: 2147483647) {
72 | edges {
73 | node {
74 | id,
75 | ${Todo.getFragment('todo')},
76 | },
77 | },
78 | ${MarkAllTodosMutation.getFragment('todos')},
79 | },
80 | totalCount,
81 | ${MarkAllTodosMutation.getFragment('viewer')},
82 | ${Todo.getFragment('viewer')},
83 | }
84 | `,
85 | },
86 | });
87 |
--------------------------------------------------------------------------------
/frontend/src/screens/TodoApp/screens/TodoList/mutations/ChangeTodoStatusMutation.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This file provided by Facebook is for non-commercial testing and evaluation
3 | * purposes only. Facebook reserves all rights not expressly granted.
4 | *
5 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
6 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
7 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
8 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
9 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
10 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
11 | */
12 |
13 | import Relay from 'react-relay';
14 |
15 | export default class ChangeTodoStatusMutation extends Relay.Mutation {
16 | static fragments = {
17 | todo: () => Relay.QL`
18 | fragment on Todo {
19 | id,
20 | }
21 | `,
22 | // TODO: Mark completedCount optional
23 | viewer: () => Relay.QL`
24 | fragment on Viewer {
25 | id,
26 | completedCount,
27 | }
28 | `,
29 | };
30 |
31 | getMutation() {
32 | return Relay.QL`mutation{changeTodoStatus}`;
33 | }
34 |
35 | getFatQuery() {
36 | return Relay.QL`
37 | fragment on ChangeTodoStatusPayload @relay(pattern: true) {
38 | todo {
39 | complete,
40 | },
41 | viewer {
42 | completedCount,
43 | todos,
44 | },
45 | }
46 | `;
47 | }
48 |
49 | getConfigs() {
50 | return [{
51 | type: 'FIELDS_CHANGE',
52 | fieldIDs: {
53 | todo: this.props.todo.id,
54 | viewer: this.props.viewer.id,
55 | },
56 | }];
57 | }
58 |
59 | getVariables() {
60 | return {
61 | complete: this.props.complete,
62 | id: this.props.todo.id,
63 | };
64 | }
65 |
66 | getOptimisticResponse() {
67 | const viewerPayload = { id: this.props.viewer.id };
68 | if (this.props.viewer.completedCount !== null) {
69 | viewerPayload.completedCount = this.props.complete ?
70 | this.props.viewer.completedCount + 1 :
71 | this.props.viewer.completedCount - 1;
72 | }
73 |
74 | return {
75 | todo: {
76 | complete: this.props.complete,
77 | id: this.props.todo.id,
78 | },
79 | viewer: viewerPayload,
80 | };
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/frontend/src/screens/TodoApp/screens/TodoList/mutations/MarkAllTodosMutation.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This file provided by Facebook is for non-commercial testing and evaluation
3 | * purposes only. Facebook reserves all rights not expressly granted.
4 | *
5 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
6 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
7 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
8 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
9 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
10 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
11 | */
12 |
13 | import Relay from 'react-relay';
14 |
15 | export default class MarkAllTodosMutation extends Relay.Mutation {
16 | static fragments = {
17 | // TODO: Mark edges and totalCount optional
18 | todos: () => Relay.QL`
19 | fragment on TodoConnection {
20 | edges {
21 | node {
22 | complete,
23 | id,
24 | },
25 | },
26 | }
27 | `,
28 | viewer: () => Relay.QL`
29 | fragment on Viewer {
30 | id,
31 | totalCount,
32 | }
33 | `,
34 | };
35 |
36 | getMutation() {
37 | return Relay.QL`mutation{markAllTodos}`;
38 | }
39 |
40 | getFatQuery() {
41 | return Relay.QL`
42 | fragment on MarkAllTodosPayload @relay(pattern: true) {
43 | viewer {
44 | completedCount,
45 | todos,
46 | },
47 | }
48 | `;
49 | }
50 |
51 | getConfigs() {
52 | return [{
53 | type: 'FIELDS_CHANGE',
54 | fieldIDs: {
55 | viewer: this.props.viewer.id,
56 | },
57 | }];
58 | }
59 |
60 | getVariables() {
61 | return {
62 | complete: this.props.complete,
63 | };
64 | }
65 |
66 | getOptimisticResponse() {
67 | const viewerPayload = { id: this.props.viewer.id };
68 | if (this.props.todos && this.props.todos.edges) {
69 | viewerPayload.todos = {
70 | edges: this.props.todos.edges
71 | .filter(edge => edge.node.complete !== this.props.complete)
72 | .map(edge => ({
73 | node: {
74 | complete: this.props.complete,
75 | id: edge.node.id,
76 | },
77 | })),
78 | };
79 | }
80 |
81 | if (this.props.viewer.totalCount !== null) {
82 | viewerPayload.completedCount = this.props.complete ?
83 | this.props.viewer.totalCount :
84 | 0;
85 | }
86 |
87 | return {
88 | viewer: viewerPayload,
89 | };
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/frontend/src/screens/TodoApp/screens/TodoList/mutations/RemoveTodoMutation.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This file provided by Facebook is for non-commercial testing and evaluation
3 | * purposes only. Facebook reserves all rights not expressly granted.
4 | *
5 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
6 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
7 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
8 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
9 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
10 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
11 | */
12 |
13 | import Relay from 'react-relay';
14 |
15 | export default class RemoveTodoMutation extends Relay.Mutation {
16 | static fragments = {
17 | // TODO: Mark complete as optional
18 | todo: () => Relay.QL`
19 | fragment on Todo {
20 | complete,
21 | id,
22 | }
23 | `,
24 | // TODO: Mark completedCount and totalCount as optional
25 | viewer: () => Relay.QL`
26 | fragment on Viewer {
27 | completedCount,
28 | id,
29 | totalCount,
30 | }
31 | `,
32 | };
33 |
34 | getMutation() {
35 | return Relay.QL`mutation{removeTodo}`;
36 | }
37 |
38 | getFatQuery() {
39 | return Relay.QL`
40 | fragment on RemoveTodoPayload @relay(pattern: true) {
41 | deletedTodoId,
42 | viewer {
43 | completedCount,
44 | totalCount,
45 | },
46 | }
47 | `;
48 | }
49 |
50 | getConfigs() {
51 | return [{
52 | type: 'NODE_DELETE',
53 | parentName: 'viewer',
54 | parentID: this.props.viewer.id,
55 | connectionName: 'todos',
56 | deletedIDFieldName: 'deletedTodoId',
57 | }];
58 | }
59 |
60 | getVariables() {
61 | return {
62 | id: this.props.todo.id,
63 | };
64 | }
65 |
66 | getOptimisticResponse() {
67 | const viewerPayload = { id: this.props.viewer.id };
68 | if (this.props.viewer.completedCount !== null) {
69 | viewerPayload.completedCount = this.props.todo.complete === true ?
70 | this.props.viewer.completedCount - 1 :
71 | this.props.viewer.completedCount;
72 | }
73 |
74 | if (this.props.viewer.totalCount !== null) {
75 | viewerPayload.totalCount = this.props.viewer.totalCount - 1;
76 | }
77 |
78 | return {
79 | deletedTodoId: this.props.todo.id,
80 | viewer: viewerPayload,
81 | };
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/frontend/src/screens/TodoApp/screens/TodoList/mutations/RenameTodoMutation.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This file provided by Facebook is for non-commercial testing and evaluation
3 | * purposes only. Facebook reserves all rights not expressly granted.
4 | *
5 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
6 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
7 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
8 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
9 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
10 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
11 | */
12 |
13 | import Relay from 'react-relay';
14 |
15 | export default class RenameTodoMutation extends Relay.Mutation {
16 | static fragments = {
17 | todo: () => Relay.QL`
18 | fragment on Todo {
19 | id,
20 | }
21 | `,
22 | };
23 |
24 | getMutation() {
25 | return Relay.QL`mutation{renameTodo}`;
26 | }
27 |
28 | getFatQuery() {
29 | return Relay.QL`
30 | fragment on RenameTodoPayload @relay(pattern: true) {
31 | todo {
32 | text,
33 | }
34 | }
35 | `;
36 | }
37 |
38 | getConfigs() {
39 | return [{
40 | type: 'FIELDS_CHANGE',
41 | fieldIDs: {
42 | todo: this.props.todo.id,
43 | },
44 | }];
45 | }
46 |
47 | getVariables() {
48 | return {
49 | id: this.props.todo.id,
50 | text: this.props.text,
51 | };
52 | }
53 |
54 | getOptimisticResponse() {
55 | return {
56 | todo: {
57 | id: this.props.todo.id,
58 | text: this.props.text,
59 | },
60 | };
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/frontend/src/screens/TodoApp/screens/TodoList/styles/Todo.css:
--------------------------------------------------------------------------------
1 | .todo {
2 | position: relative;
3 | font-size: 24px;
4 | border-bottom: 1px solid #ededed;
5 |
6 | &:last-child {
7 | border-bottom: none;
8 |
9 | &.editing {
10 | margin-bottom: -1px;
11 | }
12 | }
13 |
14 | &.editing {
15 | border-bottom: none;
16 | padding: 0;
17 | }
18 | }
19 |
20 | .view {
21 | .editing & {
22 | display: none;
23 | }
24 | }
25 |
26 | .toggle {
27 | text-align: center;
28 | width: 40px;
29 | /* auto, since non-WebKit browsers doesn't support input styling */
30 | height: auto;
31 | position: absolute;
32 | top: 0;
33 | bottom: 0;
34 | margin: auto 0;
35 | border: none; /* Mobile Safari */
36 | -webkit-appearance: none;
37 | appearance: none;
38 |
39 | &::after {
40 | content: url("../images/unchecked.svg");
41 | }
42 |
43 | &:checked::after {
44 | content: url("../images/checked.svg");
45 | }
46 |
47 | .todo:hover & {
48 | display: block;
49 | }
50 |
51 | /*
52 | Hack to remove background from Mobile Safari.
53 | Can't use it globally since it destroys checkboxes in Firefox
54 | */
55 | @media screen and(-webkit-min-device-pixel-ratio:0) {
56 | background: none;
57 | height: 40px;
58 | }
59 | }
60 |
61 | .label {
62 | white-space: pre-line;
63 | word-break: break-all;
64 | padding: 15px 60px 15px 15px;
65 | margin-left: 45px;
66 | display: block;
67 | line-height: 1.2;
68 | transition: color 0.4s;
69 |
70 | .completed & {
71 | color: #d9d9d9;
72 | text-decoration: line-through;
73 | }
74 | }
75 |
76 | .destroy {
77 | display: none;
78 | position: absolute;
79 | top: 0;
80 | right: 10px;
81 | bottom: 0;
82 | width: 40px;
83 | height: 40px;
84 | margin: auto 0;
85 | font-size: 30px;
86 | color: #cc9a9a;
87 | margin-bottom: 11px;
88 | transition: color 0.2s ease-out;
89 |
90 | &:hover {
91 | color: #af5b5e;
92 | }
93 |
94 | &::after {
95 | content: '×';
96 | }
97 |
98 | .todo:hover & {
99 | display: block;
100 | }
101 | }
102 |
103 | .edit {
104 | composes: edit from "shared/styles/edit.css";
105 | display: none;
106 |
107 | .editing & {
108 | display: block;
109 | width: 506px;
110 | padding: 13px 17px 12px 17px;
111 | margin: 0 0 0 43px;
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/frontend/src/screens/TodoApp/screens/TodoList/styles/TodoList.css:
--------------------------------------------------------------------------------
1 | .main {
2 | position: relative;
3 | z-index: 2;
4 | border-top: 1px solid #e6e6e6;
5 | }
6 |
7 | label[for='toggle-all'] {
8 | display: none;
9 | }
10 |
11 | .toggleAll {
12 | position: absolute;
13 | top: -55px;
14 | left: -12px;
15 | width: 60px;
16 | height: 34px;
17 | text-align: center;
18 | border: none; /* Mobile Safari */
19 |
20 | &::before {
21 | content: '❯';
22 | font-size: 22px;
23 | color: #e6e6e6;
24 | padding: 10px 27px 10px 27px;
25 |
26 | &:checked {
27 | color: #737373;
28 | }
29 | }
30 |
31 | /*
32 | Hack to remove background from Mobile Safari.
33 | Can't use it globally since it destroys checkboxes in Firefox
34 | */
35 | @media screen and(-webkit-min-device-pixel-ratio:0) {
36 | background: none;
37 | transform: rotate(90deg);
38 | appearance: none;
39 | }
40 | }
41 |
42 | .todoList {
43 | margin: 0;
44 | padding: 0;
45 | list-style: none;
46 | }
47 |
--------------------------------------------------------------------------------
/frontend/src/screens/TodoApp/screens/TodoList/views/TodoListView.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This file provided by Facebook is for non-commercial testing and evaluation
3 | * purposes only. Facebook reserves all rights not expressly granted.
4 | *
5 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
6 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
7 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
8 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
9 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
10 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
11 | */
12 |
13 | import React from 'react';
14 |
15 | import styles from '../styles/TodoList.css';
16 |
17 | const TodoListView = ({ children, completedCount, onMarkAllChange, totalCount }) => (
18 |
19 |
25 |
26 |
{children}
27 |
28 | );
29 |
30 | TodoListView.propTypes = {
31 | children: React.PropTypes.node,
32 | completedCount: React.PropTypes.number.isRequired,
33 | onMarkAllChange: React.PropTypes.func.isRequired,
34 | totalCount: React.PropTypes.number.isRequired,
35 | };
36 |
37 | export default TodoListView;
38 |
--------------------------------------------------------------------------------
/frontend/src/screens/TodoApp/screens/TodoList/views/TodoView.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This file provided by Facebook is for non-commercial testing and evaluation
3 | * purposes only. Facebook reserves all rights not expressly granted.
4 | *
5 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
6 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
7 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
8 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
9 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
10 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
11 | */
12 |
13 | import classnames from 'classnames';
14 | import React from 'react';
15 |
16 | import styles from '../styles/Todo.css';
17 | import TodoTextInput from 'shared/components/TodoTextInput';
18 |
19 | function TodoView({
20 | isEditing,
21 | onCompleteChange,
22 | onDestroyClick,
23 | onLabelDoubleClick,
24 | onTextInputCancel,
25 | onTextInputDelete,
26 | onTextInputSave,
27 | todo,
28 | }) {
29 | const renderTextInput = () => (
30 |
38 | );
39 | return (
40 |