├── .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 | }
--------------------------------------------------------------------------------