├── .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 | 3 | 4 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------