├── .gitignore
├── README.md
├── components
└── playersList.js
├── lib
└── apolloClient.js
├── package.json
├── pages
├── _app.js
├── index.js
└── table.js
├── public
├── favicon.ico
└── vercel.svg
├── schema.graphql
├── styles
├── Home.module.css
└── globals.css
└── yarn.lock
/.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 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env.local
29 | .env.development.local
30 | .env.test.local
31 | .env.production.local
32 |
33 | # vercel
34 | .vercel
35 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
2 |
3 | ## Getting Started
4 |
5 | This app was created for [Building a Next.js App with Apollo Client & Slash GraphQL](https://www.apollographql.com/blog/building-a-next-js-app-with-apollo-client-slash-graphql/) blog.
6 |
7 | Open `lib/apolloClient.js` and add your Slash GraphQL endpoint.
8 | Open `pages/table.js` and add your API key for [apifootball](https://apifootball.com), if you want to view the EPL table.
9 |
10 | First, run the development server:
11 |
12 | ```bash
13 | npm run dev
14 | # or
15 | yarn dev
16 | ```
17 |
18 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
19 |
20 | You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file.
21 |
22 | ## Learn More
23 |
24 | To learn more about Next.js, take a look at the following resources:
25 |
26 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
27 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
28 |
29 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
30 |
31 | ## Deploy on Vercel
32 |
33 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/import?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
34 |
35 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
36 |
--------------------------------------------------------------------------------
/components/playersList.js:
--------------------------------------------------------------------------------
1 | import { gql, useQuery, useLazyQuery } from "@apollo/client";
2 | import TextField from "@material-ui/core/TextField";
3 | import Autocomplete from "@material-ui/lab/Autocomplete";
4 | import { makeStyles } from "@material-ui/core/styles";
5 | import Card from "@material-ui/core/Card";
6 | import Grid from "@material-ui/core/Grid";
7 | import CardContent from "@material-ui/core/CardContent";
8 | import Button from "@material-ui/core/Button";
9 | import Typography from "@material-ui/core/Typography";
10 | import { useState } from "react";
11 |
12 | const useStyles = makeStyles({
13 | root: {
14 | minWidth: 275,
15 | },
16 | bullet: {
17 | display: "inline-block",
18 | margin: "0 2px",
19 | transform: "scale(0.8)",
20 | },
21 | title: {
22 | fontSize: 18,
23 | },
24 | pos: {
25 | marginBottom: 12,
26 | fontSize: 12,
27 | },
28 | });
29 |
30 | const ALL_PLAYERS_QUERY = gql`
31 | query allPlayers {
32 | queryPlayer {
33 | name
34 | position
35 | country {
36 | id
37 | name
38 | stadium
39 | }
40 | club {
41 | id
42 | name
43 | stadium
44 | }
45 | id
46 | }
47 | }
48 | `;
49 |
50 | export const ALL_COUNTRIES_QUERY = gql`
51 | query allCountries {
52 | queryCountry {
53 | id
54 | name
55 | }
56 | }
57 | `;
58 |
59 | export const ALL_CLUBS_QUERY = gql`
60 | query allClubs {
61 | queryClub {
62 | id
63 | name
64 | }
65 | }
66 | `;
67 |
68 | const FILTER_PLAYERS_QUERY = gql`
69 | query filterPlayers(
70 | $filter: PlayerFilter
71 | $countryID: [ID!]
72 | $clubID: [ID!]
73 | ) {
74 | queryPlayer(filter: $filter) @cascade {
75 | name
76 | position
77 | country(filter: { id: $countryID }) {
78 | id
79 | name
80 | }
81 | club(filter: { id: $clubID }) {
82 | id
83 | name
84 | }
85 | id
86 | }
87 | }
88 | `;
89 |
90 | export default function PlayersList() {
91 | const [country, setCountry] = useState(null);
92 | const [club, setClub] = useState(null);
93 | const [position, setPosition] = useState(null);
94 | const [searchText, setSearchText] = useState("");
95 | const [searchStatus, setSearchStatus] = useState(false);
96 | const classes = useStyles();
97 | const { loading, error, data } = useQuery(ALL_PLAYERS_QUERY);
98 | const {
99 | loading: loadingCountries,
100 | error: errCountries,
101 | data: countries,
102 | } = useQuery(ALL_COUNTRIES_QUERY);
103 | const { loading: loadingClubs, error: errClubs, data: clubs } = useQuery(
104 | ALL_CLUBS_QUERY
105 | );
106 | const [
107 | getFilteredPlayers,
108 | { loading: filterLoading, data: filteredPlayers, error: filterError },
109 | ] = useLazyQuery(FILTER_PLAYERS_QUERY);
110 |
111 | if (error || errCountries || errClubs || filterError)
112 | return
Error loading players.
;
113 | if (loading || loadingCountries || loadingClubs || filterLoading)
114 | return Loading
;
115 |
116 | const { queryPlayer: allPlayers } = data;
117 | const { queryCountry: allCountries } = countries;
118 | const { queryClub: allClubs } = clubs;
119 |
120 | const positions = [
121 | "GK",
122 | "RB",
123 | "LB",
124 | "CB",
125 | "DM",
126 | "CM",
127 | "LM",
128 | "RM",
129 | "CF",
130 | "ST",
131 | ];
132 |
133 | const clearSearch = () => {
134 | setClub(null);
135 | setCountry(null);
136 | setPosition(null);
137 | setSearchText("");
138 | setSearchStatus(false);
139 | };
140 |
141 | const searchPlayers = () => {
142 | let filter = {};
143 | setSearchStatus(true);
144 | if (position) {
145 | filter.position = { eq: position };
146 | }
147 | if (searchText !== "") {
148 | filter.name = { anyoftext: searchText };
149 | }
150 | if (Object.keys(filter).length === 0) {
151 | if (!club && !country) {
152 | setSearchStatus(false);
153 | return;
154 | }
155 | }
156 | getFilteredPlayers({
157 | variables: {
158 | filter: filter,
159 | clubID: club ? [club] : allClubs.map((club) => club.id), // if no club is selected then return all clubs id
160 | countryID: country
161 | ? [country.id]
162 | : allCountries.map((country) => country.id), // if no country is selected then return all countries id
163 | },
164 | });
165 | };
166 |
167 | const dataset =
168 | searchStatus && filteredPlayers ? filteredPlayers?.queryPlayer : allPlayers;
169 |
170 | return (
171 |
172 |
173 |
option.name}
177 | value={country}
178 | style={{ width: 300 }}
179 | renderInput={(params) => (
180 |
181 | )}
182 | onChange={(e, value) =>
183 | value
184 | ? setCountry({
185 | id: value.id,
186 | name: value.name,
187 | })
188 | : setCountry(null)
189 | }
190 | />
191 | option.name}
196 | style={{ width: 300, marginLeft: "10px" }}
197 | renderInput={(params) => (
198 |
199 | )}
200 | onChange={(e, value) =>
201 | value ? setClub({ name: value.name, id: value.id }) : setClub(null)
202 | }
203 | />
204 | option}
209 | style={{ width: 200, marginLeft: "10px" }}
210 | renderInput={(params) => (
211 |
212 | )}
213 | onChange={(e, value) => setPosition(value)}
214 | />
215 | setSearchText(event.target.value)}
222 | />
223 |
231 | {searchStatus && (
232 |
240 | )}
241 |
242 |
243 | {dataset.map((player) => (
244 |
245 |
246 |
247 |
252 | {player.name}
253 |
254 |
255 | {player.club.name}
256 |
257 |
258 | Position - {player.position}
259 |
260 | Country - {player.country.name}
261 |
262 |
263 |
264 |
265 | ))}
266 |
267 |
268 | );
269 | }
270 |
--------------------------------------------------------------------------------
/lib/apolloClient.js:
--------------------------------------------------------------------------------
1 | import { useMemo } from "react";
2 | import { ApolloClient, HttpLink, InMemoryCache } from "@apollo/client";
3 |
4 | let apolloClient;
5 |
6 | function createApolloClient() {
7 | return new ApolloClient({
8 | ssrMode: typeof window === "undefined",
9 | link: new HttpLink({
10 | uri: "YOUR-SLASH-ENDPOINT", // Add your Slash endpoint here
11 | }),
12 | cache: new InMemoryCache(),
13 | });
14 | }
15 |
16 | export function initializeApollo(initialState = null) {
17 | const _apolloClient = apolloClient ?? createApolloClient();
18 |
19 | // If your page has Next.js data fetching methods that use Apollo Client, the initial state
20 | // gets hydrated here
21 | if (initialState) {
22 | // Get existing cache, loaded during client side data fetching
23 | const existingCache = _apolloClient.extract();
24 | // Restore the cache using the data passed from getStaticProps/getServerSideProps
25 | // combined with the existing cached data
26 | _apolloClient.cache.restore({ ...existingCache, ...initialState });
27 | }
28 | // For SSG and SSR always create a new Apollo Client
29 | if (typeof window === "undefined") return _apolloClient;
30 | // Create the Apollo Client once in the client
31 | if (!apolloClient) apolloClient = _apolloClient;
32 | return _apolloClient;
33 | }
34 |
35 | export function useApollo(initialState) {
36 | const store = useMemo(() => initializeApollo(initialState), [initialState]);
37 | return store;
38 | }
39 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "epl-players",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start"
9 | },
10 | "dependencies": {
11 | "@apollo/client": "^3.2.2",
12 | "@material-ui/core": "^4.11.0",
13 | "@material-ui/lab": "^4.0.0-alpha.56",
14 | "graphql": "^15.3.0",
15 | "next": "9.5.3",
16 | "react": "16.13.1",
17 | "react-dom": "16.13.1"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/pages/_app.js:
--------------------------------------------------------------------------------
1 | import { ApolloProvider } from "@apollo/client";
2 | import { useApollo } from "../lib/apolloClient";
3 |
4 | export default function App({ Component, pageProps }) {
5 | const apolloClient = useApollo(pageProps.initialApolloState);
6 |
7 | return (
8 |
9 |
10 |
11 |
12 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/pages/index.js:
--------------------------------------------------------------------------------
1 | import PlayersList, { ALL_CLUBS_QUERY, ALL_COUNTRIES_QUERY } from "../components/playersList";
2 | import { initializeApollo } from "../lib/apolloClient";
3 | import Link from "next/link";
4 |
5 | const IndexPage = () => {
6 | return (
7 |
8 |
9 | EPL Player Directory (EPL Table)
10 |
11 |
12 |
13 | )
14 | };
15 |
16 | export async function getStaticProps() {
17 | const apolloClient = initializeApollo();
18 |
19 | await apolloClient.query({
20 | query: ALL_COUNTRIES_QUERY,
21 | });
22 |
23 | await apolloClient.query({
24 | query: ALL_CLUBS_QUERY,
25 | });
26 |
27 | return {
28 | props: {
29 | initialApolloState: apolloClient.cache.extract(),
30 | },
31 | revalidate: 1,
32 | };
33 | }
34 |
35 | export default IndexPage;
--------------------------------------------------------------------------------
/pages/table.js:
--------------------------------------------------------------------------------
1 | import { Table } from "@material-ui/core";
2 | import TableBody from "@material-ui/core/TableBody";
3 | import TableCell from "@material-ui/core/TableCell";
4 | import TableContainer from "@material-ui/core/TableContainer";
5 | import TableHead from "@material-ui/core/TableHead";
6 | import TableRow from "@material-ui/core/TableRow";
7 | import Paper from "@material-ui/core/Paper";
8 | import Link from "next/link";
9 |
10 | function EPLTable({ data }) {
11 | return (
12 |
13 |
Back to player directory
14 |
EPL Table
15 |
16 |
17 |
18 |
19 | Position
20 | Team Badge
21 | Club
22 | Played
23 | Won
24 | Drawn
25 | Lost
26 | GF
27 | GA
28 | GD
29 | Points
30 |
31 |
32 |
33 | {data.map((row) => (
34 |
35 |
36 | {row.overall_league_position}
37 |
38 |
39 |
40 |
41 | {row.team_name}
42 | {row.overall_league_payed}
43 | {row.overall_league_W}
44 | {row.overall_league_D}
45 | {row.overall_league_L}
46 | {row.overall_league_GF}
47 | {row.overall_league_GA}
48 |
49 | {row.overall_league_GF - row.overall_league_GA}
50 |
51 | {row.overall_league_PTS}
52 |
53 | ))}
54 |
55 |
56 |
57 |
58 | );
59 | }
60 |
61 | export async function getServerSideProps() {
62 | // Fetch data from external API
63 | const res = await fetch(
64 | `https://apiv2.apifootball.com/?action=get_standings&league_id=148&APIkey=YOUR-API-KEY` // Add your apifootball API key
65 | );
66 | const data = await res.json();
67 |
68 | // Pass data to the page via props
69 | return { props: { data } };
70 | }
71 |
72 | export default EPLTable;
73 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vardhanapoorv/epl-nextjs-app/7f11059611db422eb49867f0d3a7bff788365250/public/favicon.ico
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/schema.graphql:
--------------------------------------------------------------------------------
1 | type Player {
2 | id: ID!
3 | name: String! @search(by: [fulltext])
4 | position: Position @search
5 | overall: Int
6 | club: Club
7 | country: Country
8 | }
9 |
10 | enum Position {
11 | GK
12 | RB
13 | LB
14 | CB
15 | DM
16 | CM
17 | LM
18 | RM
19 | CF
20 | ST
21 | }
22 |
23 | type Club {
24 | id: ID!
25 | name: String!
26 | league: String
27 | stadium: String
28 | }
29 |
30 | type Country {
31 | id: ID!
32 | name: String!
33 | stadium: String
34 | }
35 |
--------------------------------------------------------------------------------
/styles/Home.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | min-height: 100vh;
3 | padding: 0 0.5rem;
4 | display: flex;
5 | flex-direction: column;
6 | justify-content: center;
7 | align-items: center;
8 | }
9 |
10 | .main {
11 | padding: 5rem 0;
12 | flex: 1;
13 | display: flex;
14 | flex-direction: column;
15 | justify-content: center;
16 | align-items: center;
17 | }
18 |
19 | .footer {
20 | width: 100%;
21 | height: 100px;
22 | border-top: 1px solid #eaeaea;
23 | display: flex;
24 | justify-content: center;
25 | align-items: center;
26 | }
27 |
28 | .footer img {
29 | margin-left: 0.5rem;
30 | }
31 |
32 | .footer a {
33 | display: flex;
34 | justify-content: center;
35 | align-items: center;
36 | }
37 |
38 | .title a {
39 | color: #0070f3;
40 | text-decoration: none;
41 | }
42 |
43 | .title a:hover,
44 | .title a:focus,
45 | .title a:active {
46 | text-decoration: underline;
47 | }
48 |
49 | .title {
50 | margin: 0;
51 | line-height: 1.15;
52 | font-size: 4rem;
53 | }
54 |
55 | .title,
56 | .description {
57 | text-align: center;
58 | }
59 |
60 | .description {
61 | line-height: 1.5;
62 | font-size: 1.5rem;
63 | }
64 |
65 | .code {
66 | background: #fafafa;
67 | border-radius: 5px;
68 | padding: 0.75rem;
69 | font-size: 1.1rem;
70 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
71 | Bitstream Vera Sans Mono, Courier New, monospace;
72 | }
73 |
74 | .grid {
75 | display: flex;
76 | align-items: center;
77 | justify-content: center;
78 | flex-wrap: wrap;
79 |
80 | max-width: 800px;
81 | margin-top: 3rem;
82 | }
83 |
84 | .card {
85 | margin: 1rem;
86 | flex-basis: 45%;
87 | padding: 1.5rem;
88 | text-align: left;
89 | color: inherit;
90 | text-decoration: none;
91 | border: 1px solid #eaeaea;
92 | border-radius: 10px;
93 | transition: color 0.15s ease, border-color 0.15s ease;
94 | }
95 |
96 | .card:hover,
97 | .card:focus,
98 | .card:active {
99 | color: #0070f3;
100 | border-color: #0070f3;
101 | }
102 |
103 | .card h3 {
104 | margin: 0 0 1rem 0;
105 | font-size: 1.5rem;
106 | }
107 |
108 | .card p {
109 | margin: 0;
110 | font-size: 1.25rem;
111 | line-height: 1.5;
112 | }
113 |
114 | .logo {
115 | height: 1em;
116 | }
117 |
118 | @media (max-width: 600px) {
119 | .grid {
120 | width: 100%;
121 | flex-direction: column;
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/styles/globals.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | padding: 0;
4 | margin: 0;
5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
7 | }
8 |
9 | a {
10 | color: inherit;
11 | text-decoration: none;
12 | }
13 |
14 | * {
15 | box-sizing: border-box;
16 | }
17 |
--------------------------------------------------------------------------------