├── .babelrc ├── .gitignore ├── README.md ├── package.json ├── public ├── index.html └── robots.txt ├── src ├── App.tsx ├── components │ ├── CreateTodoModal │ │ ├── __generated__ │ │ │ └── CreateTodoModal_CreateTodoMutation.graphql.ts │ │ ├── index.tsx │ │ └── styles.ts │ ├── ErrorMessage │ │ ├── index.tsx │ │ └── styles.ts │ ├── FormButton │ │ ├── index.tsx │ │ └── styles.ts │ ├── Header │ │ ├── index.tsx │ │ └── styles.ts │ ├── Input │ │ ├── index.tsx │ │ └── styles.ts │ ├── Spinner │ │ ├── index.tsx │ │ └── styles.ts │ ├── Todo │ │ ├── __generated__ │ │ │ ├── Todo_CompleteMutation.graphql.ts │ │ │ ├── Todo_DeleteTodoMutation.graphql.ts │ │ │ └── Todo_todo.graphql.ts │ │ ├── index.tsx │ │ └── styles.ts │ └── TodoList │ │ ├── __generated__ │ │ ├── TodoListPaginationQuery.graphql.ts │ │ └── TodoList_query.graphql.ts │ │ ├── index.tsx │ │ └── styles.ts ├── data │ └── schema.graphql ├── index.tsx ├── pages │ ├── HomePage │ │ ├── __generated__ │ │ │ └── HomePageQuery_todosQuery.graphql.ts │ │ ├── index.tsx │ │ └── styles.ts │ ├── LoginPage │ │ ├── __generated__ │ │ │ └── LoginPage_authMutation.graphql.ts │ │ ├── index.tsx │ │ └── styles.ts │ └── SignUpPage │ │ ├── __generated__ │ │ └── SignUpPage_CreateUserMutation.graphql.ts │ │ ├── index.tsx │ │ └── styles.ts ├── react-app-env.d.ts ├── relay │ └── RelayEnviroment.tsx ├── routes │ └── index.tsx ├── setupTests.ts └── styles │ └── global.ts ├── tsconfig.json └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | [ 4 | "relay", 5 | { 6 | "artifactDirectory": "./__generated__" 7 | } 8 | ] 9 | ] 10 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `yarn start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `yarn test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `yarn build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `yarn eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todo-list", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.11.4", 7 | "@testing-library/react": "^11.1.0", 8 | "@testing-library/user-event": "^12.1.10", 9 | "@types/jest": "^26.0.15", 10 | "@types/node": "^12.0.0", 11 | "@types/react": "^17.0.0", 12 | "@types/react-dom": "^17.0.0", 13 | "formik": "^2.1.4", 14 | "react": "^17.0.2", 15 | "react-dom": "^17.0.2", 16 | "react-icons": "^4.2.0", 17 | "react-modal": "^3.14.3", 18 | "react-relay": "^11.0.2", 19 | "react-relay-network-modern": "^6.0.0", 20 | "react-router-dom": "^5.2.0", 21 | "react-scripts": "4.0.3", 22 | "react-toastify": "^7.0.4", 23 | "relay-compiler-language-typescript": "^14.1.0", 24 | "relay-runtime": "^11.0.2", 25 | "styled-components": "^5.3.0", 26 | "typescript": "^4.1.2", 27 | "web-vitals": "^1.0.1", 28 | "yup": "^0.32.9" 29 | }, 30 | "scripts": { 31 | "start": "yarn run relay && react-scripts start", 32 | "build": "yarn run relay && react-scripts build", 33 | "relay": "relay-compiler --src ./src --schema ./src/data/schema.graphql --language typescript", 34 | "test": "react-scripts test", 35 | "eject": "react-scripts eject" 36 | }, 37 | "eslintConfig": { 38 | "extends": [ 39 | "react-app", 40 | "react-app/jest" 41 | ] 42 | }, 43 | "browserslist": { 44 | "production": [ 45 | ">0.2%", 46 | "not dead", 47 | "not op_mini all" 48 | ], 49 | "development": [ 50 | "last 1 chrome version", 51 | "last 1 firefox version", 52 | "last 1 safari version" 53 | ] 54 | }, 55 | "devDependencies": { 56 | "@types/react-modal": "^3.12.1", 57 | "@types/react-relay": "^11.0.2", 58 | "@types/react-router-dom": "^5.1.8", 59 | "@types/relay-compiler": "^8.0.1", 60 | "@types/relay-runtime": "^11.0.2", 61 | "@types/styled-components": "^5.1.12", 62 | "babel-plugin-relay": "^11.0.2", 63 | "graphql": "^15.5.1", 64 | "relay-compiler": "^11.0.2" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Relay Todo App 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import RelayEnviroment from "./relay/RelayEnviroment"; 2 | import { RelayEnvironmentProvider } from "react-relay"; 3 | import { Routes } from "./routes"; 4 | import { GlobalStyle } from "./styles/global"; 5 | import { BrowserRouter } from "react-router-dom"; 6 | import { Suspense } from 'react'; 7 | import { Spinner } from "./components/Spinner"; 8 | 9 | function App() { 10 | 11 | return ( 12 | 13 | 14 | }> 15 | 16 | 17 | 18 | 19 | 20 | 21 | ); 22 | } 23 | 24 | export default App; 25 | -------------------------------------------------------------------------------- /src/components/CreateTodoModal/__generated__/CreateTodoModal_CreateTodoMutation.graphql.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // @ts-nocheck 4 | 5 | import { ConcreteRequest } from "relay-runtime"; 6 | 7 | export type CreateTodoInput = { 8 | content: string; 9 | clientMutationId?: string | null; 10 | }; 11 | export type CreateTodoModal_CreateTodoMutationVariables = { 12 | input: CreateTodoInput; 13 | connections: Array; 14 | }; 15 | export type CreateTodoModal_CreateTodoMutationResponse = { 16 | readonly CreateTodoMutation: { 17 | readonly todoEdge: { 18 | readonly cursor: string; 19 | readonly node: { 20 | readonly id: string; 21 | readonly content: string; 22 | readonly isCompleted: boolean; 23 | readonly createdAt: string; 24 | } | null; 25 | } | null; 26 | readonly created: boolean | null; 27 | readonly error: string | null; 28 | } | null; 29 | }; 30 | export type CreateTodoModal_CreateTodoMutation = { 31 | readonly response: CreateTodoModal_CreateTodoMutationResponse; 32 | readonly variables: CreateTodoModal_CreateTodoMutationVariables; 33 | }; 34 | 35 | 36 | 37 | /* 38 | mutation CreateTodoModal_CreateTodoMutation( 39 | $input: CreateTodoInput! 40 | ) { 41 | CreateTodoMutation(input: $input) { 42 | todoEdge { 43 | cursor 44 | node { 45 | id 46 | content 47 | isCompleted 48 | createdAt 49 | } 50 | } 51 | created 52 | error 53 | } 54 | } 55 | */ 56 | 57 | const node: ConcreteRequest = (function(){ 58 | var v0 = { 59 | "defaultValue": null, 60 | "kind": "LocalArgument", 61 | "name": "connections" 62 | }, 63 | v1 = { 64 | "defaultValue": null, 65 | "kind": "LocalArgument", 66 | "name": "input" 67 | }, 68 | v2 = [ 69 | { 70 | "kind": "Variable", 71 | "name": "input", 72 | "variableName": "input" 73 | } 74 | ], 75 | v3 = { 76 | "alias": null, 77 | "args": null, 78 | "concreteType": "TodoEdge", 79 | "kind": "LinkedField", 80 | "name": "todoEdge", 81 | "plural": false, 82 | "selections": [ 83 | { 84 | "alias": null, 85 | "args": null, 86 | "kind": "ScalarField", 87 | "name": "cursor", 88 | "storageKey": null 89 | }, 90 | { 91 | "alias": null, 92 | "args": null, 93 | "concreteType": "Todo", 94 | "kind": "LinkedField", 95 | "name": "node", 96 | "plural": false, 97 | "selections": [ 98 | { 99 | "alias": null, 100 | "args": null, 101 | "kind": "ScalarField", 102 | "name": "id", 103 | "storageKey": null 104 | }, 105 | { 106 | "alias": null, 107 | "args": null, 108 | "kind": "ScalarField", 109 | "name": "content", 110 | "storageKey": null 111 | }, 112 | { 113 | "alias": null, 114 | "args": null, 115 | "kind": "ScalarField", 116 | "name": "isCompleted", 117 | "storageKey": null 118 | }, 119 | { 120 | "alias": null, 121 | "args": null, 122 | "kind": "ScalarField", 123 | "name": "createdAt", 124 | "storageKey": null 125 | } 126 | ], 127 | "storageKey": null 128 | } 129 | ], 130 | "storageKey": null 131 | }, 132 | v4 = { 133 | "alias": null, 134 | "args": null, 135 | "kind": "ScalarField", 136 | "name": "created", 137 | "storageKey": null 138 | }, 139 | v5 = { 140 | "alias": null, 141 | "args": null, 142 | "kind": "ScalarField", 143 | "name": "error", 144 | "storageKey": null 145 | }; 146 | return { 147 | "fragment": { 148 | "argumentDefinitions": [ 149 | (v0/*: any*/), 150 | (v1/*: any*/) 151 | ], 152 | "kind": "Fragment", 153 | "metadata": null, 154 | "name": "CreateTodoModal_CreateTodoMutation", 155 | "selections": [ 156 | { 157 | "alias": null, 158 | "args": (v2/*: any*/), 159 | "concreteType": "CreateTodoPayload", 160 | "kind": "LinkedField", 161 | "name": "CreateTodoMutation", 162 | "plural": false, 163 | "selections": [ 164 | (v3/*: any*/), 165 | (v4/*: any*/), 166 | (v5/*: any*/) 167 | ], 168 | "storageKey": null 169 | } 170 | ], 171 | "type": "Mutation", 172 | "abstractKey": null 173 | }, 174 | "kind": "Request", 175 | "operation": { 176 | "argumentDefinitions": [ 177 | (v1/*: any*/), 178 | (v0/*: any*/) 179 | ], 180 | "kind": "Operation", 181 | "name": "CreateTodoModal_CreateTodoMutation", 182 | "selections": [ 183 | { 184 | "alias": null, 185 | "args": (v2/*: any*/), 186 | "concreteType": "CreateTodoPayload", 187 | "kind": "LinkedField", 188 | "name": "CreateTodoMutation", 189 | "plural": false, 190 | "selections": [ 191 | (v3/*: any*/), 192 | { 193 | "alias": null, 194 | "args": null, 195 | "filters": null, 196 | "handle": "prependEdge", 197 | "key": "", 198 | "kind": "LinkedHandle", 199 | "name": "todoEdge", 200 | "handleArgs": [ 201 | { 202 | "kind": "Variable", 203 | "name": "connections", 204 | "variableName": "connections" 205 | } 206 | ] 207 | }, 208 | (v4/*: any*/), 209 | (v5/*: any*/) 210 | ], 211 | "storageKey": null 212 | } 213 | ] 214 | }, 215 | "params": { 216 | "cacheID": "712b66867131a67ee45b77ec838f6623", 217 | "id": null, 218 | "metadata": {}, 219 | "name": "CreateTodoModal_CreateTodoMutation", 220 | "operationKind": "mutation", 221 | "text": "mutation CreateTodoModal_CreateTodoMutation(\n $input: CreateTodoInput!\n) {\n CreateTodoMutation(input: $input) {\n todoEdge {\n cursor\n node {\n id\n content\n isCompleted\n createdAt\n }\n }\n created\n error\n }\n}\n" 222 | } 223 | }; 224 | })(); 225 | (node as any).hash = 'eb75490a6dea5aff63f7744f7949c1b2'; 226 | export default node; 227 | -------------------------------------------------------------------------------- /src/components/CreateTodoModal/index.tsx: -------------------------------------------------------------------------------- 1 | import Modal from "react-modal"; 2 | import graphql from "babel-plugin-relay/macro"; 3 | import { 4 | CloseModalButton, 5 | ModalForm, 6 | ModalFormInput, 7 | ModalFormLabel, 8 | SubmitModalButton, 9 | } from "./styles"; 10 | import { IoClose } from "react-icons/io5"; 11 | import { useMutation } from "react-relay"; 12 | import { CreateTodoModal_CreateTodoMutation } from "./__generated__/CreateTodoModal_CreateTodoMutation.graphql"; 13 | import { FormEvent } from "react"; 14 | import { useState } from "react"; 15 | 16 | type CreateTodoModalProps = { 17 | isModalOpen: boolean; 18 | handleCloseModal: () => void; 19 | connectionID: any; 20 | }; 21 | 22 | export const CreateTodoModal = ({ 23 | handleCloseModal, 24 | isModalOpen, 25 | connectionID, 26 | }: CreateTodoModalProps) => { 27 | const [inputValue, setInputValue] = useState(""); 28 | 29 | const [commit] = useMutation( 30 | graphql` 31 | mutation CreateTodoModal_CreateTodoMutation( 32 | $input: CreateTodoInput! 33 | $connections: [ID!]! 34 | ) { 35 | CreateTodoMutation(input: $input) { 36 | todoEdge @prependEdge(connections: $connections) { 37 | cursor 38 | node { 39 | id 40 | content 41 | isCompleted 42 | createdAt 43 | } 44 | } 45 | created 46 | error 47 | } 48 | } 49 | ` 50 | ); 51 | 52 | const handleSumit = (event: FormEvent) => { 53 | event.preventDefault(); 54 | commit({ 55 | variables: { 56 | connections: [connectionID], 57 | input: { 58 | content: inputValue, 59 | }, 60 | }, 61 | onCompleted() { 62 | handleCloseModal(); 63 | setInputValue(""); 64 | }, 65 | }); 66 | }; 67 | 68 | return ( 69 | 75 | 76 | handleCloseModal()}> 77 | 78 | 79 |
80 | Create Todo 81 | setInputValue(e.target.value)} 85 | /> 86 |
87 | Submit 88 |
89 |
90 | ); 91 | }; 92 | -------------------------------------------------------------------------------- /src/components/CreateTodoModal/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const ModalForm = styled.form` 4 | display: flex; 5 | flex-direction: column; 6 | `; 7 | 8 | export const ModalFormInput = styled.input` 9 | width: 100%; 10 | background: #f6f8fa; 11 | outline: none; 12 | padding: 4px 16px; 13 | font-weight: 500; 14 | border-radius: 24px; 15 | border: 2px solid var(--relay-orange); 16 | `; 17 | 18 | export const ModalFormLabel = styled.label` 19 | font-size: 24px; 20 | font-weight: 500; 21 | `; 22 | 23 | export const SubmitModalButton = styled.button` 24 | border: none; 25 | width: 100%; 26 | background: var(--relay-orange); 27 | padding: 8px; 28 | color: #fff; 29 | margin-top: 8px; 30 | border-radius: 24px; 31 | transition: filter 0.2s; 32 | 33 | &:hover { 34 | filter: brightness(0.9); 35 | } 36 | `; 37 | 38 | export const CloseModalButton = styled.button` 39 | position: absolute; 40 | right: 1.5rem; 41 | top: 1.5rem; 42 | border: 0; 43 | background: transparent; 44 | transition: filter 0.2s; 45 | color: var(--relay-orange); 46 | font-size: 16px; 47 | 48 | &:hover { 49 | filter: brightness(0.8); 50 | } 51 | `; -------------------------------------------------------------------------------- /src/components/ErrorMessage/index.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | import { ErrorMsg } from "./styles"; 3 | 4 | type ErrorMessageProps = { 5 | children: ReactNode; 6 | }; 7 | export const ErrorMessage = ({ children }: ErrorMessageProps) => { 8 | return ( 9 | {children} 10 | ); 11 | }; -------------------------------------------------------------------------------- /src/components/ErrorMessage/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const ErrorMsg = styled.span` 4 | color: red; 5 | font-size: 14px; 6 | `; -------------------------------------------------------------------------------- /src/components/FormButton/index.tsx: -------------------------------------------------------------------------------- 1 | import { ButtonHTMLAttributes, ReactNode } from "react"; 2 | import { Button } from "./styles"; 3 | 4 | interface ButtonProps extends ButtonHTMLAttributes { 5 | children: ReactNode; 6 | } 7 | 8 | export const FormButton = ({ children, ...rest }: ButtonProps) => { 9 | return ( 10 | 13 | ); 14 | }; -------------------------------------------------------------------------------- /src/components/FormButton/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Button = styled.button` 4 | border: none; 5 | background: var(--relay-orange); 6 | color: #fff; 7 | margin-top: 16px; 8 | padding: 8px; 9 | border-radius: 24px; 10 | transition: filter 0.2s; 11 | 12 | &:hover { 13 | filter: brightness(0.9); 14 | } 15 | `; -------------------------------------------------------------------------------- /src/components/Header/index.tsx: -------------------------------------------------------------------------------- 1 | import { Container, NavButton, HeaderTitle, NavButtonsContainer, NavUserGreeting } from "./styles"; 2 | import { AiOutlineNodeIndex } from 'react-icons/ai'; 3 | import { IoIosAddCircleOutline } from 'react-icons/io'; 4 | import { FiLogOut } from 'react-icons/fi'; 5 | /* import { useAuth } from "../../auth/useAuth"; */ 6 | import { useHistory } from "react-router-dom"; 7 | 8 | type HeaderProps = { 9 | handleOpenModal: () => void; 10 | }; 11 | 12 | export const Header = ({ handleOpenModal }: HeaderProps) => { 13 | const user = JSON.parse(localStorage.getItem('@relayTodo:user')); 14 | const history = useHistory(); 15 | return ( 16 | 17 | Relay Todo 18 | 19 | 20 | Welcome {user.username} 👋 21 | handleOpenModal()}>Create Todo 22 | { 23 | /* signOut(); */ 24 | localStorage.removeItem('@relayTodo:token'); 25 | localStorage.removeItem('@relayTodo:user'); 26 | history.push('/'); 27 | }}> Logout 28 | 29 | 30 | ); 31 | }; -------------------------------------------------------------------------------- /src/components/Header/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Container = styled.header` 4 | display: flex; 5 | justify-content: space-between; 6 | align-items: center; 7 | height: 50px; 8 | background-color: var(--relay-orange); 9 | color: #fff; 10 | padding: 0 1%; 11 | `; 12 | 13 | export const NavButtonsContainer = styled.div` 14 | display: flex; 15 | width: 480px; 16 | justify-content: space-between; 17 | `; 18 | 19 | export const HeaderTitle = styled.span` 20 | font-size: 24px; 21 | font-weight: 500; 22 | display: flex; 23 | align-items: center; 24 | justify-content: space-between; 25 | svg { 26 | margin: 4px 0 0 4px; 27 | font-size: 28px; 28 | } 29 | `; 30 | 31 | export const NavButton = styled.button` 32 | display: flex; 33 | align-items: center; 34 | background: none; 35 | border: none; 36 | color: #fff; 37 | font-size: 18.72px; 38 | font-weight: 500; 39 | transition: filter 0.2s; 40 | 41 | svg { 42 | margin: 2px 0 0 4px; 43 | } 44 | 45 | &:hover { 46 | filter: brightness(0.9); 47 | } 48 | `; 49 | 50 | export const NavUserGreeting = styled.span` 51 | color: #fff; 52 | font-size: 18.72px; 53 | font-weight: 500; 54 | `; -------------------------------------------------------------------------------- /src/components/Input/index.tsx: -------------------------------------------------------------------------------- 1 | import { ChangeEvent, ComponentType } from 'react'; 2 | import { IconBaseProps } from 'react-icons'; 3 | import { Container, CustomInput, IconContainer } from './styles'; 4 | 5 | type InputProps = { 6 | icon: ComponentType; 7 | value: string; 8 | onChange: (e: ChangeEvent) => void; 9 | placeholder: string; 10 | id: string; 11 | name: string; 12 | type: string; 13 | isErrored: boolean; 14 | }; 15 | 16 | export const Input = ({ icon: Icon, value, onChange, placeholder, type, id, name, isErrored }: InputProps) => { 17 | return ( 18 | 19 | 20 | 21 | 22 | 30 | 31 | ); 32 | }; -------------------------------------------------------------------------------- /src/components/Input/styles.ts: -------------------------------------------------------------------------------- 1 | import styled, { css } from "styled-components"; 2 | 3 | type ContainerProps = { 4 | isErrored: boolean; 5 | }; 6 | 7 | export const Container = styled.div` 8 | display: flex; 9 | align-items: center; 10 | justify-content: center; 11 | border: 2px solid var(--relay-orange); 12 | padding: 6px; 13 | 14 | & + div { 15 | margin-top: 8px; 16 | } 17 | 18 | ${props => props.isErrored && css` 19 | border-color: red; 20 | `} 21 | `; 22 | 23 | export const IconContainer = styled.div` 24 | display: flex; 25 | border-right: 1px solid var(--relay-orange); 26 | padding-right: 4px; 27 | color: var(--relay-orange); 28 | 29 | ${props => props.isErrored && css` 30 | color: red; 31 | border-color: red; 32 | `} 33 | `; 34 | 35 | export const CustomInput = styled.input` 36 | width: 100%; 37 | font-weight: 500; 38 | outline: none; 39 | border: none; 40 | margin-left: 4px; 41 | `; -------------------------------------------------------------------------------- /src/components/Spinner/index.tsx: -------------------------------------------------------------------------------- 1 | import { CgSpinner } from 'react-icons/cg'; 2 | import { SpinnerContainer } from './styles'; 3 | 4 | export const Spinner = () => { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; -------------------------------------------------------------------------------- /src/components/Spinner/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const SpinnerContainer = styled.div` 4 | display: flex; 5 | justify-content: center; 6 | align-items: center; 7 | width: 100vw; 8 | height: 100vh; 9 | border: 3px; 10 | font-size: 32px; 11 | color: var(--relay-orange); 12 | 13 | svg { 14 | animation-name: spin; 15 | animation-duration: 500ms; 16 | animation-iteration-count: infinite; 17 | animation-timing-function: linear; 18 | /* transform: rotate(3deg); */ 19 | /* transform: rotate(0.3rad);/ */ 20 | /* transform: rotate(3grad); */ 21 | /* transform: rotate(.03turn); */ 22 | } 23 | 24 | @keyframes spin { 25 | from { 26 | transform:rotate(0deg); 27 | } 28 | to { 29 | transform:rotate(360deg); 30 | } 31 | } 32 | `; -------------------------------------------------------------------------------- /src/components/Todo/__generated__/Todo_CompleteMutation.graphql.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // @ts-nocheck 4 | 5 | import { ConcreteRequest } from "relay-runtime"; 6 | 7 | export type CompleteTodoInput = { 8 | id: string; 9 | clientMutationId?: string | null; 10 | }; 11 | export type Todo_CompleteMutationVariables = { 12 | input: CompleteTodoInput; 13 | }; 14 | export type Todo_CompleteMutationResponse = { 15 | readonly CompleteTodoMutation: { 16 | readonly todoEdge: { 17 | readonly node: { 18 | readonly isCompleted: boolean; 19 | } | null; 20 | } | null; 21 | readonly success: boolean | null; 22 | readonly error: string | null; 23 | } | null; 24 | }; 25 | export type Todo_CompleteMutation = { 26 | readonly response: Todo_CompleteMutationResponse; 27 | readonly variables: Todo_CompleteMutationVariables; 28 | }; 29 | 30 | 31 | 32 | /* 33 | mutation Todo_CompleteMutation( 34 | $input: CompleteTodoInput! 35 | ) { 36 | CompleteTodoMutation(input: $input) { 37 | todoEdge { 38 | node { 39 | isCompleted 40 | id 41 | } 42 | } 43 | success 44 | error 45 | } 46 | } 47 | */ 48 | 49 | const node: ConcreteRequest = (function(){ 50 | var v0 = [ 51 | { 52 | "defaultValue": null, 53 | "kind": "LocalArgument", 54 | "name": "input" 55 | } 56 | ], 57 | v1 = [ 58 | { 59 | "kind": "Variable", 60 | "name": "input", 61 | "variableName": "input" 62 | } 63 | ], 64 | v2 = { 65 | "alias": null, 66 | "args": null, 67 | "kind": "ScalarField", 68 | "name": "isCompleted", 69 | "storageKey": null 70 | }, 71 | v3 = { 72 | "alias": null, 73 | "args": null, 74 | "kind": "ScalarField", 75 | "name": "success", 76 | "storageKey": null 77 | }, 78 | v4 = { 79 | "alias": null, 80 | "args": null, 81 | "kind": "ScalarField", 82 | "name": "error", 83 | "storageKey": null 84 | }; 85 | return { 86 | "fragment": { 87 | "argumentDefinitions": (v0/*: any*/), 88 | "kind": "Fragment", 89 | "metadata": null, 90 | "name": "Todo_CompleteMutation", 91 | "selections": [ 92 | { 93 | "alias": null, 94 | "args": (v1/*: any*/), 95 | "concreteType": "CompleteTodoPayload", 96 | "kind": "LinkedField", 97 | "name": "CompleteTodoMutation", 98 | "plural": false, 99 | "selections": [ 100 | { 101 | "alias": null, 102 | "args": null, 103 | "concreteType": "TodoEdge", 104 | "kind": "LinkedField", 105 | "name": "todoEdge", 106 | "plural": false, 107 | "selections": [ 108 | { 109 | "alias": null, 110 | "args": null, 111 | "concreteType": "Todo", 112 | "kind": "LinkedField", 113 | "name": "node", 114 | "plural": false, 115 | "selections": [ 116 | (v2/*: any*/) 117 | ], 118 | "storageKey": null 119 | } 120 | ], 121 | "storageKey": null 122 | }, 123 | (v3/*: any*/), 124 | (v4/*: any*/) 125 | ], 126 | "storageKey": null 127 | } 128 | ], 129 | "type": "Mutation", 130 | "abstractKey": null 131 | }, 132 | "kind": "Request", 133 | "operation": { 134 | "argumentDefinitions": (v0/*: any*/), 135 | "kind": "Operation", 136 | "name": "Todo_CompleteMutation", 137 | "selections": [ 138 | { 139 | "alias": null, 140 | "args": (v1/*: any*/), 141 | "concreteType": "CompleteTodoPayload", 142 | "kind": "LinkedField", 143 | "name": "CompleteTodoMutation", 144 | "plural": false, 145 | "selections": [ 146 | { 147 | "alias": null, 148 | "args": null, 149 | "concreteType": "TodoEdge", 150 | "kind": "LinkedField", 151 | "name": "todoEdge", 152 | "plural": false, 153 | "selections": [ 154 | { 155 | "alias": null, 156 | "args": null, 157 | "concreteType": "Todo", 158 | "kind": "LinkedField", 159 | "name": "node", 160 | "plural": false, 161 | "selections": [ 162 | (v2/*: any*/), 163 | { 164 | "alias": null, 165 | "args": null, 166 | "kind": "ScalarField", 167 | "name": "id", 168 | "storageKey": null 169 | } 170 | ], 171 | "storageKey": null 172 | } 173 | ], 174 | "storageKey": null 175 | }, 176 | (v3/*: any*/), 177 | (v4/*: any*/) 178 | ], 179 | "storageKey": null 180 | } 181 | ] 182 | }, 183 | "params": { 184 | "cacheID": "7158f937e581772642291ca8d240ce10", 185 | "id": null, 186 | "metadata": {}, 187 | "name": "Todo_CompleteMutation", 188 | "operationKind": "mutation", 189 | "text": "mutation Todo_CompleteMutation(\n $input: CompleteTodoInput!\n) {\n CompleteTodoMutation(input: $input) {\n todoEdge {\n node {\n isCompleted\n id\n }\n }\n success\n error\n }\n}\n" 190 | } 191 | }; 192 | })(); 193 | (node as any).hash = 'f6fca50eae85ad0a4feccc12a4fa5ce8'; 194 | export default node; 195 | -------------------------------------------------------------------------------- /src/components/Todo/__generated__/Todo_DeleteTodoMutation.graphql.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // @ts-nocheck 4 | 5 | import { ConcreteRequest } from "relay-runtime"; 6 | 7 | export type DeleteTodoInput = { 8 | id: string; 9 | clientMutationId?: string | null; 10 | }; 11 | export type Todo_DeleteTodoMutationVariables = { 12 | input: DeleteTodoInput; 13 | connections: Array; 14 | }; 15 | export type Todo_DeleteTodoMutationResponse = { 16 | readonly DeleteTodoMutation: { 17 | readonly deletedTodo: string | null; 18 | readonly success: boolean | null; 19 | readonly error: string | null; 20 | } | null; 21 | }; 22 | export type Todo_DeleteTodoMutation = { 23 | readonly response: Todo_DeleteTodoMutationResponse; 24 | readonly variables: Todo_DeleteTodoMutationVariables; 25 | }; 26 | 27 | 28 | 29 | /* 30 | mutation Todo_DeleteTodoMutation( 31 | $input: DeleteTodoInput! 32 | ) { 33 | DeleteTodoMutation(input: $input) { 34 | deletedTodo 35 | success 36 | error 37 | } 38 | } 39 | */ 40 | 41 | const node: ConcreteRequest = (function(){ 42 | var v0 = { 43 | "defaultValue": null, 44 | "kind": "LocalArgument", 45 | "name": "connections" 46 | }, 47 | v1 = { 48 | "defaultValue": null, 49 | "kind": "LocalArgument", 50 | "name": "input" 51 | }, 52 | v2 = [ 53 | { 54 | "kind": "Variable", 55 | "name": "input", 56 | "variableName": "input" 57 | } 58 | ], 59 | v3 = { 60 | "alias": null, 61 | "args": null, 62 | "kind": "ScalarField", 63 | "name": "deletedTodo", 64 | "storageKey": null 65 | }, 66 | v4 = { 67 | "alias": null, 68 | "args": null, 69 | "kind": "ScalarField", 70 | "name": "success", 71 | "storageKey": null 72 | }, 73 | v5 = { 74 | "alias": null, 75 | "args": null, 76 | "kind": "ScalarField", 77 | "name": "error", 78 | "storageKey": null 79 | }; 80 | return { 81 | "fragment": { 82 | "argumentDefinitions": [ 83 | (v0/*: any*/), 84 | (v1/*: any*/) 85 | ], 86 | "kind": "Fragment", 87 | "metadata": null, 88 | "name": "Todo_DeleteTodoMutation", 89 | "selections": [ 90 | { 91 | "alias": null, 92 | "args": (v2/*: any*/), 93 | "concreteType": "DeleteTodoPayload", 94 | "kind": "LinkedField", 95 | "name": "DeleteTodoMutation", 96 | "plural": false, 97 | "selections": [ 98 | (v3/*: any*/), 99 | (v4/*: any*/), 100 | (v5/*: any*/) 101 | ], 102 | "storageKey": null 103 | } 104 | ], 105 | "type": "Mutation", 106 | "abstractKey": null 107 | }, 108 | "kind": "Request", 109 | "operation": { 110 | "argumentDefinitions": [ 111 | (v1/*: any*/), 112 | (v0/*: any*/) 113 | ], 114 | "kind": "Operation", 115 | "name": "Todo_DeleteTodoMutation", 116 | "selections": [ 117 | { 118 | "alias": null, 119 | "args": (v2/*: any*/), 120 | "concreteType": "DeleteTodoPayload", 121 | "kind": "LinkedField", 122 | "name": "DeleteTodoMutation", 123 | "plural": false, 124 | "selections": [ 125 | (v3/*: any*/), 126 | { 127 | "alias": null, 128 | "args": null, 129 | "filters": null, 130 | "handle": "deleteEdge", 131 | "key": "", 132 | "kind": "ScalarHandle", 133 | "name": "deletedTodo", 134 | "handleArgs": [ 135 | { 136 | "kind": "Variable", 137 | "name": "connections", 138 | "variableName": "connections" 139 | } 140 | ] 141 | }, 142 | (v4/*: any*/), 143 | (v5/*: any*/) 144 | ], 145 | "storageKey": null 146 | } 147 | ] 148 | }, 149 | "params": { 150 | "cacheID": "bb21d3afa412033bd266844800949d1e", 151 | "id": null, 152 | "metadata": {}, 153 | "name": "Todo_DeleteTodoMutation", 154 | "operationKind": "mutation", 155 | "text": "mutation Todo_DeleteTodoMutation(\n $input: DeleteTodoInput!\n) {\n DeleteTodoMutation(input: $input) {\n deletedTodo\n success\n error\n }\n}\n" 156 | } 157 | }; 158 | })(); 159 | (node as any).hash = 'fdf80cd4c988ec763ef9d576ec353174'; 160 | export default node; 161 | -------------------------------------------------------------------------------- /src/components/Todo/__generated__/Todo_todo.graphql.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // @ts-nocheck 4 | 5 | import { ReaderFragment } from "relay-runtime"; 6 | 7 | import { FragmentRefs } from "relay-runtime"; 8 | export type Todo_todo = { 9 | readonly id: string; 10 | readonly content: string; 11 | readonly isCompleted: boolean; 12 | readonly createdAt: string; 13 | readonly " $refType": "Todo_todo"; 14 | }; 15 | export type Todo_todo$data = Todo_todo; 16 | export type Todo_todo$key = { 17 | readonly " $data"?: Todo_todo$data; 18 | readonly " $fragmentRefs": FragmentRefs<"Todo_todo">; 19 | }; 20 | 21 | 22 | 23 | const node: ReaderFragment = { 24 | "argumentDefinitions": [], 25 | "kind": "Fragment", 26 | "metadata": null, 27 | "name": "Todo_todo", 28 | "selections": [ 29 | { 30 | "alias": null, 31 | "args": null, 32 | "kind": "ScalarField", 33 | "name": "id", 34 | "storageKey": null 35 | }, 36 | { 37 | "alias": null, 38 | "args": null, 39 | "kind": "ScalarField", 40 | "name": "content", 41 | "storageKey": null 42 | }, 43 | { 44 | "alias": null, 45 | "args": null, 46 | "kind": "ScalarField", 47 | "name": "isCompleted", 48 | "storageKey": null 49 | }, 50 | { 51 | "alias": null, 52 | "args": null, 53 | "kind": "ScalarField", 54 | "name": "createdAt", 55 | "storageKey": null 56 | } 57 | ], 58 | "type": "Todo", 59 | "abstractKey": null 60 | }; 61 | (node as any).hash = 'f48c0196674f547c6152acc15c23beef'; 62 | export default node; 63 | -------------------------------------------------------------------------------- /src/components/Todo/index.tsx: -------------------------------------------------------------------------------- 1 | import graphql from "babel-plugin-relay/macro"; 2 | import { useFragment, useMutation } from "react-relay"; 3 | import { 4 | CompleteTodoButton, 5 | Container, 6 | DeleteTodoButton, 7 | TodoContainer, 8 | TodoContent, 9 | TodoContentContainer, 10 | TodoDate, 11 | } from "./styles"; 12 | import { Todo_todo$key } from "./__generated__/Todo_todo.graphql"; 13 | import { Todo_DeleteTodoMutation } from "./__generated__/Todo_DeleteTodoMutation.graphql"; 14 | import { Todo_CompleteMutation } from "./__generated__/Todo_CompleteMutation.graphql"; 15 | import { IoClose } from "react-icons/io5"; 16 | import { AiFillCheckCircle, AiOutlineCheckCircle } from "react-icons/ai"; 17 | 18 | type TodoListProps = { 19 | query: Todo_todo$key; 20 | connectionID: any; 21 | }; 22 | 23 | export const Todo = ({ query, connectionID }: TodoListProps) => { 24 | const data = useFragment( 25 | graphql` 26 | fragment Todo_todo on Todo { 27 | id 28 | content 29 | isCompleted 30 | createdAt 31 | } 32 | `, 33 | query 34 | ); 35 | 36 | const [commitComplete] = useMutation( 37 | graphql` 38 | mutation Todo_CompleteMutation($input: CompleteTodoInput!) { 39 | CompleteTodoMutation(input: $input) { 40 | todoEdge { 41 | node { 42 | isCompleted 43 | } 44 | } 45 | success 46 | error 47 | } 48 | } 49 | ` 50 | ); 51 | 52 | const [commitDelete] = useMutation( 53 | graphql` 54 | mutation Todo_DeleteTodoMutation( 55 | $input: DeleteTodoInput! 56 | $connections: [ID!]! 57 | ) { 58 | DeleteTodoMutation(input: $input) { 59 | deletedTodo @deleteEdge(connections: $connections) 60 | success 61 | error 62 | } 63 | } 64 | ` 65 | ); 66 | 67 | const handleCompleteTodo = () => { 68 | commitComplete({ 69 | variables: { 70 | input: { 71 | id: data.id, 72 | }, 73 | }, 74 | onCompleted: () => {}, 75 | }); 76 | }; 77 | 78 | const handleDeleteTodo = () => { 79 | commitDelete({ 80 | variables: { 81 | connections: [connectionID], 82 | input: { 83 | id: data.id, 84 | }, 85 | }, 86 | onCompleted: () => { 87 | console.log(""); 88 | }, 89 | }); 90 | }; 91 | 92 | return ( 93 | 94 | 95 | handleCompleteTodo()}> 96 | {data.isCompleted ? : } 97 | 98 | 99 | {data.content} 100 | 101 | {new Date(Number(data.createdAt)).toLocaleDateString("pt-BR", { 102 | day: "2-digit", 103 | month: "long", 104 | year: "numeric", 105 | })} 106 | 107 | 108 | 109 | handleDeleteTodo()}> 110 | 111 | 112 | 113 | ); 114 | }; 115 | -------------------------------------------------------------------------------- /src/components/Todo/styles.ts: -------------------------------------------------------------------------------- 1 | import styled, { css } from "styled-components"; 2 | 3 | type TodoContentContainerProps = { 4 | isCompleted: boolean; 5 | }; 6 | 7 | export const Container = styled.div` 8 | display: flex; 9 | justify-content: space-between; 10 | border: 2px solid var(--relay-orange); 11 | background: #f6f8fa; 12 | padding: 16px; 13 | 14 | & + div { 15 | margin-top: 10px; 16 | } 17 | `; 18 | 19 | export const TodoContainer = styled.div` 20 | display: flex; 21 | `; 22 | 23 | export const TodoContentContainer = styled.div` 24 | ${props => props.isCompleted && css` 25 | text-decoration: line-through; 26 | `} 27 | `; 28 | 29 | export const TodoContent = styled.h1` 30 | font-size: 24px; 31 | text-transform: capitalize; 32 | `; 33 | 34 | export const TodoDate = styled.h2` 35 | font-size: 16px; 36 | `; 37 | 38 | 39 | export const DeleteTodoButton = styled.button` 40 | display: flex; 41 | align-items: center; 42 | background: none; 43 | border: none; 44 | font-size: 24px; 45 | font-weight: 600; 46 | color: var(--relay-orange); 47 | transition: color 0.2s; 48 | 49 | &:hover { 50 | color: #ff0d0d; 51 | } 52 | `; 53 | 54 | export const CompleteTodoButton = styled.button` 55 | display: flex; 56 | align-items: center; 57 | background: none; 58 | border: none; 59 | margin-right: 16px; 60 | font-size: 24px; 61 | color: var(--relay-orange); 62 | transition: color 0.2s; 63 | 64 | &:hover { 65 | color: #0bbf4a; 66 | } 67 | `; -------------------------------------------------------------------------------- /src/components/TodoList/__generated__/TodoListPaginationQuery.graphql.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // @ts-nocheck 4 | 5 | import { ConcreteRequest } from "relay-runtime"; 6 | 7 | import { FragmentRefs } from "relay-runtime"; 8 | export type TodoListPaginationQueryVariables = { 9 | after?: string | null; 10 | first?: number | null; 11 | }; 12 | export type TodoListPaginationQueryResponse = { 13 | readonly " $fragmentRefs": FragmentRefs<"TodoList_query">; 14 | }; 15 | export type TodoListPaginationQuery = { 16 | readonly response: TodoListPaginationQueryResponse; 17 | readonly variables: TodoListPaginationQueryVariables; 18 | }; 19 | 20 | 21 | 22 | /* 23 | query TodoListPaginationQuery( 24 | $after: String 25 | $first: Int = 4 26 | ) { 27 | ...TodoList_query_2HEEH6 28 | } 29 | 30 | fragment TodoList_query_2HEEH6 on Query { 31 | todos(first: $first, after: $after) { 32 | edges { 33 | cursor 34 | node { 35 | id 36 | ...Todo_todo 37 | __typename 38 | } 39 | } 40 | pageInfo { 41 | hasNextPage 42 | hasPreviousPage 43 | startCursor 44 | endCursor 45 | } 46 | } 47 | } 48 | 49 | fragment Todo_todo on Todo { 50 | id 51 | content 52 | isCompleted 53 | createdAt 54 | } 55 | */ 56 | 57 | const node: ConcreteRequest = (function(){ 58 | var v0 = [ 59 | { 60 | "defaultValue": null, 61 | "kind": "LocalArgument", 62 | "name": "after" 63 | }, 64 | { 65 | "defaultValue": 4, 66 | "kind": "LocalArgument", 67 | "name": "first" 68 | } 69 | ], 70 | v1 = [ 71 | { 72 | "kind": "Variable", 73 | "name": "after", 74 | "variableName": "after" 75 | }, 76 | { 77 | "kind": "Variable", 78 | "name": "first", 79 | "variableName": "first" 80 | } 81 | ]; 82 | return { 83 | "fragment": { 84 | "argumentDefinitions": (v0/*: any*/), 85 | "kind": "Fragment", 86 | "metadata": null, 87 | "name": "TodoListPaginationQuery", 88 | "selections": [ 89 | { 90 | "args": (v1/*: any*/), 91 | "kind": "FragmentSpread", 92 | "name": "TodoList_query" 93 | } 94 | ], 95 | "type": "Query", 96 | "abstractKey": null 97 | }, 98 | "kind": "Request", 99 | "operation": { 100 | "argumentDefinitions": (v0/*: any*/), 101 | "kind": "Operation", 102 | "name": "TodoListPaginationQuery", 103 | "selections": [ 104 | { 105 | "alias": null, 106 | "args": (v1/*: any*/), 107 | "concreteType": "TodoConnection", 108 | "kind": "LinkedField", 109 | "name": "todos", 110 | "plural": false, 111 | "selections": [ 112 | { 113 | "alias": null, 114 | "args": null, 115 | "concreteType": "TodoEdge", 116 | "kind": "LinkedField", 117 | "name": "edges", 118 | "plural": true, 119 | "selections": [ 120 | { 121 | "alias": null, 122 | "args": null, 123 | "kind": "ScalarField", 124 | "name": "cursor", 125 | "storageKey": null 126 | }, 127 | { 128 | "alias": null, 129 | "args": null, 130 | "concreteType": "Todo", 131 | "kind": "LinkedField", 132 | "name": "node", 133 | "plural": false, 134 | "selections": [ 135 | { 136 | "alias": null, 137 | "args": null, 138 | "kind": "ScalarField", 139 | "name": "id", 140 | "storageKey": null 141 | }, 142 | { 143 | "alias": null, 144 | "args": null, 145 | "kind": "ScalarField", 146 | "name": "content", 147 | "storageKey": null 148 | }, 149 | { 150 | "alias": null, 151 | "args": null, 152 | "kind": "ScalarField", 153 | "name": "isCompleted", 154 | "storageKey": null 155 | }, 156 | { 157 | "alias": null, 158 | "args": null, 159 | "kind": "ScalarField", 160 | "name": "createdAt", 161 | "storageKey": null 162 | }, 163 | { 164 | "alias": null, 165 | "args": null, 166 | "kind": "ScalarField", 167 | "name": "__typename", 168 | "storageKey": null 169 | } 170 | ], 171 | "storageKey": null 172 | } 173 | ], 174 | "storageKey": null 175 | }, 176 | { 177 | "alias": null, 178 | "args": null, 179 | "concreteType": "PageInfo", 180 | "kind": "LinkedField", 181 | "name": "pageInfo", 182 | "plural": false, 183 | "selections": [ 184 | { 185 | "alias": null, 186 | "args": null, 187 | "kind": "ScalarField", 188 | "name": "hasNextPage", 189 | "storageKey": null 190 | }, 191 | { 192 | "alias": null, 193 | "args": null, 194 | "kind": "ScalarField", 195 | "name": "hasPreviousPage", 196 | "storageKey": null 197 | }, 198 | { 199 | "alias": null, 200 | "args": null, 201 | "kind": "ScalarField", 202 | "name": "startCursor", 203 | "storageKey": null 204 | }, 205 | { 206 | "alias": null, 207 | "args": null, 208 | "kind": "ScalarField", 209 | "name": "endCursor", 210 | "storageKey": null 211 | } 212 | ], 213 | "storageKey": null 214 | }, 215 | { 216 | "kind": "ClientExtension", 217 | "selections": [ 218 | { 219 | "alias": null, 220 | "args": null, 221 | "kind": "ScalarField", 222 | "name": "__id", 223 | "storageKey": null 224 | } 225 | ] 226 | } 227 | ], 228 | "storageKey": null 229 | }, 230 | { 231 | "alias": null, 232 | "args": (v1/*: any*/), 233 | "filters": null, 234 | "handle": "connection", 235 | "key": "TodoList_todos", 236 | "kind": "LinkedHandle", 237 | "name": "todos" 238 | } 239 | ] 240 | }, 241 | "params": { 242 | "cacheID": "7abb12a4112d3996b0fe3a81bcb2c8d8", 243 | "id": null, 244 | "metadata": {}, 245 | "name": "TodoListPaginationQuery", 246 | "operationKind": "query", 247 | "text": "query TodoListPaginationQuery(\n $after: String\n $first: Int = 4\n) {\n ...TodoList_query_2HEEH6\n}\n\nfragment TodoList_query_2HEEH6 on Query {\n todos(first: $first, after: $after) {\n edges {\n cursor\n node {\n id\n ...Todo_todo\n __typename\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n}\n\nfragment Todo_todo on Todo {\n id\n content\n isCompleted\n createdAt\n}\n" 248 | } 249 | }; 250 | })(); 251 | (node as any).hash = 'f3bd7748da33aeb9809be4ef551e18d9'; 252 | export default node; 253 | -------------------------------------------------------------------------------- /src/components/TodoList/__generated__/TodoList_query.graphql.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // @ts-nocheck 4 | 5 | import { ReaderFragment } from "relay-runtime"; 6 | import TodoListPaginationQuery from "./TodoListPaginationQuery.graphql"; 7 | import { FragmentRefs } from "relay-runtime"; 8 | export type TodoList_query = { 9 | readonly todos: { 10 | readonly __id: string; 11 | readonly edges: ReadonlyArray<{ 12 | readonly cursor: string; 13 | readonly node: { 14 | readonly id: string; 15 | readonly " $fragmentRefs": FragmentRefs<"Todo_todo">; 16 | } | null; 17 | } | null> | null; 18 | readonly pageInfo: { 19 | readonly hasNextPage: boolean; 20 | readonly hasPreviousPage: boolean; 21 | readonly startCursor: string | null; 22 | readonly endCursor: string | null; 23 | }; 24 | } | null; 25 | readonly " $refType": "TodoList_query"; 26 | }; 27 | export type TodoList_query$data = TodoList_query; 28 | export type TodoList_query$key = { 29 | readonly " $data"?: TodoList_query$data; 30 | readonly " $fragmentRefs": FragmentRefs<"TodoList_query">; 31 | }; 32 | 33 | 34 | 35 | const node: ReaderFragment = (function(){ 36 | var v0 = [ 37 | "todos" 38 | ]; 39 | return { 40 | "argumentDefinitions": [ 41 | { 42 | "defaultValue": null, 43 | "kind": "LocalArgument", 44 | "name": "after" 45 | }, 46 | { 47 | "defaultValue": 4, 48 | "kind": "LocalArgument", 49 | "name": "first" 50 | } 51 | ], 52 | "kind": "Fragment", 53 | "metadata": { 54 | "connection": [ 55 | { 56 | "count": "first", 57 | "cursor": "after", 58 | "direction": "forward", 59 | "path": (v0/*: any*/) 60 | } 61 | ], 62 | "refetch": { 63 | "connection": { 64 | "forward": { 65 | "count": "first", 66 | "cursor": "after" 67 | }, 68 | "backward": null, 69 | "path": (v0/*: any*/) 70 | }, 71 | "fragmentPathInResult": [], 72 | "operation": TodoListPaginationQuery 73 | } 74 | }, 75 | "name": "TodoList_query", 76 | "selections": [ 77 | { 78 | "alias": "todos", 79 | "args": null, 80 | "concreteType": "TodoConnection", 81 | "kind": "LinkedField", 82 | "name": "__TodoList_todos_connection", 83 | "plural": false, 84 | "selections": [ 85 | { 86 | "alias": null, 87 | "args": null, 88 | "concreteType": "TodoEdge", 89 | "kind": "LinkedField", 90 | "name": "edges", 91 | "plural": true, 92 | "selections": [ 93 | { 94 | "alias": null, 95 | "args": null, 96 | "kind": "ScalarField", 97 | "name": "cursor", 98 | "storageKey": null 99 | }, 100 | { 101 | "alias": null, 102 | "args": null, 103 | "concreteType": "Todo", 104 | "kind": "LinkedField", 105 | "name": "node", 106 | "plural": false, 107 | "selections": [ 108 | { 109 | "alias": null, 110 | "args": null, 111 | "kind": "ScalarField", 112 | "name": "id", 113 | "storageKey": null 114 | }, 115 | { 116 | "alias": null, 117 | "args": null, 118 | "kind": "ScalarField", 119 | "name": "__typename", 120 | "storageKey": null 121 | }, 122 | { 123 | "args": null, 124 | "kind": "FragmentSpread", 125 | "name": "Todo_todo" 126 | } 127 | ], 128 | "storageKey": null 129 | } 130 | ], 131 | "storageKey": null 132 | }, 133 | { 134 | "alias": null, 135 | "args": null, 136 | "concreteType": "PageInfo", 137 | "kind": "LinkedField", 138 | "name": "pageInfo", 139 | "plural": false, 140 | "selections": [ 141 | { 142 | "alias": null, 143 | "args": null, 144 | "kind": "ScalarField", 145 | "name": "hasNextPage", 146 | "storageKey": null 147 | }, 148 | { 149 | "alias": null, 150 | "args": null, 151 | "kind": "ScalarField", 152 | "name": "hasPreviousPage", 153 | "storageKey": null 154 | }, 155 | { 156 | "alias": null, 157 | "args": null, 158 | "kind": "ScalarField", 159 | "name": "startCursor", 160 | "storageKey": null 161 | }, 162 | { 163 | "alias": null, 164 | "args": null, 165 | "kind": "ScalarField", 166 | "name": "endCursor", 167 | "storageKey": null 168 | } 169 | ], 170 | "storageKey": null 171 | }, 172 | { 173 | "kind": "ClientExtension", 174 | "selections": [ 175 | { 176 | "alias": null, 177 | "args": null, 178 | "kind": "ScalarField", 179 | "name": "__id", 180 | "storageKey": null 181 | } 182 | ] 183 | } 184 | ], 185 | "storageKey": null 186 | } 187 | ], 188 | "type": "Query", 189 | "abstractKey": null 190 | }; 191 | })(); 192 | (node as any).hash = 'f3bd7748da33aeb9809be4ef551e18d9'; 193 | export default node; 194 | -------------------------------------------------------------------------------- /src/components/TodoList/index.tsx: -------------------------------------------------------------------------------- 1 | import { graphql } from "babel-plugin-relay/macro"; 2 | import { useEffect, useState } from "react"; 3 | import { usePaginationFragment } from "react-relay"; 4 | import { Todo } from "../Todo"; 5 | import { Container } from "./styles"; 6 | import { TodoList_query$key } from "./__generated__/TodoList_query.graphql"; 7 | import { useHistory } from "react-router-dom"; 8 | import { CreateTodoModal } from "../CreateTodoModal"; 9 | 10 | type TodoListProps = { 11 | query: TodoList_query$key; 12 | isModalOpen: boolean; 13 | setIsModalOpen: React.Dispatch>; 14 | }; 15 | 16 | export const TodoList = ({ 17 | query, 18 | isModalOpen, 19 | setIsModalOpen, 20 | }: TodoListProps) => { 21 | const { data, loadNext, isLoadingNext } = usePaginationFragment( 22 | graphql` 23 | fragment TodoList_query on Query 24 | @argumentDefinitions( 25 | first: { type: Int, defaultValue: 4 } 26 | after: { type: String } 27 | ) 28 | @refetchable(queryName: "TodoListPaginationQuery") { 29 | todos(first: $first, after: $after) @connection(key: "TodoList_todos") { 30 | __id 31 | edges { 32 | cursor 33 | node { 34 | id 35 | ...Todo_todo 36 | } 37 | } 38 | pageInfo { 39 | hasNextPage 40 | hasPreviousPage 41 | startCursor 42 | endCursor 43 | } 44 | } 45 | } 46 | `, 47 | query 48 | ); 49 | const history = useHistory(); 50 | // const [isModalOpen, setIsModalOpen] = useState(false); 51 | 52 | const handleCloseModal = () => { 53 | setIsModalOpen(false); 54 | }; 55 | 56 | useEffect(() => { 57 | const token = localStorage.getItem("@relayTodo:token"); 58 | if (!token) { 59 | history.push("/"); 60 | } 61 | }, [history]); 62 | 63 | return ( 64 | 65 | 70 | {data?.todos.edges.map(({ node }) => ( 71 | 72 | ))} 73 | 74 | ); 75 | }; 76 | -------------------------------------------------------------------------------- /src/components/TodoList/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Container = styled.div` 4 | display: flex; 5 | justify-content: center; 6 | flex-direction: column; 7 | width: 60%; 8 | margin-top: 12px; 9 | padding: 32px; 10 | `; -------------------------------------------------------------------------------- /src/data/schema.graphql: -------------------------------------------------------------------------------- 1 | """Exposes a URL that specifies the behaviour of this scalar.""" 2 | directive @specifiedBy( 3 | """The URL that specifies the behaviour of this scalar.""" 4 | url: String! 5 | ) on SCALAR 6 | 7 | input AuthUserInput { 8 | email: String! 9 | password: String! 10 | clientMutationId: String 11 | } 12 | 13 | type AuthUserPayload { 14 | me: User 15 | token: String 16 | error: String 17 | clientMutationId: String 18 | } 19 | 20 | input CompleteTodoInput { 21 | id: String! 22 | clientMutationId: String 23 | } 24 | 25 | type CompleteTodoPayload { 26 | todoEdge: TodoEdge 27 | success: Boolean 28 | error: String 29 | clientMutationId: String 30 | } 31 | 32 | input CreateTodoInput { 33 | content: String! 34 | clientMutationId: String 35 | } 36 | 37 | type CreateTodoPayload { 38 | todoEdge: TodoEdge 39 | created: Boolean 40 | error: String 41 | clientMutationId: String 42 | } 43 | 44 | input CreateUserInput { 45 | username: String! 46 | email: String! 47 | password: String! 48 | clientMutationId: String 49 | } 50 | 51 | type CreateUserPayload { 52 | me: User 53 | token: String 54 | error: String 55 | clientMutationId: String 56 | } 57 | 58 | input DeleteTodoInput { 59 | id: String! 60 | clientMutationId: String 61 | } 62 | 63 | type DeleteTodoPayload { 64 | deletedTodo: ID 65 | success: Boolean 66 | error: String 67 | clientMutationId: String 68 | } 69 | 70 | input DeleteUserInput { 71 | clientMutationId: String 72 | } 73 | 74 | type DeleteUserPayload { 75 | success: Boolean 76 | error: String 77 | clientMutationId: String 78 | } 79 | 80 | input EditTodoInput { 81 | id: String! 82 | content: String! 83 | clientMutationId: String 84 | } 85 | 86 | type EditTodoPayload { 87 | todo: TodoEdge 88 | error: String 89 | clientMutationId: String 90 | } 91 | 92 | """Root of all mutation""" 93 | type Mutation { 94 | """Create User Mutation""" 95 | CreateUserMutation(input: CreateUserInput!): CreateUserPayload 96 | 97 | """Authenticate User Mutation""" 98 | AuthUserMutation(input: AuthUserInput!): AuthUserPayload 99 | 100 | """Delete User Mutation""" 101 | DeleteUserMutation(input: DeleteUserInput!): DeleteUserPayload 102 | 103 | """Complete Todo Mutation""" 104 | CompleteTodoMutation(input: CompleteTodoInput!): CompleteTodoPayload 105 | 106 | """Create Todo Mutation""" 107 | CreateTodoMutation(input: CreateTodoInput!): CreateTodoPayload 108 | 109 | """Edit Todo Mutation""" 110 | EditTodoMutation(input: EditTodoInput!): EditTodoPayload 111 | DeleteTodoMutation(input: DeleteTodoInput!): DeleteTodoPayload 112 | } 113 | 114 | """An object with an ID""" 115 | interface Node { 116 | """The id of the object.""" 117 | id: ID! 118 | } 119 | 120 | """Information about pagination in a connection.""" 121 | type PageInfo { 122 | """When paginating forwards, are there more items?""" 123 | hasNextPage: Boolean! 124 | 125 | """When paginating backwards, are there more items?""" 126 | hasPreviousPage: Boolean! 127 | 128 | """When paginating backwards, the cursor to continue.""" 129 | startCursor: String 130 | 131 | """When paginating forwards, the cursor to continue.""" 132 | endCursor: String 133 | } 134 | 135 | """The root of all queries""" 136 | type Query { 137 | """Fetches an object given its ID""" 138 | node( 139 | """The ID of an object""" 140 | id: ID! 141 | ): Node 142 | 143 | """Fetches objects given their IDs""" 144 | nodes( 145 | """The IDs of objects""" 146 | ids: [ID!]! 147 | ): [Node]! 148 | users( 149 | """Returns the items in the list that come after the specified cursor.""" 150 | after: String 151 | 152 | """Returns the first n items from the list.""" 153 | first: Int 154 | 155 | """Returns the items in the list that come before the specified cursor.""" 156 | before: String 157 | 158 | """Returns the last n items from the list.""" 159 | last: Int 160 | ): UserConnection 161 | user: User 162 | todos( 163 | """Returns the items in the list that come after the specified cursor.""" 164 | after: String 165 | 166 | """Returns the first n items from the list.""" 167 | first: Int 168 | 169 | """Returns the items in the list that come before the specified cursor.""" 170 | before: String 171 | 172 | """Returns the last n items from the list.""" 173 | last: Int 174 | ): TodoConnection 175 | todo(id: String!): Todo 176 | } 177 | 178 | """Todo type""" 179 | type Todo implements Node { 180 | """The ID of an object""" 181 | id: ID! 182 | content: String! 183 | owner: User 184 | isCompleted: Boolean! 185 | createdAt: String! 186 | } 187 | 188 | """A connection to a list of items.""" 189 | type TodoConnection { 190 | """Information to aid in pagination.""" 191 | pageInfo: PageInfo! 192 | 193 | """A list of edges.""" 194 | edges: [TodoEdge] 195 | } 196 | 197 | """An edge in a connection.""" 198 | type TodoEdge { 199 | """The item at the end of the edge""" 200 | node: Todo 201 | 202 | """A cursor for use in pagination""" 203 | cursor: String! 204 | } 205 | 206 | """User type""" 207 | type User implements Node { 208 | """The ID of an object""" 209 | id: ID! 210 | username: String! 211 | email: String! 212 | todos( 213 | """Returns the items in the list that come after the specified cursor.""" 214 | after: String 215 | 216 | """Returns the first n items from the list.""" 217 | first: Int 218 | 219 | """Returns the items in the list that come before the specified cursor.""" 220 | before: String 221 | 222 | """Returns the last n items from the list.""" 223 | last: Int 224 | ): TodoConnection! 225 | } 226 | 227 | """A connection to a list of items.""" 228 | type UserConnection { 229 | """Information to aid in pagination.""" 230 | pageInfo: PageInfo! 231 | 232 | """A list of edges.""" 233 | edges: [UserEdge] 234 | } 235 | 236 | """An edge in a connection.""" 237 | type UserEdge { 238 | """The item at the end of the edge""" 239 | node: User 240 | 241 | """A cursor for use in pagination""" 242 | cursor: String! 243 | } 244 | 245 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | ReactDOM.render( 6 | 7 | 8 | , 9 | document.getElementById('root') 10 | ); 11 | 12 | -------------------------------------------------------------------------------- /src/pages/HomePage/__generated__/HomePageQuery_todosQuery.graphql.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // @ts-nocheck 4 | 5 | import { ConcreteRequest } from "relay-runtime"; 6 | 7 | import { FragmentRefs } from "relay-runtime"; 8 | export type HomePageQuery_todosQueryVariables = {}; 9 | export type HomePageQuery_todosQueryResponse = { 10 | readonly " $fragmentRefs": FragmentRefs<"TodoList_query">; 11 | }; 12 | export type HomePageQuery_todosQuery = { 13 | readonly response: HomePageQuery_todosQueryResponse; 14 | readonly variables: HomePageQuery_todosQueryVariables; 15 | }; 16 | 17 | 18 | 19 | /* 20 | query HomePageQuery_todosQuery { 21 | ...TodoList_query 22 | } 23 | 24 | fragment TodoList_query on Query { 25 | todos(first: 4) { 26 | edges { 27 | cursor 28 | node { 29 | id 30 | ...Todo_todo 31 | __typename 32 | } 33 | } 34 | pageInfo { 35 | hasNextPage 36 | hasPreviousPage 37 | startCursor 38 | endCursor 39 | } 40 | } 41 | } 42 | 43 | fragment Todo_todo on Todo { 44 | id 45 | content 46 | isCompleted 47 | createdAt 48 | } 49 | */ 50 | 51 | const node: ConcreteRequest = (function(){ 52 | var v0 = [ 53 | { 54 | "kind": "Literal", 55 | "name": "first", 56 | "value": 4 57 | } 58 | ]; 59 | return { 60 | "fragment": { 61 | "argumentDefinitions": [], 62 | "kind": "Fragment", 63 | "metadata": null, 64 | "name": "HomePageQuery_todosQuery", 65 | "selections": [ 66 | { 67 | "args": null, 68 | "kind": "FragmentSpread", 69 | "name": "TodoList_query" 70 | } 71 | ], 72 | "type": "Query", 73 | "abstractKey": null 74 | }, 75 | "kind": "Request", 76 | "operation": { 77 | "argumentDefinitions": [], 78 | "kind": "Operation", 79 | "name": "HomePageQuery_todosQuery", 80 | "selections": [ 81 | { 82 | "alias": null, 83 | "args": (v0/*: any*/), 84 | "concreteType": "TodoConnection", 85 | "kind": "LinkedField", 86 | "name": "todos", 87 | "plural": false, 88 | "selections": [ 89 | { 90 | "alias": null, 91 | "args": null, 92 | "concreteType": "TodoEdge", 93 | "kind": "LinkedField", 94 | "name": "edges", 95 | "plural": true, 96 | "selections": [ 97 | { 98 | "alias": null, 99 | "args": null, 100 | "kind": "ScalarField", 101 | "name": "cursor", 102 | "storageKey": null 103 | }, 104 | { 105 | "alias": null, 106 | "args": null, 107 | "concreteType": "Todo", 108 | "kind": "LinkedField", 109 | "name": "node", 110 | "plural": false, 111 | "selections": [ 112 | { 113 | "alias": null, 114 | "args": null, 115 | "kind": "ScalarField", 116 | "name": "id", 117 | "storageKey": null 118 | }, 119 | { 120 | "alias": null, 121 | "args": null, 122 | "kind": "ScalarField", 123 | "name": "content", 124 | "storageKey": null 125 | }, 126 | { 127 | "alias": null, 128 | "args": null, 129 | "kind": "ScalarField", 130 | "name": "isCompleted", 131 | "storageKey": null 132 | }, 133 | { 134 | "alias": null, 135 | "args": null, 136 | "kind": "ScalarField", 137 | "name": "createdAt", 138 | "storageKey": null 139 | }, 140 | { 141 | "alias": null, 142 | "args": null, 143 | "kind": "ScalarField", 144 | "name": "__typename", 145 | "storageKey": null 146 | } 147 | ], 148 | "storageKey": null 149 | } 150 | ], 151 | "storageKey": null 152 | }, 153 | { 154 | "alias": null, 155 | "args": null, 156 | "concreteType": "PageInfo", 157 | "kind": "LinkedField", 158 | "name": "pageInfo", 159 | "plural": false, 160 | "selections": [ 161 | { 162 | "alias": null, 163 | "args": null, 164 | "kind": "ScalarField", 165 | "name": "hasNextPage", 166 | "storageKey": null 167 | }, 168 | { 169 | "alias": null, 170 | "args": null, 171 | "kind": "ScalarField", 172 | "name": "hasPreviousPage", 173 | "storageKey": null 174 | }, 175 | { 176 | "alias": null, 177 | "args": null, 178 | "kind": "ScalarField", 179 | "name": "startCursor", 180 | "storageKey": null 181 | }, 182 | { 183 | "alias": null, 184 | "args": null, 185 | "kind": "ScalarField", 186 | "name": "endCursor", 187 | "storageKey": null 188 | } 189 | ], 190 | "storageKey": null 191 | }, 192 | { 193 | "kind": "ClientExtension", 194 | "selections": [ 195 | { 196 | "alias": null, 197 | "args": null, 198 | "kind": "ScalarField", 199 | "name": "__id", 200 | "storageKey": null 201 | } 202 | ] 203 | } 204 | ], 205 | "storageKey": "todos(first:4)" 206 | }, 207 | { 208 | "alias": null, 209 | "args": (v0/*: any*/), 210 | "filters": null, 211 | "handle": "connection", 212 | "key": "TodoList_todos", 213 | "kind": "LinkedHandle", 214 | "name": "todos" 215 | } 216 | ] 217 | }, 218 | "params": { 219 | "cacheID": "520b7e61fba84bb2e8f2c0a9ec4098e7", 220 | "id": null, 221 | "metadata": {}, 222 | "name": "HomePageQuery_todosQuery", 223 | "operationKind": "query", 224 | "text": "query HomePageQuery_todosQuery {\n ...TodoList_query\n}\n\nfragment TodoList_query on Query {\n todos(first: 4) {\n edges {\n cursor\n node {\n id\n ...Todo_todo\n __typename\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n}\n\nfragment Todo_todo on Todo {\n id\n content\n isCompleted\n createdAt\n}\n" 225 | } 226 | }; 227 | })(); 228 | (node as any).hash = 'd414982547124933900bb8b779edebf7'; 229 | export default node; 230 | -------------------------------------------------------------------------------- /src/pages/HomePage/index.tsx: -------------------------------------------------------------------------------- 1 | import { TodoList } from "../../components/TodoList"; 2 | import { Header } from "../../components/Header"; 3 | import { HomePageContainer } from "./styles"; 4 | import { graphql } from "babel-plugin-relay/macro"; 5 | import { useState } from "react"; 6 | import { CreateTodoModal } from "../../components/CreateTodoModal"; 7 | import { useLazyLoadQuery } from "react-relay"; 8 | import { HomePageQuery_todosQuery } from "./__generated__/HomePageQuery_todosQuery.graphql"; 9 | import Modal from "react-modal"; 10 | 11 | Modal.setAppElement("#root"); 12 | 13 | export const HomePage = () => { 14 | const [isModalOpen, setIsModalOpen] = useState(false); 15 | const query = useLazyLoadQuery( 16 | graphql` 17 | query HomePageQuery_todosQuery { 18 | ...TodoList_query 19 | } 20 | `, 21 | {}, 22 | { fetchPolicy: "store-or-network" } 23 | ); 24 | 25 | const handleOpenModal = () => { 26 | setIsModalOpen(true); 27 | }; 28 | 29 | return ( 30 | <> 31 |
32 | 33 | 38 | 39 | 40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /src/pages/HomePage/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const HomePageContainer = styled.div` 4 | display: flex; 5 | justify-content: center; 6 | align-items: center; 7 | `; -------------------------------------------------------------------------------- /src/pages/LoginPage/__generated__/LoginPage_authMutation.graphql.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // @ts-nocheck 4 | 5 | import { ConcreteRequest } from "relay-runtime"; 6 | 7 | export type AuthUserInput = { 8 | email: string; 9 | password: string; 10 | clientMutationId?: string | null; 11 | }; 12 | export type LoginPage_authMutationVariables = { 13 | input: AuthUserInput; 14 | }; 15 | export type LoginPage_authMutationResponse = { 16 | readonly AuthUserMutation: { 17 | readonly me: { 18 | readonly id: string; 19 | readonly username: string; 20 | readonly email: string; 21 | } | null; 22 | readonly token: string | null; 23 | readonly error: string | null; 24 | } | null; 25 | }; 26 | export type LoginPage_authMutation = { 27 | readonly response: LoginPage_authMutationResponse; 28 | readonly variables: LoginPage_authMutationVariables; 29 | }; 30 | 31 | 32 | 33 | /* 34 | mutation LoginPage_authMutation( 35 | $input: AuthUserInput! 36 | ) { 37 | AuthUserMutation(input: $input) { 38 | me { 39 | id 40 | username 41 | email 42 | } 43 | token 44 | error 45 | } 46 | } 47 | */ 48 | 49 | const node: ConcreteRequest = (function(){ 50 | var v0 = [ 51 | { 52 | "defaultValue": null, 53 | "kind": "LocalArgument", 54 | "name": "input" 55 | } 56 | ], 57 | v1 = [ 58 | { 59 | "alias": null, 60 | "args": [ 61 | { 62 | "kind": "Variable", 63 | "name": "input", 64 | "variableName": "input" 65 | } 66 | ], 67 | "concreteType": "AuthUserPayload", 68 | "kind": "LinkedField", 69 | "name": "AuthUserMutation", 70 | "plural": false, 71 | "selections": [ 72 | { 73 | "alias": null, 74 | "args": null, 75 | "concreteType": "User", 76 | "kind": "LinkedField", 77 | "name": "me", 78 | "plural": false, 79 | "selections": [ 80 | { 81 | "alias": null, 82 | "args": null, 83 | "kind": "ScalarField", 84 | "name": "id", 85 | "storageKey": null 86 | }, 87 | { 88 | "alias": null, 89 | "args": null, 90 | "kind": "ScalarField", 91 | "name": "username", 92 | "storageKey": null 93 | }, 94 | { 95 | "alias": null, 96 | "args": null, 97 | "kind": "ScalarField", 98 | "name": "email", 99 | "storageKey": null 100 | } 101 | ], 102 | "storageKey": null 103 | }, 104 | { 105 | "alias": null, 106 | "args": null, 107 | "kind": "ScalarField", 108 | "name": "token", 109 | "storageKey": null 110 | }, 111 | { 112 | "alias": null, 113 | "args": null, 114 | "kind": "ScalarField", 115 | "name": "error", 116 | "storageKey": null 117 | } 118 | ], 119 | "storageKey": null 120 | } 121 | ]; 122 | return { 123 | "fragment": { 124 | "argumentDefinitions": (v0/*: any*/), 125 | "kind": "Fragment", 126 | "metadata": null, 127 | "name": "LoginPage_authMutation", 128 | "selections": (v1/*: any*/), 129 | "type": "Mutation", 130 | "abstractKey": null 131 | }, 132 | "kind": "Request", 133 | "operation": { 134 | "argumentDefinitions": (v0/*: any*/), 135 | "kind": "Operation", 136 | "name": "LoginPage_authMutation", 137 | "selections": (v1/*: any*/) 138 | }, 139 | "params": { 140 | "cacheID": "b35e672874164ec6ac793d69226fd579", 141 | "id": null, 142 | "metadata": {}, 143 | "name": "LoginPage_authMutation", 144 | "operationKind": "mutation", 145 | "text": "mutation LoginPage_authMutation(\n $input: AuthUserInput!\n) {\n AuthUserMutation(input: $input) {\n me {\n id\n username\n email\n }\n token\n error\n }\n}\n" 146 | } 147 | }; 148 | })(); 149 | (node as any).hash = '29f48fd3965fe97767d10ad9e3e72ef7'; 150 | export default node; 151 | -------------------------------------------------------------------------------- /src/pages/LoginPage/index.tsx: -------------------------------------------------------------------------------- 1 | import { useFormik } from "formik"; 2 | import { Link, useHistory } from "react-router-dom"; 3 | import { LoginPageContainer, LoginPageForm, LoginpageLabel, LinkContainer } from "./styles"; 4 | import * as Yup from 'yup'; 5 | import { Input } from "../../components/Input"; 6 | import { BiLockAlt } from 'react-icons/bi'; 7 | import { FiMail } from 'react-icons/fi'; 8 | import { AiOutlineUser } from 'react-icons/ai'; 9 | import { FormButton } from "../../components/FormButton"; 10 | import { ErrorMessage } from "../../components/ErrorMessage"; 11 | import { useMutation } from "react-relay"; 12 | import { LoginPage_authMutation, LoginPage_authMutationResponse } from './__generated__/LoginPage_authMutation.graphql'; 13 | import { FormEvent } from "react"; 14 | import { useEffect } from "react"; 15 | import { ToastContainer, toast } from 'react-toastify'; 16 | import 'react-toastify/dist/ReactToastify.css'; 17 | import graphql from 'babel-plugin-relay/macro'; 18 | 19 | type Values = { 20 | email: string; 21 | password: string; 22 | }; 23 | 24 | export const LoginPage = () => { 25 | 26 | const [commit] = useMutation( 27 | graphql` 28 | mutation LoginPage_authMutation($input: AuthUserInput!) { 29 | AuthUserMutation(input: $input) { 30 | me { 31 | id 32 | username 33 | email 34 | } 35 | token 36 | error 37 | } 38 | } 39 | `, 40 | 41 | ); 42 | 43 | const history = useHistory(); 44 | 45 | useEffect(() => { 46 | const token = localStorage.getItem('@relayTodo:token'); 47 | if (token) { 48 | history.push('/home'); 49 | } 50 | }, [history]); 51 | 52 | const onSubmit = (values: Values) => { 53 | const config = { 54 | variables: { 55 | input: { 56 | email: values.email, 57 | password: values.password 58 | } 59 | }, 60 | onCompleted: ({ AuthUserMutation }: LoginPage_authMutationResponse) => { 61 | if (AuthUserMutation?.error) { 62 | toast(AuthUserMutation.error, { 63 | type: "error", 64 | position: "top-right", 65 | autoClose: 5000, 66 | hideProgressBar: false, 67 | closeOnClick: true, 68 | pauseOnHover: true, 69 | draggable: true, 70 | progress: undefined, 71 | }); 72 | return; 73 | } 74 | 75 | if (AuthUserMutation?.token) { 76 | localStorage.setItem('@relayTodo:token', AuthUserMutation.token); 77 | localStorage.setItem('@relayTodo:user', JSON.stringify(AuthUserMutation.me)); 78 | history.push('/home'); 79 | } 80 | }, 81 | }; 82 | commit(config); 83 | }; 84 | 85 | const formik = useFormik({ 86 | initialValues: { 87 | email: '', 88 | password: '' 89 | }, 90 | validationSchema: Yup.object({ 91 | email: Yup.string().required('E-mail required').email('Provide a valid e-mail'), 92 | password: Yup.string().required('Password required').min(6, 'Minimum of 6 characters') 93 | }), 94 | onSubmit 95 | }); 96 | 97 | const handleSubmitGambiarra = (e: FormEvent) => { 98 | e.preventDefault(); 99 | formik.handleSubmit(); 100 | }; 101 | 102 | return ( 103 | 104 | 105 | 106 | E-mail 107 | 117 | {formik.touched.email && formik.errors.email ? ( 118 | {formik.errors.email} 119 | ) : 120 | null 121 | } 122 | 123 | Password 124 | 134 | {formik.touched.password && formik.errors.password ? ( 135 | {formik.errors.password} 136 | ) : 137 | null 138 | } 139 | Sign in 140 | 141 | 142 | Don't have an account? 143 | sign up 144 | 145 | 146 | 147 | 148 | 149 | 150 | ); 151 | }; -------------------------------------------------------------------------------- /src/pages/LoginPage/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const LoginPageContainer = styled.div` 4 | height: 100vh; 5 | border: 3px; 6 | display: flex; 7 | justify-content: center; 8 | align-items: center; 9 | `; 10 | 11 | export const LoginPageForm = styled.form` 12 | display: flex; 13 | justify-content: center; 14 | flex-direction: column; 15 | width: 400px; 16 | height: 300px; 17 | padding: 16px; 18 | box-shadow: 0 3px 10px rgb(0 0 0 / 0.2); 19 | `; 20 | 21 | export const LoginpageLabel = styled.label` 22 | font-size: 24px; 23 | font-weight: 500; 24 | `; 25 | 26 | export const LinkContainer = styled.div` 27 | display: flex; 28 | justify-content: center; 29 | 30 | a { 31 | display: flex; 32 | align-items: center; 33 | color: #000; 34 | text-decoration: none; 35 | 36 | &:hover { 37 | text-decoration: underline; 38 | text-decoration-color: var(--relay-orange); 39 | } 40 | 41 | span { 42 | color: var(--relay-orange); 43 | margin-left: 4px; 44 | } 45 | 46 | svg { 47 | color: var(--relay-orange); 48 | margin-left: 2px; 49 | } 50 | } 51 | `; 52 | -------------------------------------------------------------------------------- /src/pages/SignUpPage/__generated__/SignUpPage_CreateUserMutation.graphql.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // @ts-nocheck 4 | 5 | import { ConcreteRequest } from "relay-runtime"; 6 | 7 | export type CreateUserInput = { 8 | username: string; 9 | email: string; 10 | password: string; 11 | clientMutationId?: string | null; 12 | }; 13 | export type SignUpPage_CreateUserMutationVariables = { 14 | input: CreateUserInput; 15 | }; 16 | export type SignUpPage_CreateUserMutationResponse = { 17 | readonly CreateUserMutation: { 18 | readonly me: { 19 | readonly id: string; 20 | readonly username: string; 21 | readonly email: string; 22 | } | null; 23 | readonly token: string | null; 24 | readonly error: string | null; 25 | } | null; 26 | }; 27 | export type SignUpPage_CreateUserMutation = { 28 | readonly response: SignUpPage_CreateUserMutationResponse; 29 | readonly variables: SignUpPage_CreateUserMutationVariables; 30 | }; 31 | 32 | 33 | 34 | /* 35 | mutation SignUpPage_CreateUserMutation( 36 | $input: CreateUserInput! 37 | ) { 38 | CreateUserMutation(input: $input) { 39 | me { 40 | id 41 | username 42 | email 43 | } 44 | token 45 | error 46 | } 47 | } 48 | */ 49 | 50 | const node: ConcreteRequest = (function(){ 51 | var v0 = [ 52 | { 53 | "defaultValue": null, 54 | "kind": "LocalArgument", 55 | "name": "input" 56 | } 57 | ], 58 | v1 = [ 59 | { 60 | "alias": null, 61 | "args": [ 62 | { 63 | "kind": "Variable", 64 | "name": "input", 65 | "variableName": "input" 66 | } 67 | ], 68 | "concreteType": "CreateUserPayload", 69 | "kind": "LinkedField", 70 | "name": "CreateUserMutation", 71 | "plural": false, 72 | "selections": [ 73 | { 74 | "alias": null, 75 | "args": null, 76 | "concreteType": "User", 77 | "kind": "LinkedField", 78 | "name": "me", 79 | "plural": false, 80 | "selections": [ 81 | { 82 | "alias": null, 83 | "args": null, 84 | "kind": "ScalarField", 85 | "name": "id", 86 | "storageKey": null 87 | }, 88 | { 89 | "alias": null, 90 | "args": null, 91 | "kind": "ScalarField", 92 | "name": "username", 93 | "storageKey": null 94 | }, 95 | { 96 | "alias": null, 97 | "args": null, 98 | "kind": "ScalarField", 99 | "name": "email", 100 | "storageKey": null 101 | } 102 | ], 103 | "storageKey": null 104 | }, 105 | { 106 | "alias": null, 107 | "args": null, 108 | "kind": "ScalarField", 109 | "name": "token", 110 | "storageKey": null 111 | }, 112 | { 113 | "alias": null, 114 | "args": null, 115 | "kind": "ScalarField", 116 | "name": "error", 117 | "storageKey": null 118 | } 119 | ], 120 | "storageKey": null 121 | } 122 | ]; 123 | return { 124 | "fragment": { 125 | "argumentDefinitions": (v0/*: any*/), 126 | "kind": "Fragment", 127 | "metadata": null, 128 | "name": "SignUpPage_CreateUserMutation", 129 | "selections": (v1/*: any*/), 130 | "type": "Mutation", 131 | "abstractKey": null 132 | }, 133 | "kind": "Request", 134 | "operation": { 135 | "argumentDefinitions": (v0/*: any*/), 136 | "kind": "Operation", 137 | "name": "SignUpPage_CreateUserMutation", 138 | "selections": (v1/*: any*/) 139 | }, 140 | "params": { 141 | "cacheID": "b6426aff3055e2fb4bf2c5b2212fcee7", 142 | "id": null, 143 | "metadata": {}, 144 | "name": "SignUpPage_CreateUserMutation", 145 | "operationKind": "mutation", 146 | "text": "mutation SignUpPage_CreateUserMutation(\n $input: CreateUserInput!\n) {\n CreateUserMutation(input: $input) {\n me {\n id\n username\n email\n }\n token\n error\n }\n}\n" 147 | } 148 | }; 149 | })(); 150 | (node as any).hash = 'f0adec41d23dfc1edf78b83f70d164e9'; 151 | export default node; 152 | -------------------------------------------------------------------------------- /src/pages/SignUpPage/index.tsx: -------------------------------------------------------------------------------- 1 | import { Input } from "../../components/Input"; 2 | import { SignUpPageContainer, SignUpPageForm, SignUpPageLabel, LinkContainer } from "./styles"; 3 | import { AiOutlineUser } from 'react-icons/ai'; 4 | import { FiLogIn } from 'react-icons/fi'; 5 | import { FormButton } from "../../components/FormButton"; 6 | import { useFormik } from "formik"; 7 | import { useMutation } from "react-relay"; 8 | import * as Yup from 'yup'; 9 | import graphql from 'babel-plugin-relay/macro'; 10 | import { ErrorMessage } from "../../components/ErrorMessage"; 11 | import { Link, useHistory } from "react-router-dom"; 12 | import { SignUpPage_CreateUserMutation, SignUpPage_CreateUserMutationResponse } from "./__generated__/SignUpPage_CreateUserMutation.graphql"; 13 | import { FormEvent, useEffect } from "react"; 14 | import { FiMail } from "react-icons/fi"; 15 | import { BiLockAlt } from "react-icons/bi"; 16 | import { ToastContainer, toast } from 'react-toastify'; 17 | import 'react-toastify/dist/ReactToastify.css'; 18 | 19 | type Values = { 20 | email: string; 21 | username: string; 22 | password: string; 23 | }; 24 | 25 | export const SignUpPage = () => { 26 | 27 | const [commit] = useMutation( 28 | graphql` 29 | mutation SignUpPage_CreateUserMutation($input: CreateUserInput!) { 30 | CreateUserMutation(input: $input) { 31 | me { 32 | id 33 | username 34 | email 35 | } 36 | token 37 | error 38 | } 39 | } 40 | `, 41 | 42 | ); 43 | 44 | const history = useHistory(); 45 | 46 | useEffect(() => { 47 | const token = localStorage.getItem('@relayTodo:token'); 48 | if (token) { 49 | history.push('/home'); 50 | } 51 | }, [history]); 52 | 53 | const onSubmit = (values: Values) => { 54 | const config = { 55 | variables: { 56 | input: { 57 | email: values.email, 58 | username: values.username, 59 | password: values.password 60 | } 61 | }, 62 | onCompleted: ({ CreateUserMutation }: SignUpPage_CreateUserMutationResponse) => { 63 | if (CreateUserMutation?.error) { 64 | toast(CreateUserMutation.error, { 65 | type: "error", 66 | position: "top-right", 67 | autoClose: 5000, 68 | hideProgressBar: false, 69 | closeOnClick: true, 70 | pauseOnHover: true, 71 | draggable: true, 72 | progress: undefined, 73 | }); 74 | return; 75 | } 76 | 77 | if (CreateUserMutation?.token) { 78 | localStorage.setItem('@relayTodo:token', CreateUserMutation.token); 79 | localStorage.setItem('@relayTodo:user', JSON.stringify(CreateUserMutation.me)); 80 | history.push('/home'); 81 | } 82 | }, 83 | }; 84 | commit(config); 85 | }; 86 | 87 | const formik = useFormik({ 88 | initialValues: { 89 | email: '', 90 | username: '', 91 | password: '' 92 | }, 93 | validationSchema: Yup.object({ 94 | email: Yup.string().required('E-mail required').email('Provide a valid e-mail'), 95 | username: Yup.string().required('Username required'), 96 | password: Yup.string().required('Password required').min(6, 'Minimum of 6 characters') 97 | }), 98 | onSubmit 99 | }); 100 | 101 | const handleSubmitGambiarra = (e: FormEvent) => { 102 | e.preventDefault(); 103 | formik.handleSubmit(); 104 | }; 105 | 106 | return ( 107 | 108 | 109 | Email 110 | 120 | {formik.touched.email && formik.errors.email ? ( 121 | {formik.errors.email} 122 | ) : 123 | null 124 | } 125 | Username 126 | 136 | {formik.touched.username && formik.errors.username ? ( 137 | {formik.errors.username} 138 | ) : 139 | null 140 | } 141 | Password 142 | 152 | {formik.touched.password && formik.errors.password ? ( 153 | {formik.errors.password} 154 | ) : 155 | null 156 | } 157 | 158 | { }}>Submit 159 | 160 | 161 | Already have an account? 162 | sign in 163 | 164 | 165 | 166 | 167 | 168 | 169 | ); 170 | }; -------------------------------------------------------------------------------- /src/pages/SignUpPage/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const SignUpPageContainer = styled.div` 4 | height: 100vh; 5 | border: 3px; 6 | display: flex; 7 | justify-content: center; 8 | align-items: center; 9 | `; 10 | 11 | export const SignUpPageForm = styled.form` 12 | display: flex; 13 | justify-content: center; 14 | flex-direction: column; 15 | width: 400px; 16 | height: 400px; 17 | padding: 16px; 18 | box-shadow: 0 3px 10px rgb(0 0 0 / 0.2); 19 | `; 20 | 21 | export const SignUpPageLabel = styled.label` 22 | font-size: 24px; 23 | font-weight: 500; 24 | `; 25 | 26 | export const LinkContainer = styled.div` 27 | display: flex; 28 | justify-content: center; 29 | 30 | a { 31 | display: flex; 32 | align-items: center; 33 | color: #000; 34 | text-decoration: none; 35 | 36 | &:hover { 37 | text-decoration: underline; 38 | text-decoration-color: var(--relay-orange); 39 | } 40 | 41 | span { 42 | color: var(--relay-orange); 43 | margin-left: 4px; 44 | } 45 | 46 | svg { 47 | color: var(--relay-orange); 48 | margin-left: 2px; 49 | } 50 | } 51 | `; 52 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/relay/RelayEnviroment.tsx: -------------------------------------------------------------------------------- 1 | import { Environment, Network, RecordSource, Store } from "relay-runtime"; 2 | 3 | const getToken = () => { 4 | return localStorage.getItem("@relayTodo:token"); 5 | }; 6 | 7 | const fetchQuery = (operation: any, variables: any) => { 8 | const authorization = `Bearer ${getToken()}`; 9 | return fetch("http://localhost:8080/graphql", { 10 | method: "POST", 11 | headers: { 12 | "content-type": "application/json", 13 | Authorization: authorization, 14 | }, 15 | body: JSON.stringify({ 16 | query: operation.text, 17 | variables, 18 | }), 19 | }).then((response) => { 20 | return response.json(); 21 | }); 22 | }; 23 | 24 | const network = Network.create(fetchQuery); 25 | 26 | const store = new Store(new RecordSource()); 27 | 28 | export default new Environment({ 29 | network, 30 | store, 31 | }); 32 | -------------------------------------------------------------------------------- /src/routes/index.tsx: -------------------------------------------------------------------------------- 1 | import { Switch, Route } from 'react-router-dom'; 2 | import { HomePage } from '../pages/HomePage'; 3 | import { LoginPage } from '../pages/LoginPage'; 4 | import { SignUpPage } from '../pages/SignUpPage'; 5 | 6 | export const Routes = () => { 7 | return ( 8 | 9 | 10 | 11 | 12 | 13 | ); 14 | }; -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /src/styles/global.ts: -------------------------------------------------------------------------------- 1 | import { createGlobalStyle } from "styled-components"; 2 | 3 | export const GlobalStyle = createGlobalStyle` 4 | :root { 5 | --relay-orange: #f26b00; 6 | } 7 | 8 | * { 9 | margin: 0; 10 | padding: 0; 11 | box-sizing: border-box; 12 | } 13 | 14 | body, input, textarea, button { 15 | font-family: 'Poppins', sans-serif; 16 | font-weight: 400; 17 | } 18 | 19 | h1, h1, h3, h4, h5, h6, strong { 20 | font-weight: 600; 21 | } 22 | 23 | button, a { 24 | cursor: pointer; 25 | } 26 | 27 | .react-modal-overlay { 28 | background: rgba(0, 0, 0, 0.5); 29 | position: fixed; 30 | top: 0; 31 | bottom: 0; 32 | right: 0; 33 | left: 0; 34 | 35 | display: flex; 36 | align-items: center; 37 | justify-content: center; 38 | } 39 | 40 | .react-modal-content { 41 | width: 100%; 42 | max-width: 576px; 43 | background: #f0f2f5; 44 | padding: 3rem; 45 | position: relative; 46 | border-radius: 0.24rem; 47 | } 48 | `; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": false, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } --------------------------------------------------------------------------------