Skip to content

Commit

Permalink
Add authors page
Browse files Browse the repository at this point in the history
  • Loading branch information
juffalow committed Nov 23, 2020
1 parent 7bcef9d commit 4aba467
Show file tree
Hide file tree
Showing 17 changed files with 2,146 additions and 3 deletions.
4 changes: 3 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@ import ForkMe from 'components/ForkMe';
import Footer from 'components/Footer';
import Home from 'pages/Home';
import Quotes from 'pages/Quotes';
import Authors from 'pages/Authors';

const App = () => (
<Router>
<>
<Menu />
<ForkMe />
<main role="main" className="flex-shrink-0" style={{ marginTop: 25, marginBottom: 25 }}>
<main role="main" className="flex-shrink-0" style={{ marginTop: 25, marginBottom: 81 }}>
<Route path="/quotes" component={Quotes} />
<Route path="/authors" component={Authors} />
<Route path="/" component={Home} exact />
</main>
<Footer />
Expand Down
2 changes: 1 addition & 1 deletion src/components/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Row from 'react-bootstrap/Row';
import Col from 'react-bootstrap/Col';

const Footer = () => (
<footer style={{ width: '100%', backgroundColor: '#222', paddingTop: '20px', paddingBottom: '20px', color: '#fff' }}>
<footer className="footer mt-auto py-3 bg-dark fixed-bottom" style={{ color: '#fff' }}>
<Container fluid={true}>
<Row>
<Col>
Expand Down
23 changes: 23 additions & 0 deletions src/pages/Authors.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React from 'react';
import Container from 'react-bootstrap/Container';
import Row from 'react-bootstrap/Row';
import Col from 'react-bootstrap/Col';
import SEO from 'components/SEO';
import AuthorsContainer from './authors/AuthorsContainer';

const Authors: React.FC = () => {
return (
<SEO title="Authors" description="List of Authors implemented in React and Relay.">
<Container>
<Row>
<Col>
<h1>Authors</h1>
</Col>
</Row>
<AuthorsContainer />
</Container>
</SEO>
);
};

export default Authors;
4 changes: 3 additions & 1 deletion src/pages/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,17 @@ import Container from 'react-bootstrap/Container';
import Row from 'react-bootstrap/Row';
import Col from 'react-bootstrap/Col';
import SEO from 'components/SEO';
import HomeContainer from './home/HomeContainer';

const Home: React.FC = () => (
<SEO title="Quotes" description="">
<Container>
<Row>
<Col>
<h1>Quotes - React &amp; Relay examole project</h1>
<p className="lead">The whole page is created with <a href="https://create-react-app.dev/" rel="noopener noreferrer" target="_blank">create-react-app</a> without eject. It uses newest version of <a href="https://reactjs.org/" rel="noopener noreferrer" target="_blank">React</a> (<i>16.13.1</i>) and <a href="https://relay.dev/" rel="noopener noreferrer" target="_blank">Relay</a> (<i>9.0.0</i>) that is now available (<i>27.3.2020</i>) and everything is coded in <a href="https://www.typescriptlang.org/" rel="noopener noreferrer" target="_blank">TypeScript</a>. You can find here how to use <code>QueryRenderer</code>, <code>createPaginationContainer</code> and <code>createFragmentContainer</code>. Backend is available in <a href="https://github.com/juffalow/slim-graphql-eloquent-example" rel="noopener noreferrer" target="_blank">PHP</a> and <a href="https://github.com/juffalow/express-graphql-example" rel="noopener noreferrer" target="_blank">NodeJS</a>.</p>
<p className="lead">The whole page is created with <a href="https://create-react-app.dev/" rel="noopener noreferrer" target="_blank">create-react-app</a> without eject. It uses newest version of <a href="https://reactjs.org/" rel="noopener noreferrer" target="_blank">React</a> (<i>17.0.1</i>) and <a href="https://relay.dev/" rel="noopener noreferrer" target="_blank">Relay</a> (<i>10.0.0</i>) that is now available (<i>21.11.2020</i>) and everything is coded in <a href="https://www.typescriptlang.org/" rel="noopener noreferrer" target="_blank">TypeScript</a>. You can find here how to use <code>QueryRenderer</code>, <code>createPaginationContainer</code> and <code>createFragmentContainer</code>. Backend is available in <a href="https://github.com/juffalow/slim-graphql-eloquent-example" rel="noopener noreferrer" target="_blank">PHP</a> and <a href="https://github.com/juffalow/express-graphql-example" rel="noopener noreferrer" target="_blank">NodeJS</a>.</p>
<p>This project was created because of lot of hours spent searching, trying and debugging. There are several articles and projects about Relay, but those are mostly using older versions, or it is just a small sample of a code without other major parts. Here is a complete web application with everything you need to start using React, Relay and GraphQL.</p>
<HomeContainer />
<h2>Want to contribute?</h2>
<p>I am sure you can find several things that can be coded better, or are not clear. And it really needs tests to be a fully complete project. So if you have time, I would be very happy if you can write me some feedback, or even create a pull request.</p>
</Col>
Expand Down
28 changes: 28 additions & 0 deletions src/pages/authors/AuthorRow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React from 'react'
import { createFragmentContainer } from 'react-relay';
import { graphql } from 'babel-plugin-relay/macro';
import { Author } from '../../types/Author';

interface Props {
author: Author;
}

const AuthorRow = ({ author }: Props) => (
<>
<td>{author._id}</td>
<td>{author.firstName}</td>
<td>{author.lastName}</td>
</>
);

const AuthorRowFragmentContainer = createFragmentContainer(AuthorRow, {
author: graphql`
fragment AuthorRow_author on Author {
_id
firstName
lastName
}
`
});

export default AuthorRowFragmentContainer;
145 changes: 145 additions & 0 deletions src/pages/authors/AuthorsContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import React from 'react';
import Alert from 'react-bootstrap/Alert';
import Spinner from 'react-bootstrap/Spinner';
import Button from 'react-bootstrap/Button';
import Row from 'react-bootstrap/Row';
import Col from 'react-bootstrap/Col';
import { QueryRenderer, createPaginationContainer } from 'react-relay';
import { graphql } from 'babel-plugin-relay/macro';
import environment from 'environment';
import AuthorsTable from './AuthorsTable';
import Filter from './Filter';
import { Author } from '../../types/Author';

interface Props {
authors: {
authors: {
edges: Array<{
node: Author
}>
},
};
relay: any;
}

const refetch = (relay: any, values: any) => {
relay.refetchConnection(
10,
(error: any) => {
console.log(error);
},
values,
);
};

const Authors: React.FC<Props> = (props: Props) => (
<>
<Row className="mt-4 mb-4">
<Col>
<Filter onUpdate={(values: any) => refetch(props.relay, values)} />
</Col>
</Row>
<AuthorsTable authors={props.authors.authors.edges.map(edge => edge.node)} />
<Row className="mt-4">
<Col>
{
props.relay.hasMore() &&
<Button onClick={() => props.relay.loadMore(9, null)}>Load more</Button>
}
</Col>
</Row>
</>
);

const AuthorsContainer = createPaginationContainer(
Authors,
{
authors: graphql`
fragment AuthorsContainer_authors on Query {
authors (
first: $first
after: $after
firstName: $firstName
lastName: $lastName
orderBy: $orderBy
) @connection(key: "AuthorsTable_authors") {
totalCount
edges {
node {
...AuthorsTable_authors
}
}
pageInfo {
startCursor
endCursor
hasNextPage
hasPreviousPage
}
}
}`
},
{
direction: 'forward',
getConnectionFromProps(props: any) {
return props.authors.authors;
},
getFragmentVariables(prevVars: any, totalCount: number) {
return {
...prevVars,
count: totalCount,
};
},
getVariables(props: any, { count, cursor }, fragmentVariables: any) {
return {
first: count,
after: cursor,
firstName: fragmentVariables.firstName,
lastName: fragmentVariables.lastName,
orderBy: fragmentVariables.orderBy,
};
},
query: graphql`
query AuthorsContainerPaginationQuery(
$first: Int!
$after: String
$firstName: String
$lastName: String
$orderBy: [AuthorsOrder]
) {
...AuthorsContainer_authors
}
`
}
);

export default function RelayAuthorsContainer() {
return (
<QueryRenderer
environment={environment}
query={graphql`
query AuthorsContainerQuery($first: Int, $after: String, $firstName: String, $lastName: String, $orderBy: [AuthorsOrder]) {
...AuthorsContainer_authors
}
`}
variables={{ first: 10, after: null, firstName: null, lastName: null, orderBy: [{ field: 'ID', direction: 'ASC' }] }}
render={({error, props}: any) => {

if (error) {
return (
<Alert variant={'danger'}>
Unexpected error occured! Please contact maintainer or try again later.
</Alert>
);
}

if (!props) {
return ( <Spinner animation="border" /> );
}

return (
<AuthorsContainer authors={props} />
);
}}
/>
);
}
42 changes: 42 additions & 0 deletions src/pages/authors/AuthorsTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React from 'react'
import Table from 'react-bootstrap/Table';
import { createFragmentContainer } from 'react-relay';
import { graphql } from 'babel-plugin-relay/macro';
import AuthorRow from './AuthorRow';
import { Author } from '../../types/Author';

interface Props {
authors: Array<Author>;
}

const AuthorsTable = (props: Props) => (
<Table striped bordered responsive hover>
<thead>
<tr>
<th>#</th>
<th>First name</th>
<th>Last name</th>
</tr>
</thead>
<tbody>
{
props.authors.map(author => (
<tr key={author.id}>
<AuthorRow author={author} />
</tr>
))
}
</tbody>
</Table>
);

const AuthorsTableFragmentContainer = createFragmentContainer(AuthorsTable, {
authors: graphql`
fragment AuthorsTable_authors on Author @relay(plural: true) {
id
...AuthorRow_author
}
`
});

export default AuthorsTableFragmentContainer;
90 changes: 90 additions & 0 deletions src/pages/authors/Filter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import React, { useState } from 'react';
import Form from 'react-bootstrap/Form';
import FormGroup from 'react-bootstrap/FormGroup';

interface Props {
onUpdate: (values: any) => void;
}

const Filter: React.FC<Props> = (props: Props) => {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [orderField, setOrderField] = useState('ID');
const [orderDirection, setOrderDirection] = useState('ASC');

const handleFirstNameChange = (e: any) => {
const { value } = e.target;

setFirstName(value);
update({ firstName: value.length === 0 ? null : value });
};

const handleLastNameChange = (e: any) => {
const { value } = e.target;

setLastName(value);
update({ lastName: value.length === 0 ? null : value });
};

const handleOrderFieldChange = (e: any) => {
const { value } = e.target;

setOrderField(value);
update({ orderBy:[{ field: value, direction: orderDirection }] });
};

const handleOrderDirectionChange = (e: any) => {
const { value } = e.target;

setOrderDirection(value);
update({ orderBy:[{ field: orderField, direction: value }] });
};

const update = (values: any) => {
props.onUpdate({
...{
firstName: firstName.length === 0 ? null : firstName,
lastName: lastName.length === 0 ? null : lastName,
orderBy: [{ field: orderField, direction: orderDirection }],
}, ...values
});
};

return (
<Form inline>
<FormGroup controlId="firstName">
<Form.Label>First name:</Form.Label>
<Form.Control
type="text"
placeholder=""
value={firstName}
onChange={handleFirstNameChange}
/>
</FormGroup>
<FormGroup controlId="lastName">
<Form.Label>Last name:</Form.Label>
<Form.Control
type="text"
placeholder=""
value={lastName}
onChange={handleLastNameChange}
/>
</FormGroup>
<FormGroup controlId="orderField">
<Form.Label>Order by:</Form.Label>
<Form.Control as="select" placeholder="field" value={orderField} onChange={handleOrderFieldChange}>
<option value="ID">ID</option>
<option value="CREATED_AT">CREATED_AT</option>
</Form.Control>
</FormGroup>
<FormGroup controlId="orderDirection">
<Form.Control as="select" placeholder="direction" value={orderDirection} onChange={handleOrderDirectionChange}>
<option value="ASC">ASC</option>
<option value="DESC">DESC</option>
</Form.Control>
</FormGroup>
</Form>
);
};

export default Filter;
Loading

0 comments on commit 4aba467

Please sign in to comment.