├── .DS_Store ├── .gitignore ├── .vscode └── settings.json ├── README.md ├── app ├── .babelrc ├── .env.sample ├── .gitignore ├── Dockerfile ├── README.md ├── __mocks__ │ └── NextApi.ts ├── __pages__ │ ├── __snapshots__ │ │ └── index.test.tsx.snap │ ├── api │ │ ├── graphql.test.ts │ │ └── ping.test.ts │ └── index.test.tsx ├── __screenshots__ │ ├── analyze-client.png │ ├── analyze-server.png │ ├── jest-coverage-report-cli.png │ ├── jest-coverage-report-html.png │ ├── neo4j-browser-00-paste-cypher.png │ ├── neo4j-browser-01-paste-cypher.png │ ├── neo4j-browser-02-example-database-from-seed.png │ ├── neo4j-browser-03-example-database-from-seed-fullscreen-view.png │ └── nextjs-frontend-default-page.png ├── analytics │ └── google │ │ ├── googleAnalytics.test.ts │ │ └── googleAnalytics.ts ├── apollo │ ├── client.test.ts │ ├── client.ts │ ├── queries │ │ └── hello.ts │ ├── resolvers.test.ts │ ├── resolvers.ts │ ├── schema.test.ts │ ├── schema.ts │ ├── type-defs.test.ts │ └── type-defs.ts ├── grandstack-demo │ ├── components │ │ ├── Dashboard │ │ │ ├── Dashboard.styles.tsx │ │ │ └── Dashboard.tsx │ │ ├── RatingsChart │ │ │ ├── RatingsChart.test.tsx │ │ │ └── RatingsChart.tsx │ │ ├── RecentReviews │ │ │ ├── RecentReviews.test.tsx │ │ │ └── RecentReviews.tsx │ │ ├── Title │ │ │ ├── Title.test.tsx │ │ │ ├── Title.tsx │ │ │ └── __snapshots__ │ │ │ │ └── Title.test.tsx.snap │ │ └── UserCount │ │ │ ├── UserCount.styles.tsx │ │ │ ├── UserCount.test.tsx │ │ │ ├── UserCount.tsx │ │ │ └── __snapshots__ │ │ │ └── UserCount.test.tsx.snap │ └── layout │ │ ├── Footer │ │ └── Footer.tsx │ │ ├── Header │ │ ├── Header.styles.tsx │ │ └── Header.tsx │ │ ├── Layout.styles.tsx │ │ └── Layout.tsx ├── jest.config.js ├── jest.setup.js ├── neo4j │ ├── __seed__ │ │ └── db.cypher │ ├── db.test.ts │ └── db.ts ├── next-env.d.ts ├── next.config.js ├── package-lock.json ├── package.json ├── pages │ ├── _app.tsx │ ├── _document.tsx │ ├── api │ │ ├── graphql.ts │ │ └── ping.ts │ └── index.tsx ├── public │ └── img │ │ └── grandstack.png ├── tsconfig.json └── types │ └── Window.d.ts ├── docker-compose-neo4j-v4.x.yml ├── docker-compose.yml ├── neo4j ├── README.md ├── cypher │ └── cheatsheet.cypher ├── v3.5.x │ └── Dockerfile └── v4.x.x │ └── Dockerfile └── package.json /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheRobBrennan/nextjs-grandstack-starter-typescript/c5a2f5677e9a7e9390e9ba50432838cf5c3c9bdd/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vercel 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "autogenerate", 4 | "clsx", 5 | "dockerized", 6 | "dstack", 7 | "fullscreen", 8 | "gran", 9 | "grandstack", 10 | "imdb", 11 | "jwts", 12 | "lcov", 13 | "letmein", 14 | "mkdir", 15 | "rating", 16 | "therobbrennan", 17 | "vercel" 18 | ] 19 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GRANDstack Starter for Next.js with TypeScript 2 | 3 | **What's new in 2021?** 4 | 5 | A lot. 🎉 6 | 7 | On Thursday, January 14th, 2021, Neo4j had a big announcement in their GraphQL Community Call. 8 | 9 | As part of the initial [GRANDstack](https://grandstack.io) ([GraphQL](https://graphql.org), [React](https://reactjs.org), [Apollo](https://www.apollographql.com), [Neo4j Database](https://neo4j.com)) offering, Neo4j labs had created [https://github.com/neo4j-graphql/neo4j-graphql-js](https://github.com/neo4j-graphql/neo4j-graphql-js) - enabling developers to build a GraphQL API layer on top of an already existing Neo4j database and the Cypher query language. 10 | 11 | Neo4j has announced this effort is graduating from a labs project into a fully supported Neo4j product for the GraphQL layer - `@neo4j/graphql` 12 | 13 | This was a huge announcement; and I would encourage you to review the community call and materials below. 14 | 15 | `@neo4j/graphql` is currently in early alpha release and is available NOW. 16 | 17 | Useful resources 18 | 19 | - [Neo4j GraphQL Community Call - January 2021](https://www.youtube.com/watch?v=Og2I2K21MdI) 20 | - [Slide deck for the Community Call](https://docs.google.com/presentation/d/1GfdFUpYguMBUtWrrjSynGrJBkqfzHZYvffYScPltnDE/edit#slide=id.gb270d1e6e4_1_119) 21 | - [@neo4j/graphql on npm](https://www.npmjs.com/package/@neo4j/graphql) 22 | - [https://github.com/neo4j/graphql-tracker-temp](https://github.com/neo4j/graphql-tracker-temp) - This is the temporary repo where you can submit issues, etc. 23 | - [Neo4j on Slack](http://neo4j-users-slack-invite.herokuapp.com) 24 | 25 | --- 26 | 27 | This project is a starter for building a [GRANDstack](https://grandstack.io) ([GraphQL](https://graphql.org), [React](https://reactjs.org), [Apollo](https://www.apollographql.com), [Neo4j Database](https://neo4j.com)) application using [Next.js](https://nextjs.org) and [TypeScript](https://www.typescriptlang.org) instead of the original [create-react-app](https://reactjs.org/docs/create-a-new-react-app.html) example available at [https://github.com/grand-stack/grand-stack-starter](https://github.com/grand-stack/grand-stack-starter). 28 | 29 | ## Getting started 30 | 31 | To run this application as intended, you will need to: 32 | 33 | - Build and run the Dockerized project 34 | - Seed your Neo4j database with sample data 35 | 36 | ### DEMO 37 | 38 | ![app/__screenshots__/nextjs-frontend-default-page.png](app/__screenshots__/nextjs-frontend-default-page.png) 39 | 40 | A [demo](https://nextjs-grandstack-starter-typescript.vercel.app/) application has been deployed using a free [Neo4j Sandbox](https://sandbox.neo4j.com) as the backend database at [https://nextjs-grandstack-starter-typescript.vercel.app/] 41 | 42 | Please note that these sandboxes do not last forever - ten (10) days at most with active renewal and upkeep. 43 | 44 | TL:DR If the demo app is broken, that's why. You can run this locally to your heart's content. 🤣 45 | 46 | ### Build and run the Dockerized project 47 | 48 | If you would like to have your [Next.js](https://nextjs.org) application and [Neo4j Database](https://neo4j.com) running in a [Docker](https://www.docker.com) environment, you can quickly build, start, and stop versions of [Neo4j Database](https://neo4j.com) to your heart's content! 49 | 50 | To run this example, all you need to have installed on your system is [Docker](https://www.docker.com) and `npm` installed on your development system. 51 | 52 | If you do not have [Docker](https://www.docker.com) installed on your development system, go to freely available [Docker Desktop](https://www.docker.com/products/docker-desktop) and get that installed and configured on your development machine. 53 | 54 | If you already have `npm` and [Docker](https://www.docker.com) installed on your development system, run: 55 | 56 | ```sh 57 | # Run the project using Neo4j v3.5.x 58 | $ npm run dev 59 | 60 | # ALTERNATIVE: Run the project using Neo4j v4.x.x 61 | $ npm run dev:v4 62 | ``` 63 | 64 | You should be able to access the following URLs: 65 | 66 | - [http://localhost:7474/browser/](http://localhost:7474/browser/) - This is the Neo4j Browser application that you can use to explore your Neo4j database - as well as run Cypher commands to seed your database with example data 67 | - [http://localhost:3000](http://localhost:3000) - The frontend for our Next.js application 68 | - [http://localhost:3000/api/graphql](http://localhost:3000/api/graphql) - The GraphIQL explorer for our backend Next.js API which will be a serverless GraphQL function on Vercel 69 | - [http://localhost:3000/api/ping](http://localhost:3000/api/ping) - A sample API route that will be a serverless function on Vercel 70 | 71 | Additionally, the following scripts have been created for managing your Docker environment based on the version of Neo4j you are using. 72 | 73 | Neo4j v3.5.x: 74 | 75 | - `dev` - This starts the Dockerized project with services defined in `docker-compose.yml` 76 | - `dev:clean` - This builds fresh images and containers for the Dockerized project with services defined in `docker-compose.yml` 77 | - `dev:stop` - This stops the Dockerized project with services defined in `docker-compose.yml` 78 | 79 | Neo4j v4.x.x: 80 | 81 | - `dev:v4` - This starts the Dockerized project with services defined in `docker-compose-neo4j-v4.x.yml` 82 | - `dev:v4:clean` - This builds fresh images and containers for the Dockerized project with services defined in `docker-compose-neo4j-v4.x.yml` 83 | - `dev:v4:stop` - This stops the Dockerized project with services defined in `docker-compose-neo4j-v4.x.yml` 84 | 85 | Docker 86 | 87 | - `docker:destroy:global` - **WARNING: This command will delete all Docker images, containers, networks, and volumes for ALL Dockerized applications on your system** 88 | 89 | ### Seed your Neo4j database with sample data 90 | 91 | Once your Dockerized project is running, you can navigate to the [Neo4j Browser](https://neo4j.com/developer/neo4j-browser/) at [http://0.0.0.0:7474/browser/](http://0.0.0.0:7474/browser/). 92 | 93 | Open up `./app/neo4j/__seed__/db.cypher` so you can copy and paste the example Cypher statements into the Cypher window and press `play` to seed your database with example data. 94 | 95 | ## Tests 96 | 97 | This project uses [Jest](https://jestjs.io/) and [Enzyme](https://enzymejs.github.io/enzyme/) for unit and integration tests. 98 | 99 | The following scripts are available at both the top-level project directory as well as the `app` directory: 100 | 101 | - `npm run test` or `npm test` - This runs a single run of the Jest tests for our project. 102 | - `npm run test:ci` - Useful for running Jest tests in a continuous integration (CI) environment 103 | - See [https://jestjs.io/docs/en/cli#--ci](https://jestjs.io/docs/en/cli#--ci) for more details 104 | - `npm run test:coverage` - Generates a code coverage report of the Jest tests 105 | 106 | ![app/__screenshots__/jest-coverage-report-cli.png](app/__screenshots__/jest-coverage-report-cli.png) 107 | 108 | - `npm run test:coverage:view` - Generates a code coverage report of the Jest tests and automatically launches a web browser on macOS/Linux to interactively see what code is and is not being covered in tests 109 | 110 | ![app/__screenshots__/jest-coverage-report-html.png](app/__screenshots__/jest-coverage-report-html.png) 111 | 112 | - `npm run test:verbose` - This displays verbose output during the execution of the Jest tests 113 | 114 | - `npm run test:watch` - **This is the only test script that runs locally.** It runs Jest in `--watch` mode; running tests related to current code changes and not the entire suite 115 | 116 | ## Bundle analysis 117 | 118 | This project uses the [Next.js + Webpack Bundle Analyzer](https://github.com/vercel/next.js/tree/canary/packages/next-bundle-analyzer) to generate bundle analysis of both client and server bundles by running the `npm run analyze` script: 119 | 120 | ![app/__screenshots__/analyze-client.png](app/__screenshots__/analyze-client.png) 121 | 122 | ![app/__screenshots__/analyze-server.png](app/__screenshots__/analyze-server.png) 123 | 124 | ## Static files 125 | 126 | The `public` folder in your [Next.js](https://nextjs.org) app can be used to serve whatever assets you desire (e.g. `app/public/logo.png` would be available at [http://localhost:3000/logo.png](http://localhost:3000/logo.png)) 127 | 128 | ## Back-end API 129 | 130 | One benefit of developing with [Next.js](https://nextjs.org) and deploying to [Vercel](https://vercel.com/) is that any files contained within your `pages/api` folder will be separate serverless functions. 131 | 132 | In our case, we have two serverless functions that we are exposing: 133 | 134 | - `graphql` - This is the GraphQL endpoint our application and our GraphIQL IDE will use 135 | - `ping` - Displays a simple message to verify our serverless functions are online 136 | 137 | For more details, please refer to [Vercel Serverless Functions](https://vercel.com/docs/serverless-functions/introduction) 138 | 139 | ### GraphQL 140 | 141 | You can explore your GraphQL schema using the GraphIQL IDE - available at [http://localhost:3000/api/graphql](http://localhost:3000/api/graphql) 142 | 143 | ### Ping 144 | 145 | To verify your back-end API is running, you should be able to visit [http://localhost:3000/api/ping](http://localhost:3000/api/ping) and see a response. 146 | 147 | ## Third-party services 148 | 149 | ### Apollo GraphQL 150 | 151 | This project has an example [Apollo GraphQL](https://www.apollographql.com) server up and running at `/api/graphql` 152 | 153 | If you are running this project locally, you can view the [GraphIQL IDE](http://localhost:3000/api/graphql) at [http://localhost:3000/api/graphql](http://localhost:3000/api/graphql) 154 | 155 | ### Google Analytics 156 | 157 | This project uses [Google Analytics](http://analytics.google.com) to track user interactions and evaluate the application's usage. 158 | 159 | ### Neo4j 160 | 161 | The [Neo4j Database](https://neo4j.com) is the cornerstone of our GRANDstack application. 162 | 163 | Once you have defined the environment variables - either in `app/.env` for local development or the appropriate places for your environment - you can have a [Neo4j Database](https://neo4j.com) hosted anywhere you'd like. 164 | 165 | [Neo4j](https://neo4j.com) has three options to get you up and running quickly: 166 | 167 | - [Neo4j Desktop](https://neo4j.com/download/) - FREE - Perfect for exploring [Neo4j](https://neo4j.com) on your local machine 168 | - [Neo4j Sandbox](https://sandbox.neo4j.com) - FREE - This allows you to run short-lived Neo4j projects in the cloud for free. By default, Neo4j sandboxes will terminate after three (3) days; however you do have the ability to optionally extend for another seven (7) days if desired. 169 | - [Neo4j Aura](https://neo4j.com/cloud/aura/) - \$ - This is [Neo4j](https://neo4j.com)'s flagship offering with a developer-friendly graph database as a service. 170 | 171 | All you will need is the bolt URL and the credentials to access your database. 172 | 173 | #### Sample data 174 | 175 | The original [https://github.com/grand-stack/grand-stack-starter](https://github.com/grand-stack/grand-stack-starter) had a helper script to seed your [Neo4j Database](https://neo4j.com) with example data. That same example database is available for you in a series of Cypher commands. 176 | 177 | Once you have connected to your [Neo4j Database](https://neo4j.com) with [Neo4j Browser](https://neo4j.com/developer/neo4j-browser/), simply copy the text as is from `app/neo4j/__seed__/db.cypher` and execute the Cypher commands in a single action: 178 | 179 | ![app/__screenshots__/neo4j-browser-00-paste-cypher.png](app/__screenshots__/neo4j-browser-00-paste-cypher.png) 180 | 181 | ![app/__screenshots__/neo4j-browser-01-paste-cypher.png](app/__screenshots__/neo4j-browser-01-paste-cypher.png) 182 | 183 | You can verify that your [Neo4j Database](https://neo4j.com) has been successfully created: 184 | 185 | ![app/__screenshots__/neo4j-browser-02-example-database-from-seed.png](app/__screenshots__/neo4j-browser-02-example-database-from-seed.png) 186 | 187 | ![app/__screenshots__/neo4j-browser-03-example-database-from-seed-fullscreen-view.png](app/__screenshots__/neo4j-browser-03-example-database-from-seed-fullscreen-view.png) 188 | 189 | ### Vercel 190 | 191 | This project is ready to be configured for deployment to [Vercel](https://vercel.com/), as well as optionally using the [Vercel for GitHub](https://vercel.com/github) integration for automatic deployment. 192 | 193 | #### Environment variables 194 | 195 | We can define the environment variables we would like to use for our `Production,` `Preview,` and `Development` environments. Navigate to your [Vercel](https://vercel.com/) project settings and define the following environment variables: 196 | 197 | ```sh 198 | GOOGLE_ANALYTICS_TRACKING_ID=UA-156456153-7 199 | NEO4J_URI=bolt://54.167.150.120:32844 200 | NEO4J_USER=neo4j 201 | NEO4J_PASSWORD= 202 | 203 | # Enable encrypted driver connection for Neo4j 204 | #NEO4J_ENCRYPTED=true 205 | 206 | # Specify a specific Neo4j database (v4.x+ only) 207 | #NEO4J_DATABASE=neo4j 208 | ``` 209 | 210 | Please refer to `app/.env.sample` to see the latest environment variables that you will need to declare if the above list is incomplete. 211 | 212 | Please see [https://vercel.com/docs/v2/build-step#environment-variables](https://vercel.com/docs/v2/build-step#environment-variables) and/or [https://nextjs.org/docs/basic-features/environment-variables](https://nextjs.org/docs/basic-features/environment-variables) for details on defining environment variables for your application on [Vercel](https://vercel.com/). 213 | -------------------------------------------------------------------------------- /app/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [["next/babel"]] 3 | } 4 | -------------------------------------------------------------------------------- /app/.env.sample: -------------------------------------------------------------------------------- 1 | # Use this file to set environment variables with credentials and configuration options 2 | # This file is provided as an example and should be replaced with your own values 3 | 4 | # Analytics 5 | GOOGLE_ANALYTICS_TRACKING_ID=UA-156456153-7 6 | 7 | # Neo4j v3.5.x / v4.x.x [DOCKER] 8 | # NEO4J_URI=bolt://neo4j:7687 9 | # NEO4J_USER=neo4j 10 | # NEO4J_PASSWORD=letmein 11 | 12 | # Neo4j Desktop [LOCAL] 13 | # NEO4J_URI=bolt://localhost:7687 14 | # NEO4J_USER=neo4j 15 | # NEO4J_PASSWORD=password 16 | 17 | # Neo4j Sandbox [2021.01.16 ~3:03pm - Neo4j v3.5.17 - Enterprise] 18 | # Each sandbox is available for three (3) days but can optionally be extended another seven (7) days before it is destroyed. 19 | NEO4J_URI=bolt://54.167.150.120:32844 20 | NEO4J_USER=neo4j 21 | NEO4J_PASSWORD=the-assigned-password 22 | 23 | # Uncomment this line to enable encrypted driver connection for Neo4j 24 | #NEO4J_ENCRYPTED=true 25 | 26 | # Uncomment this line to specify a specific Neo4j database (v4.x+ only) 27 | #NEO4J_DATABASE=neo4j -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .next 3 | .env 4 | coverage/ -------------------------------------------------------------------------------- /app/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14.15.1 2 | 3 | ENV PORT 3000 4 | 5 | # Create app directory 6 | RUN mkdir -p /usr/src/app 7 | 8 | # Define the working directory of our Docker container 9 | WORKDIR /usr/src/app 10 | 11 | # Install dependencies 12 | COPY package*.json /usr/src/app/ 13 | RUN npm install 14 | 15 | # Copy source files 16 | COPY . /usr/src/app 17 | 18 | # Build app 19 | RUN npm run build 20 | 21 | # Expose our Next.js web application port 22 | EXPOSE 3000 23 | 24 | # Expose our Node.js debug port 25 | EXPOSE 9229 26 | 27 | # Start the app 28 | CMD ["npm", "run", "dev"] -------------------------------------------------------------------------------- /app/README.md: -------------------------------------------------------------------------------- 1 | # REFERENCE: Create a Next.js app from scratch 2 | 3 | ```sh 4 | # Navigate to your desired directory (such as app) 5 | $ cd app 6 | 7 | # Use npm init to create a package.json with typical values for your app 8 | $ npm init 9 | 10 | # Install required Next.js and React dependencies 11 | $ npm install next react react-dom 12 | 13 | # Create a pages directory 14 | $ mkdir pages 15 | 16 | # Create a default page 17 | $ cd pages 18 | $ touch index.js 19 | ``` 20 | 21 | Create a simple default page: 22 | 23 | ```jsx 24 | // app/pages/index.js 25 | const DefaultPage = () => { 26 | return
Welcome to Next.js!
27 | } 28 | 29 | export default DefaultPage 30 | ``` 31 | 32 | Once you have created the default page, you can now run your app with `$ npm run dev` 33 | 34 | You should be able to view your application at [http://localhost:3000](http://localhost:3000). 🤓 35 | 36 | ## Add TypeScript 37 | 38 | To add [TypeScript](https://www.typescriptlang.org) to your [Next.js](https://nextjs.org) app: 39 | 40 | ```sh 41 | # Navigate to your app directory 42 | $ cd app 43 | 44 | # Install TypeScript dev dependencies 45 | $ npm i -D typescript @types/react @types/node 46 | 47 | # Create an empty tsconfig.json file 48 | $ touch tsconfig.json 49 | 50 | # Run your app, and Next.js will automatically discover and configure TypeScript for you 51 | $ npm run dev 52 | ``` 53 | -------------------------------------------------------------------------------- /app/__mocks__/NextApi.ts: -------------------------------------------------------------------------------- 1 | import httpMocks from "node-mocks-http" 2 | 3 | export const generateMockNextApiRequest = () => { 4 | const req = httpMocks.createRequest() 5 | 6 | // We need to add an env property to our request for it to be a valid NextApiRequest type 7 | req.env = { test: "test" } 8 | 9 | return req 10 | } 11 | 12 | export const generateMockNextApiResponse = () => { 13 | const res = httpMocks.createResponse() 14 | 15 | // Mock our response status function so we can verify what HTTP status code it has been called with 16 | res.status = jest.fn(function () { 17 | return this 18 | }) 19 | 20 | // Mock our JSON response function so we can see what data has been returned from our API 21 | res.json = jest.fn() 22 | 23 | return res 24 | } 25 | -------------------------------------------------------------------------------- /app/__pages__/__snapshots__/index.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`The default page should render 1`] = ` 4 |
7 |
10 |
13 | GRANDstack logo 21 |

24 | Next.js GRANDstack Starter with @neo4j/graphql ^1.0.0-alpha.2 and TypeScript 25 |

26 |
27 |
28 |
31 |
34 |
37 |
40 |
43 |
46 |

47 | Loading 48 |

49 |
50 |
51 |
54 |
57 |

60 | Total Users 61 |

62 |

65 | Loading... 66 |

67 |

70 | users found 71 |

72 |
73 |
74 |
77 |
80 |

81 | Loading 82 |

83 |
84 |
85 |
86 |
89 |

92 | Copyright © 93 | 2021 94 | 95 | 101 | Robert J Brennan 102 | 103 | . All rights reserved. 104 |

105 |
106 |
107 |
108 |
109 | `; 110 | -------------------------------------------------------------------------------- /app/__pages__/api/graphql.test.ts: -------------------------------------------------------------------------------- 1 | // Special thanks to https://medium.com/@jdeflaux/graphql-integration-tests-with-apollo-server-testing-jest-mongodb-and-nock-af5a82e95954 for inspiration 2 | import { apolloServer } from "../../pages/api/graphql" 3 | import { createTestClient } from "apollo-server-testing" 4 | 5 | // Types 6 | import { HelloQuery, HelloResponse } from "../../apollo/queries/hello" 7 | 8 | describe("Apollo Server", () => { 9 | describe("Query", () => { 10 | describe("hello", () => { 11 | it("should return an expected greeting", async () => { 12 | // Setup a test client against our Apollo Server 13 | // @ts-ignore - Argument of type 'ApolloServer' is not assignable to parameter of type 'ApolloServerBase'. Types have separate declarations of a private property 'logger' 14 | const { query } = createTestClient(apolloServer) 15 | 16 | // Execute our query 17 | const { data } = await query({ 18 | query: HelloQuery, 19 | }) 20 | const { hello } = data 21 | const expectedGreeting = "Hello. The current timestamp is" 22 | 23 | // Validate 24 | expect(hello).toContain(expectedGreeting) 25 | }) 26 | }) 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /app/__pages__/api/ping.test.ts: -------------------------------------------------------------------------------- 1 | import { handler, PingResponse as HandlerResponse } from "../../pages/api/ping" 2 | import { 3 | generateMockNextApiRequest, 4 | generateMockNextApiResponse, 5 | } from "../../__mocks__/NextApi" 6 | 7 | describe("/api/ping", () => { 8 | describe("when invoked should return", () => { 9 | let req, res 10 | 11 | beforeEach(() => { 12 | req = generateMockNextApiRequest() 13 | res = generateMockNextApiResponse() 14 | }) 15 | 16 | // We need to reset mocks after every test so that we can reuse them 17 | afterEach(() => { 18 | jest.resetAllMocks() 19 | }) 20 | 21 | it("an HTTP status code of 200", async () => { 22 | const result = await handler(req, res) 23 | 24 | expect(res.status).toHaveBeenCalledWith(200) 25 | }) 26 | it("an expected JSON response from our handler", async () => { 27 | const result = await handler(req, res) 28 | const expectedResponse: HandlerResponse = { 29 | message: expect.any(String), 30 | } 31 | const expectedResponseShape = expect.objectContaining(expectedResponse) 32 | 33 | expect(res.json).toHaveBeenCalledWith(expectedResponseShape) 34 | }) 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /app/__pages__/index.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import TestRenderer from "react-test-renderer" 3 | 4 | import DefaultPage from "../pages/index" 5 | import { MockedProvider } from "@apollo/client/testing" 6 | 7 | describe("The default page", () => { 8 | it(`should render`, () => { 9 | // Verify success state 10 | const component = TestRenderer.create( 11 | 12 | 13 | 14 | ) 15 | expect(component.toJSON()).toMatchSnapshot() 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /app/__screenshots__/analyze-client.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheRobBrennan/nextjs-grandstack-starter-typescript/c5a2f5677e9a7e9390e9ba50432838cf5c3c9bdd/app/__screenshots__/analyze-client.png -------------------------------------------------------------------------------- /app/__screenshots__/analyze-server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheRobBrennan/nextjs-grandstack-starter-typescript/c5a2f5677e9a7e9390e9ba50432838cf5c3c9bdd/app/__screenshots__/analyze-server.png -------------------------------------------------------------------------------- /app/__screenshots__/jest-coverage-report-cli.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheRobBrennan/nextjs-grandstack-starter-typescript/c5a2f5677e9a7e9390e9ba50432838cf5c3c9bdd/app/__screenshots__/jest-coverage-report-cli.png -------------------------------------------------------------------------------- /app/__screenshots__/jest-coverage-report-html.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheRobBrennan/nextjs-grandstack-starter-typescript/c5a2f5677e9a7e9390e9ba50432838cf5c3c9bdd/app/__screenshots__/jest-coverage-report-html.png -------------------------------------------------------------------------------- /app/__screenshots__/neo4j-browser-00-paste-cypher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheRobBrennan/nextjs-grandstack-starter-typescript/c5a2f5677e9a7e9390e9ba50432838cf5c3c9bdd/app/__screenshots__/neo4j-browser-00-paste-cypher.png -------------------------------------------------------------------------------- /app/__screenshots__/neo4j-browser-01-paste-cypher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheRobBrennan/nextjs-grandstack-starter-typescript/c5a2f5677e9a7e9390e9ba50432838cf5c3c9bdd/app/__screenshots__/neo4j-browser-01-paste-cypher.png -------------------------------------------------------------------------------- /app/__screenshots__/neo4j-browser-02-example-database-from-seed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheRobBrennan/nextjs-grandstack-starter-typescript/c5a2f5677e9a7e9390e9ba50432838cf5c3c9bdd/app/__screenshots__/neo4j-browser-02-example-database-from-seed.png -------------------------------------------------------------------------------- /app/__screenshots__/neo4j-browser-03-example-database-from-seed-fullscreen-view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheRobBrennan/nextjs-grandstack-starter-typescript/c5a2f5677e9a7e9390e9ba50432838cf5c3c9bdd/app/__screenshots__/neo4j-browser-03-example-database-from-seed-fullscreen-view.png -------------------------------------------------------------------------------- /app/__screenshots__/nextjs-frontend-default-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheRobBrennan/nextjs-grandstack-starter-typescript/c5a2f5677e9a7e9390e9ba50432838cf5c3c9bdd/app/__screenshots__/nextjs-frontend-default-page.png -------------------------------------------------------------------------------- /app/analytics/google/googleAnalytics.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GOOGLE_ANALYTICS_TRACKING_ID, 3 | pageview, 4 | event, 5 | } from "./googleAnalytics" 6 | 7 | describe("Our Google Analytics library", () => { 8 | describe("should contain a GOOGLE_ANALYTICS_TRACKING_ID that", () => { 9 | it("has been defined", () => { 10 | expect(GOOGLE_ANALYTICS_TRACKING_ID).toBeDefined() 11 | }) 12 | it("is a string", () => { 13 | expect(typeof GOOGLE_ANALYTICS_TRACKING_ID).toEqual("string") 14 | }) 15 | }) 16 | 17 | describe("should contain a pageview function that", () => { 18 | // Create a mock for the gtag script on our window object 19 | const originalWindow = window 20 | 21 | beforeEach(() => { 22 | window.gtag = jest.fn() 23 | }) 24 | afterEach(() => { 25 | window.gtag = originalWindow.gtag 26 | }) 27 | 28 | it("has been defined", () => { 29 | expect(pageview).toBeDefined() 30 | }) 31 | it("sends a config event to window.gtag with parameters containing a URL", () => { 32 | // Create a mock for the gtag script on our window object 33 | const mockEvent = "config" 34 | const mockUrl = "/" 35 | const expectedArgs = [ 36 | mockEvent, 37 | GOOGLE_ANALYTICS_TRACKING_ID, 38 | { page_path: `${mockUrl}` }, 39 | ] 40 | 41 | // Call our function 42 | pageview(mockUrl) 43 | 44 | // Validate 45 | expect(window.gtag).toBeCalledTimes(1) 46 | expect(window.gtag).toBeCalledWith( 47 | expectedArgs[0], 48 | expectedArgs[1], 49 | expectedArgs[2] 50 | ) 51 | }) 52 | }) 53 | describe("should contain an event function that", () => { 54 | // Create a mock for the gtag script on our window object 55 | const originalWindow = window 56 | 57 | beforeEach(() => { 58 | window.gtag = jest.fn() 59 | }) 60 | afterEach(() => { 61 | window.gtag = originalWindow.gtag 62 | }) 63 | 64 | it("has been defined", () => { 65 | expect(event).toBeDefined() 66 | }) 67 | it("sends an action to window.gtag with event details", () => { 68 | // Define our mocks 69 | const mockEvent = "event" 70 | const mockEventCategory = "ecommerce" 71 | const mockEventLabel = "test-label" 72 | const mockEventValue = "test-value" 73 | const mockEventDetails = { 74 | action: "login", 75 | category: mockEventCategory, 76 | label: mockEventLabel, 77 | value: mockEventValue, 78 | } 79 | const expectedArgs = [ 80 | mockEvent, 81 | mockEventDetails.action, 82 | { 83 | event_category: mockEventDetails.category, 84 | event_label: mockEventDetails.label, 85 | value: mockEventDetails.value, 86 | }, 87 | ] 88 | 89 | // Call our function 90 | event(mockEventDetails) 91 | 92 | // Validate 93 | expect(window.gtag).toBeCalledTimes(1) 94 | expect(window.gtag).toBeCalledWith( 95 | expectedArgs[0], 96 | expectedArgs[1], 97 | expectedArgs[2] 98 | ) 99 | }) 100 | }) 101 | }) 102 | -------------------------------------------------------------------------------- /app/analytics/google/googleAnalytics.ts: -------------------------------------------------------------------------------- 1 | export const GOOGLE_ANALYTICS_TRACKING_ID = 2 | process.env.GOOGLE_ANALYTICS_TRACKING_ID 3 | 4 | // https://developers.google.com/analytics/devguides/collection/gtagjs/pages 5 | export const pageview = (url) => { 6 | window.gtag("config", GOOGLE_ANALYTICS_TRACKING_ID, { 7 | page_path: url, 8 | }) 9 | } 10 | 11 | // https://developers.google.com/analytics/devguides/collection/gtagjs/events 12 | export const event = ({ action, category, label, value }) => { 13 | window.gtag("event", action, { 14 | event_category: category, 15 | event_label: label, 16 | value: value, 17 | }) 18 | } 19 | -------------------------------------------------------------------------------- /app/apollo/client.test.ts: -------------------------------------------------------------------------------- 1 | import { initializeApollo, useApollo } from "./client" 2 | 3 | describe("Apollo client", () => { 4 | describe("initializeApollo", () => { 5 | it("is defined", () => { 6 | expect(initializeApollo).toBeDefined() 7 | }) 8 | describe("when executed without any parameters", () => { 9 | it("should return an initialized Apollo client with an empty initial state", () => { 10 | const result = initializeApollo() 11 | const { version, localState } = result 12 | const expectedVersion = "local" 13 | const expectedState = {} 14 | 15 | expect(version).toEqual(expectedVersion) 16 | expect(localState.cache.data.data).toEqual(expectedState) 17 | }) 18 | }) 19 | describe("when executed with a null initialState", () => { 20 | it("should return an initialized Apollo client with an empty initial state", () => { 21 | const result = initializeApollo(null) 22 | const { version, localState } = result 23 | const expectedVersion = "local" 24 | const expectedState = {} 25 | 26 | expect(version).toEqual(expectedVersion) 27 | expect(localState.cache.data.data).toEqual(expectedState) 28 | }) 29 | }) 30 | describe("when executed with a defined object as initialState", () => { 31 | it("should return an Apollo client with the supplied state", () => { 32 | const mockInitialState = { aKey: "aValue" } 33 | 34 | const result = initializeApollo(mockInitialState) 35 | const { version, localState } = result 36 | const expectedVersion = "local" 37 | const expectedState = mockInitialState 38 | 39 | expect(version).toEqual(expectedVersion) 40 | expect(localState.cache.data.data).not.toEqual({}) 41 | expect(localState.cache.data.data).toEqual(expectedState) 42 | }) 43 | }) 44 | }) 45 | describe("useApollo", () => { 46 | it("is defined", () => { 47 | expect(useApollo).toBeDefined() 48 | }) 49 | // REMEMBER: Hooks need to be tested within functional components directly; not as standalone code 50 | }) 51 | }) 52 | -------------------------------------------------------------------------------- /app/apollo/client.ts: -------------------------------------------------------------------------------- 1 | import fetch from "cross-fetch" 2 | import { useMemo } from "react" 3 | import { ApolloClient, InMemoryCache } from "@apollo/client" 4 | 5 | let apolloClient 6 | 7 | function createIsomorphLink() { 8 | /* istanbul ignore next */ 9 | if (typeof window === "undefined") { 10 | const { SchemaLink } = require("@apollo/client/link/schema") 11 | const { schema } = require("./schema") 12 | return new SchemaLink({ schema }) 13 | } else { 14 | const { HttpLink } = require("@apollo/client/link/http") 15 | return new HttpLink({ 16 | uri: "api/graphql", 17 | credentials: "same-origin", 18 | fetch, 19 | }) 20 | } 21 | } 22 | 23 | function createApolloClient() { 24 | return new ApolloClient({ 25 | ssrMode: typeof window === "undefined", 26 | link: createIsomorphLink(), 27 | cache: new InMemoryCache(), 28 | }) 29 | } 30 | 31 | export function initializeApollo(initialState = null) { 32 | const _apolloClient = apolloClient ?? createApolloClient() 33 | 34 | // If your page has Next.js data fetching methods that use Apollo Client, the initial state 35 | // get hydrated here 36 | if (initialState) { 37 | _apolloClient.cache.restore(initialState) 38 | } 39 | // For SSG and SSR always create a new Apollo Client 40 | /* istanbul ignore next */ 41 | if (typeof window === "undefined") return _apolloClient 42 | // Create the Apollo Client once in the client 43 | if (!apolloClient) apolloClient = _apolloClient 44 | 45 | return _apolloClient 46 | } 47 | 48 | /* istanbul ignore next */ 49 | export function useApollo(initialState) { 50 | const store = useMemo(() => initializeApollo(initialState), [initialState]) 51 | return store 52 | } 53 | -------------------------------------------------------------------------------- /app/apollo/queries/hello.ts: -------------------------------------------------------------------------------- 1 | // Define custom types 2 | export type HelloResponse = { 3 | hello: string 4 | } 5 | 6 | // GraphQL Query 7 | export const HelloQuery = ` 8 | { 9 | hello 10 | } 11 | ` 12 | -------------------------------------------------------------------------------- /app/apollo/resolvers.test.ts: -------------------------------------------------------------------------------- 1 | import { resolvers } from "./resolvers" 2 | 3 | describe("Our GraphQL resolvers should contain", () => { 4 | describe("A Query object", () => { 5 | const { Query } = resolvers 6 | 7 | it("that has been defined", () => { 8 | expect(Query).toBeDefined() 9 | }) 10 | 11 | describe("with a custom 'hello' function", () => { 12 | const { hello } = Query 13 | it("that has been defined", () => { 14 | expect(hello).toBeDefined() 15 | }) 16 | it("that returns a string when executed", async () => { 17 | const result = await hello(null, null, null) 18 | expect(typeof result).toEqual("string") 19 | }) 20 | }) 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /app/apollo/resolvers.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Write resolvers to respond to our queries and mutations 3 | * 4 | * Be sure to view the link below to see great examples of: 5 | * - Creating custom resolvers (e.g. mutation to handle creating a User node and send an automated email) 6 | * - Translate to override auto-generated resolvers by using the same name (e.g. createUsers with custom functionality) 7 | * - Use of the @cypher directive for any query or mutation (including in type definitions for properties such as relatedPosts) 8 | * - Use of the @auth directive for accepting JWTs in the request 9 | * - Use of the @exclude directive to exclude automatically generating queries or resolvers for types 10 | * - Use of the @autogenerate directive to automatically generate unique values for ID fields 11 | * 12 | * https://www.notion.so/neo4j-graphql-v1-0-0-alpha-2-d47908030d4e4a0c86babbaef63887d0 13 | */ 14 | export const resolvers = { 15 | Query: { 16 | async hello(_parent, _args, _context) { 17 | return `Hello. The current timestamp is ${Date.now()}` 18 | }, 19 | }, 20 | } 21 | -------------------------------------------------------------------------------- /app/apollo/schema.test.ts: -------------------------------------------------------------------------------- 1 | import { augmentedSchema } from "./schema" 2 | 3 | describe("GraphQL schema", () => { 4 | describe("augmentedSchema", () => { 5 | it("is defined", () => { 6 | expect(augmentedSchema).toBeDefined() 7 | }) 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /app/apollo/schema.ts: -------------------------------------------------------------------------------- 1 | import { makeAugmentedSchema } from "@neo4j/graphql" 2 | import { typeDefs } from "./type-defs" 3 | import { resolvers } from "./resolvers" 4 | 5 | export const augmentedSchema = makeAugmentedSchema({ 6 | typeDefs, 7 | resolvers, 8 | }) 9 | -------------------------------------------------------------------------------- /app/apollo/type-defs.test.ts: -------------------------------------------------------------------------------- 1 | import { typeDefs } from "./type-defs" 2 | 3 | describe("GraphQL type definitions", () => { 4 | it("are defined", () => { 5 | expect(typeDefs).toBeDefined() 6 | }) 7 | }) 8 | -------------------------------------------------------------------------------- /app/apollo/type-defs.ts: -------------------------------------------------------------------------------- 1 | // Describe our GraphQL schema with type definitions 2 | export const typeDefs = ` 3 | type Business { 4 | businessId: ID! 5 | name: String! 6 | address: String 7 | city: String 8 | state: String 9 | reviews: [Review] @relationship(type: "REVIEWS", direction: "IN") 10 | categories: [Category] @relationship(type: "IN_CATEGORY", direction: "OUT") 11 | } 12 | 13 | type Category { 14 | name: ID! 15 | businesses: [Business] @relationship(type: "IN_CATEGORY", direction: "IN") 16 | } 17 | 18 | type RatingCount { 19 | stars: Float! 20 | count: Int! 21 | } 22 | 23 | type User { 24 | userId: ID! 25 | name: String 26 | reviews: [Review] @relationship(type: "WROTE", direction: "OUT") 27 | } 28 | 29 | type Review { 30 | reviewId: ID! 31 | stars: Float 32 | text: String 33 | date: Date 34 | business: Business @relationship(type: "REVIEWS", direction: "OUT") 35 | user: User @relationship(type: "WROTE", direction: "IN") 36 | } 37 | 38 | type Query { 39 | """ 40 | A sample query to verify that our GraphQL server is online. 41 | 42 | It returns a friendly greeting with the current timestamp. 43 | """ 44 | hello: String!, 45 | 46 | """ 47 | A sample query to return the total number of users in our system 48 | """ 49 | userCount: Int! @cypher(statement: "MATCH (u:User) RETURN COUNT(u)") 50 | 51 | """ 52 | A sample query to return the total number of stars awarded in reviews for our system 53 | """ 54 | ratingsCount: [RatingCount] 55 | @cypher( 56 | statement: "MATCH (r:Review) WITH r.stars AS stars, COUNT(*) AS count ORDER BY stars RETURN {stars: stars, count: count}" 57 | ) 58 | } 59 | ` 60 | -------------------------------------------------------------------------------- /app/grandstack-demo/components/Dashboard/Dashboard.styles.tsx: -------------------------------------------------------------------------------- 1 | // Material UI 2 | import { makeStyles } from "@material-ui/core/styles" 3 | 4 | export const useStyles = makeStyles((theme) => ({ 5 | root: { 6 | display: "flex", 7 | }, 8 | paper: { 9 | padding: theme.spacing(2), 10 | display: "flex", 11 | overflow: "auto", 12 | flexDirection: "column", 13 | }, 14 | fixedHeight: { 15 | height: 240, 16 | }, 17 | })) 18 | -------------------------------------------------------------------------------- /app/grandstack-demo/components/Dashboard/Dashboard.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import clsx from "clsx" 3 | 4 | // Components 5 | import RatingsChart from "../RatingsChart/RatingsChart" 6 | import UserCount from "../UserCount/UserCount" 7 | import RecentReviews from "../RecentReviews/RecentReviews" 8 | 9 | // Material UI 10 | import { useStyles } from "./Dashboard.styles" 11 | import { useTheme } from "@material-ui/core/styles" 12 | import { Grid, Paper } from "@material-ui/core" 13 | 14 | const Dashboard = () => { 15 | const theme = useTheme() 16 | const classes = useStyles(theme) 17 | const fixedHeightPaper = clsx(classes.paper, classes.fixedHeight) 18 | 19 | return ( 20 | <> 21 | 22 | {/* Ratings Chart */} 23 | 24 | 25 | 26 | 27 | 28 | {/* User Count */} 29 | 30 | 31 | 32 | 33 | 34 | {/* Recent Reviews */} 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | ) 43 | } 44 | export default Dashboard 45 | -------------------------------------------------------------------------------- /app/grandstack-demo/components/RatingsChart/RatingsChart.test.tsx: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file */ 2 | import React from "react" 3 | import TestRenderer, { act } from "react-test-renderer" 4 | import { MockedProvider } from "@apollo/client/testing" 5 | 6 | import RatingsChart, { GET_DATA_QUERY } from "./RatingsChart" 7 | 8 | describe("GRANDstack RatingsChart component", () => { 9 | describe("when invoked WITHOUT a specific height or size", () => { 10 | it("should render the Ratings Distribution chart after receiving data", async () => { 11 | // Define our Apollo request 12 | const renderRequest = { 13 | request: { 14 | query: GET_DATA_QUERY, 15 | variables: {}, 16 | }, 17 | result: { 18 | data: { 19 | ratingsCount: [ 20 | { stars: 3, count: 2, __typename: "RatingCount" }, 21 | { stars: 4, count: 4, __typename: "RatingCount" }, 22 | { stars: 5, count: 6, __typename: "RatingCount" }, 23 | ], 24 | }, 25 | }, 26 | } 27 | 28 | // Define our mock response(s) 29 | const gqlMocks = [renderRequest] 30 | 31 | // REVISIT: Known untestable design using Recharts 🥺 32 | console.warn( 33 | `This is untestable due to a bug with testing the ResizeDetector component within the Recharts third-party library 🥺` 34 | ) 35 | 36 | // Test 37 | // const component = TestRenderer.create( 38 | // 39 | // 40 | // 41 | // ) 42 | 43 | // Advance to the next tick in the event loop so our chart can render 44 | // await act(() => { 45 | // return new Promise((resolve) => { 46 | // setTimeout(resolve, 0) 47 | // }) 48 | // }) 49 | 50 | // Verify 51 | }) 52 | }) 53 | 54 | describe("when invoked WITH a specific height and size", () => { 55 | it("should render the Ratings Distribution chart after receiving data", async () => { 56 | // Define our Apollo request 57 | const renderRequest = { 58 | request: { 59 | query: GET_DATA_QUERY, 60 | variables: {}, 61 | }, 62 | result: { 63 | data: { 64 | ratingsCount: [ 65 | { stars: 3, count: 2, __typename: "RatingCount" }, 66 | { stars: 4, count: 4, __typename: "RatingCount" }, 67 | { stars: 5, count: 6, __typename: "RatingCount" }, 68 | ], 69 | }, 70 | }, 71 | } 72 | 73 | // Define our mock response(s) 74 | const gqlMocks = [renderRequest] 75 | 76 | // REVISIT: Known untestable design using Recharts 🥺 77 | console.warn( 78 | `This is untestable due to a bug with testing the ResizeDetector component within the Recharts third-party library 🥺` 79 | ) 80 | 81 | // Test 82 | // const component = TestRenderer.create( 83 | // 84 | // 85 | // 86 | // ) 87 | 88 | // Advance to the next tick in the event loop so our chart can render 89 | // await act(() => { 90 | // return new Promise((resolve) => { 91 | // setTimeout(resolve, 0) 92 | // }) 93 | // }) 94 | 95 | // Verify 96 | }) 97 | }) 98 | }) 99 | -------------------------------------------------------------------------------- /app/grandstack-demo/components/RatingsChart/RatingsChart.tsx: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file */ 2 | import { FC } from "react" 3 | import { useQuery } from "@apollo/react-hooks" 4 | import gql from "graphql-tag" 5 | 6 | // Components 7 | import Title from "../Title/Title" 8 | 9 | // Material UI 10 | import { useTheme } from "@material-ui/core/styles" 11 | 12 | // Chart 13 | import { 14 | Bar, 15 | XAxis, 16 | YAxis, 17 | Label, 18 | ResponsiveContainer, 19 | BarChart, 20 | } from "recharts" 21 | 22 | // GraphQL 23 | export const GET_DATA_QUERY = gql` 24 | { 25 | ratingsCount { 26 | stars 27 | count 28 | } 29 | } 30 | ` 31 | 32 | interface IRatingsChart { 33 | width?: number | string 34 | height?: number | string 35 | } 36 | 37 | const RatingsChart: FC = ({ width, height }) => { 38 | const theme = useTheme() 39 | const { loading, error, data } = useQuery(GET_DATA_QUERY) 40 | 41 | if (error) return

Error

42 | if (loading) return

Loading

43 | 44 | return ( 45 | <> 46 | Ratings Distribution 47 | 51 | 60 | 61 | 62 | 69 | 70 | 71 | 72 | 73 | 74 | ) 75 | } 76 | export default RatingsChart 77 | -------------------------------------------------------------------------------- /app/grandstack-demo/components/RecentReviews/RecentReviews.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import TestRenderer, { act } from "react-test-renderer" 3 | import { MockedProvider } from "@apollo/client/testing" 4 | 5 | import RecentReviews, { GET_RECENT_REVIEWS_QUERY } from "./RecentReviews" 6 | 7 | describe("GRANDstack RecentReviews component", () => { 8 | it("should render the latest reviews after receiving data", async () => { 9 | // Define our Apollo request 10 | const renderRequest = { 11 | request: { 12 | query: GET_RECENT_REVIEWS_QUERY, 13 | variables: {}, 14 | }, 15 | result: { 16 | data: { 17 | Reviews: [ 18 | { 19 | date: { 20 | year: { 21 | low: 2018, 22 | high: 0, 23 | }, 24 | month: { 25 | low: 9, 26 | high: 0, 27 | }, 28 | day: { 29 | low: 10, 30 | high: 0, 31 | }, 32 | }, 33 | user: { 34 | userId: "u4", 35 | name: "Angie", 36 | __typename: "User", 37 | }, 38 | business: { 39 | name: "Imagine Nation Brewing", 40 | __typename: "Business", 41 | }, 42 | text: "", 43 | stars: { 44 | low: 3, 45 | high: 0, 46 | }, 47 | __typename: "Review", 48 | }, 49 | { 50 | date: { 51 | year: { 52 | low: 2018, 53 | high: 0, 54 | }, 55 | month: { 56 | low: 8, 57 | high: 0, 58 | }, 59 | day: { 60 | low: 11, 61 | high: 0, 62 | }, 63 | }, 64 | user: { 65 | userId: "u4", 66 | name: "Angie", 67 | __typename: "User", 68 | }, 69 | business: { 70 | name: "Zootown Brew", 71 | __typename: "Business", 72 | }, 73 | text: "", 74 | stars: { 75 | low: 5, 76 | high: 0, 77 | }, 78 | __typename: "Review", 79 | }, 80 | { 81 | date: { 82 | year: { 83 | low: 2018, 84 | high: 0, 85 | }, 86 | month: { 87 | low: 3, 88 | high: 0, 89 | }, 90 | day: { 91 | low: 24, 92 | high: 0, 93 | }, 94 | }, 95 | user: { 96 | userId: "u2", 97 | name: "Bob", 98 | __typename: "User", 99 | }, 100 | business: { 101 | name: "Market on Front", 102 | __typename: "Business", 103 | }, 104 | text: "", 105 | stars: { 106 | low: 4, 107 | high: 0, 108 | }, 109 | __typename: "Review", 110 | }, 111 | { 112 | date: { 113 | year: { 114 | low: 2018, 115 | high: 0, 116 | }, 117 | month: { 118 | low: 1, 119 | high: 0, 120 | }, 121 | day: { 122 | low: 3, 123 | high: 0, 124 | }, 125 | }, 126 | user: { 127 | userId: "u1", 128 | name: "Will", 129 | __typename: "User", 130 | }, 131 | business: { 132 | name: "Ninja Mike's", 133 | __typename: "Business", 134 | }, 135 | text: 136 | "Best breakfast sandwich at the Farmer's Market. Always get the works.", 137 | stars: { 138 | low: 4, 139 | high: 0, 140 | }, 141 | __typename: "Review", 142 | }, 143 | { 144 | date: { 145 | year: { 146 | low: 2017, 147 | high: 0, 148 | }, 149 | month: { 150 | low: 11, 151 | high: 0, 152 | }, 153 | day: { 154 | low: 13, 155 | high: 0, 156 | }, 157 | }, 158 | user: { 159 | userId: "u3", 160 | name: "Jenny", 161 | __typename: "User", 162 | }, 163 | business: { 164 | name: "Ninja Mike's", 165 | __typename: "Business", 166 | }, 167 | text: "", 168 | stars: { 169 | low: 5, 170 | high: 0, 171 | }, 172 | __typename: "Review", 173 | }, 174 | { 175 | date: { 176 | year: { 177 | low: 2016, 178 | high: 0, 179 | }, 180 | month: { 181 | low: 11, 182 | high: 0, 183 | }, 184 | day: { 185 | low: 21, 186 | high: 0, 187 | }, 188 | }, 189 | user: { 190 | userId: "u3", 191 | name: "Jenny", 192 | __typename: "User", 193 | }, 194 | business: { 195 | name: "Hanabi", 196 | __typename: "Business", 197 | }, 198 | text: "", 199 | stars: { 200 | low: 5, 201 | high: 0, 202 | }, 203 | __typename: "Review", 204 | }, 205 | { 206 | date: { 207 | year: { 208 | low: 2016, 209 | high: 0, 210 | }, 211 | month: { 212 | low: 7, 213 | high: 0, 214 | }, 215 | day: { 216 | low: 14, 217 | high: 0, 218 | }, 219 | }, 220 | user: { 221 | userId: "u3", 222 | name: "Jenny", 223 | __typename: "User", 224 | }, 225 | business: { 226 | name: "KettleHouse Brewing Co.", 227 | __typename: "Business", 228 | }, 229 | text: "", 230 | stars: { 231 | low: 5, 232 | high: 0, 233 | }, 234 | __typename: "Review", 235 | }, 236 | { 237 | date: { 238 | year: { 239 | low: 2016, 240 | high: 0, 241 | }, 242 | month: { 243 | low: 3, 244 | high: 0, 245 | }, 246 | day: { 247 | low: 4, 248 | high: 0, 249 | }, 250 | }, 251 | user: { 252 | userId: "u1", 253 | name: "Will", 254 | __typename: "User", 255 | }, 256 | business: { 257 | name: "Ducky's Car Wash", 258 | __typename: "Business", 259 | }, 260 | text: "Awesome full-service car wash. Love Ducky's!", 261 | stars: { 262 | low: 5, 263 | high: 0, 264 | }, 265 | __typename: "Review", 266 | }, 267 | { 268 | date: { 269 | year: { 270 | low: 2016, 271 | high: 0, 272 | }, 273 | month: { 274 | low: 1, 275 | high: 0, 276 | }, 277 | day: { 278 | low: 3, 279 | high: 0, 280 | }, 281 | }, 282 | user: { 283 | userId: "u1", 284 | name: "Will", 285 | __typename: "User", 286 | }, 287 | business: { 288 | name: "KettleHouse Brewing Co.", 289 | __typename: "Business", 290 | }, 291 | text: "Great IPA selection!", 292 | stars: { 293 | low: 4, 294 | high: 0, 295 | }, 296 | __typename: "Review", 297 | }, 298 | { 299 | date: { 300 | year: { 301 | low: 2015, 302 | high: 0, 303 | }, 304 | month: { 305 | low: 12, 306 | high: 0, 307 | }, 308 | day: { 309 | low: 15, 310 | high: 0, 311 | }, 312 | }, 313 | user: { 314 | userId: "u2", 315 | name: "Bob", 316 | __typename: "User", 317 | }, 318 | business: { 319 | name: "Imagine Nation Brewing", 320 | __typename: "Business", 321 | }, 322 | text: "", 323 | stars: { 324 | low: 4, 325 | high: 0, 326 | }, 327 | __typename: "Review", 328 | }, 329 | { 330 | date: { 331 | year: { 332 | low: 2015, 333 | high: 0, 334 | }, 335 | month: { 336 | low: 9, 337 | high: 0, 338 | }, 339 | day: { 340 | low: 1, 341 | high: 0, 342 | }, 343 | }, 344 | user: { 345 | userId: "u1", 346 | name: "Will", 347 | __typename: "User", 348 | }, 349 | business: { 350 | name: "Neo4j", 351 | __typename: "Business", 352 | }, 353 | text: "The world's leading graph database HQ!", 354 | stars: { 355 | low: 5, 356 | high: 0, 357 | }, 358 | __typename: "Review", 359 | }, 360 | { 361 | date: { 362 | year: { 363 | low: 2015, 364 | high: 0, 365 | }, 366 | month: { 367 | low: 8, 368 | high: 0, 369 | }, 370 | day: { 371 | low: 29, 372 | high: 0, 373 | }, 374 | }, 375 | user: { 376 | userId: "u1", 377 | name: "Will", 378 | __typename: "User", 379 | }, 380 | business: { 381 | name: "Missoula Public Library", 382 | __typename: "Business", 383 | }, 384 | text: 385 | "Not a great selection of books, but fortunately the inter-library loan system is good. Wifi is quite slow. Not many comfortable places to site and read. Looking forward to the new building across the street in 2020!", 386 | stars: { 387 | low: 3, 388 | high: 0, 389 | }, 390 | __typename: "Review", 391 | }, 392 | ], 393 | }, 394 | }, 395 | } 396 | 397 | // Define our mock response(s) 398 | const gqlMocks = [renderRequest] 399 | 400 | // Verify success state 401 | const component = TestRenderer.create( 402 | 403 | 404 | 405 | ) 406 | 407 | // Advance to the next tick in the event loop so our chart can render 408 | await act(() => { 409 | return new Promise((resolve) => { 410 | setTimeout(resolve, 0) 411 | }) 412 | }) 413 | 414 | // Verify 415 | const p = component.root.findByType("h2") 416 | expect(p.children).toContain("Recent Reviews") 417 | }) 418 | 419 | describe("should display an error message if", () => { 420 | it("there is a network request failure", async () => { 421 | // Define our Apollo request 422 | const renderRequest = { 423 | request: { 424 | query: GET_RECENT_REVIEWS_QUERY, 425 | variables: {}, 426 | }, 427 | error: new Error("Simulated network or HTTP error"), 428 | } 429 | 430 | // Define our mock response(s) 431 | const gqlMocks = [renderRequest] 432 | 433 | // Verify success state 434 | const component = TestRenderer.create( 435 | 436 | 437 | 438 | ) 439 | 440 | // Advance to the next tick in the event loop so our chart can render 441 | await act(() => { 442 | return new Promise((resolve) => { 443 | setTimeout(resolve, 0) 444 | }) 445 | }) 446 | 447 | // Verify 448 | const tree = component.toJSON() 449 | expect(tree.children).toContain("Simulated network or HTTP error") 450 | }) 451 | it("our GraphQL server successfully processed our request and has returned with one or more error(s) in the result", async () => { 452 | // Define our Apollo request 453 | const renderRequest = { 454 | request: { 455 | query: GET_RECENT_REVIEWS_QUERY, 456 | variables: {}, 457 | }, 458 | result: { 459 | // REVISIT: Create a pull request - errors should be able to be defined on the GraphQL result object because it can contain data and errors 460 | // https://www.apollographql.com/docs/react/development-testing/testing/#graphql-errors 461 | errors: [ 462 | new Error("Simulated GraphQL server response with an error"), 463 | ], 464 | }, 465 | } 466 | 467 | // Define our mock response(s) 468 | const gqlMocks = [renderRequest] 469 | 470 | // Verify success state 471 | const component = TestRenderer.create( 472 | 473 | 474 | 475 | ) 476 | 477 | // Advance to the next tick in the event loop so our chart can render 478 | await act(() => { 479 | return new Promise((resolve) => { 480 | setTimeout(resolve, 0) 481 | }) 482 | }) 483 | 484 | // Verify 485 | const tree = component.toJSON() 486 | expect(tree.children).toContain( 487 | "Simulated GraphQL server response with an error" 488 | ) 489 | }) 490 | }) 491 | }) 492 | -------------------------------------------------------------------------------- /app/grandstack-demo/components/RecentReviews/RecentReviews.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react" 2 | import { useQuery } from "@apollo/react-hooks" 3 | import gql from "graphql-tag" 4 | 5 | // Components 6 | import Title from "../Title/Title" 7 | 8 | // Material UI 9 | import { 10 | Table, 11 | TableBody, 12 | TableCell, 13 | TableHead, 14 | TableRow, 15 | } from "@material-ui/core" 16 | 17 | export const GET_RECENT_REVIEWS_QUERY = gql` 18 | { 19 | Reviews(options: { sort: [date_DESC] }) { 20 | date 21 | user { 22 | userId 23 | name 24 | } 25 | business { 26 | name 27 | } 28 | text 29 | stars 30 | } 31 | } 32 | ` 33 | 34 | const RecentReviews: FC = () => { 35 | const { loading, error, data } = useQuery(GET_RECENT_REVIEWS_QUERY) 36 | if (loading) return

Loading

37 | if (error) return

{error.message}

38 | 39 | return ( 40 | <> 41 | Recent Reviews 42 | 43 | 44 | 45 | Date 46 | Business 47 | User 48 | Review 49 | Stars 50 | 51 | 52 | 53 | {data?.Reviews.map((review) => ( 54 | 57 | 58 | {`${review?.date?.year?.low}.${review?.date?.month?.low}.${review?.date?.day?.low}`} 59 | 60 | {review?.business?.name} 61 | {review?.user?.name} 62 | {review?.text} 63 | {review?.stars?.low} 64 | 65 | ))} 66 | 67 |
68 | 69 | ) 70 | } 71 | export default RecentReviews 72 | -------------------------------------------------------------------------------- /app/grandstack-demo/components/Title/Title.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import TestRenderer from "react-test-renderer" 3 | import { MockedProvider } from "@apollo/client/testing" 4 | 5 | import Title from "./Title" 6 | 7 | describe("GRANDstack Title component", () => { 8 | describe("should render", () => { 9 | it("without any children", () => { 10 | const component = TestRenderer.create( 11 | 12 | 13 | </MockedProvider> 14 | ) 15 | expect(component.toJSON()).toMatchSnapshot() 16 | }) 17 | it("with a supplied title", () => { 18 | const title = "A GRANDstack Title" 19 | const component = TestRenderer.create( 20 | <MockedProvider mocks={[]} addTypename={false}> 21 | <Title>{title} 22 | 23 | ) 24 | expect(component.toJSON()).toMatchSnapshot() 25 | }) 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /app/grandstack-demo/components/Title/Title.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react" 2 | 3 | // Material UI 4 | import { Typography } from "@material-ui/core" 5 | 6 | interface ITitle { 7 | children?: string 8 | } 9 | 10 | const Title: FC = ({ children }) => { 11 | return ( 12 | 13 | {children} 14 | 15 | ) 16 | } 17 | export default Title 18 | -------------------------------------------------------------------------------- /app/grandstack-demo/components/Title/__snapshots__/Title.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`GRANDstack Title component should render with a supplied title 1`] = ` 4 |

7 | A GRANDstack Title 8 |

9 | `; 10 | 11 | exports[`GRANDstack Title component should render without any children 1`] = ` 12 |

15 | `; 16 | -------------------------------------------------------------------------------- /app/grandstack-demo/components/UserCount/UserCount.styles.tsx: -------------------------------------------------------------------------------- 1 | // Material UI 2 | import { makeStyles } from "@material-ui/core/styles" 3 | 4 | export const useStyles = makeStyles({ 5 | depositContext: { 6 | flex: 1, 7 | }, 8 | navLink: { 9 | textDecoration: "none", 10 | }, 11 | }) 12 | -------------------------------------------------------------------------------- /app/grandstack-demo/components/UserCount/UserCount.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import TestRenderer, { act } from "react-test-renderer" 3 | import { MockedProvider } from "@apollo/client/testing" 4 | 5 | import UserCount, { GET_USER_COUNT_QUERY } from "./UserCount" 6 | 7 | describe("GRANDstack UserCount component", () => { 8 | it("should render without an error when loading data", async () => { 9 | const expectedTotalUsers = 4 10 | 11 | // Define our Apollo request 12 | const renderRequest = { 13 | request: { 14 | query: GET_USER_COUNT_QUERY, 15 | variables: {}, 16 | }, 17 | result: { 18 | data: { userCount: expectedTotalUsers }, 19 | }, 20 | } 21 | 22 | // Define our mock response(s) 23 | const gqlMocks = [renderRequest] 24 | 25 | // Render component 26 | const component = TestRenderer.create( 27 | 28 | 29 | 30 | ) 31 | 32 | // NOTE: We are NOT advancing to the next tick in the event loop; we expect to see a loading state on the first render 33 | 34 | // Verify loading state 35 | expect(component.toJSON()).toMatchSnapshot() 36 | }) 37 | 38 | it("should render the Total Users chart after receiving data", async () => { 39 | const expectedTotalUsers = 4 40 | 41 | // Define our Apollo request 42 | const renderRequest = { 43 | request: { 44 | query: GET_USER_COUNT_QUERY, 45 | variables: {}, 46 | }, 47 | result: { 48 | data: { userCount: expectedTotalUsers }, 49 | }, 50 | } 51 | 52 | // Define our mock response(s) 53 | const gqlMocks = [renderRequest] 54 | 55 | // Render component 56 | const component = TestRenderer.create( 57 | 58 | 59 | 60 | ) 61 | 62 | // Advance to the next tick in the event loop so our chart can render 63 | await act(() => { 64 | return new Promise((resolve) => { 65 | setTimeout(resolve, 0) 66 | }) 67 | }) 68 | 69 | // Generate our result string 70 | const p = component.root.findAllByType("p") 71 | const result = p 72 | .map((obj) => { 73 | return obj.children 74 | }) 75 | .join(" ") 76 | 77 | // Verify success state 78 | expect(result).toContain(`${expectedTotalUsers} users found`) 79 | }) 80 | 81 | describe("should display an error message if", () => { 82 | it("there is a network request failure", async () => { 83 | // Define our Apollo request 84 | const renderRequest = { 85 | request: { 86 | query: GET_USER_COUNT_QUERY, 87 | variables: {}, 88 | }, 89 | error: new Error("Simulated network or HTTP error"), 90 | } 91 | 92 | // Define our mock response(s) 93 | const gqlMocks = [renderRequest] 94 | 95 | // Verify success state 96 | const component = TestRenderer.create( 97 | 98 | 99 | 100 | ) 101 | 102 | // Advance to the next tick in the event loop so our chart can render 103 | await act(() => { 104 | return new Promise((resolve) => { 105 | setTimeout(resolve, 0) 106 | }) 107 | }) 108 | 109 | // Verify 110 | const tree = component.toJSON() 111 | expect(tree.children).toContain("Simulated network or HTTP error") 112 | }) 113 | it("our GraphQL server successfully processed our request and has returned with one or more error(s) in the result", async () => { 114 | // Define our Apollo request 115 | const renderRequest = { 116 | request: { 117 | query: GET_USER_COUNT_QUERY, 118 | variables: {}, 119 | }, 120 | result: { 121 | // REVISIT: Create a pull request - errors should be able to be defined on the GraphQL result object because it can contain data and errors 122 | // https://www.apollographql.com/docs/react/development-testing/testing/#graphql-errors 123 | errors: [ 124 | new Error("Simulated GraphQL server response with an error"), 125 | ], 126 | }, 127 | } 128 | 129 | // Define our mock response(s) 130 | const gqlMocks = [renderRequest] 131 | 132 | // Verify success state 133 | const component = TestRenderer.create( 134 | 135 | 136 | 137 | ) 138 | 139 | // Advance to the next tick in the event loop so our chart can render 140 | await act(() => { 141 | return new Promise((resolve) => { 142 | setTimeout(resolve, 0) 143 | }) 144 | }) 145 | 146 | // Verify 147 | const tree = component.toJSON() 148 | expect(tree.children).toContain( 149 | "Simulated GraphQL server response with an error" 150 | ) 151 | }) 152 | }) 153 | }) 154 | -------------------------------------------------------------------------------- /app/grandstack-demo/components/UserCount/UserCount.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react" 2 | import { useQuery } from "@apollo/react-hooks" 3 | import gql from "graphql-tag" 4 | 5 | // Components 6 | import Title from "../Title/Title" 7 | 8 | // Material UI 9 | import { useStyles } from "./UserCount.styles" 10 | import { Typography } from "@material-ui/core" 11 | 12 | export const GET_USER_COUNT_QUERY = gql` 13 | { 14 | userCount 15 | } 16 | ` 17 | 18 | const UserCount: FC = () => { 19 | const classes = useStyles() 20 | 21 | const { loading, error, data } = useQuery(GET_USER_COUNT_QUERY) 22 | if (error) return

{error.message}

23 | 24 | return ( 25 | <> 26 | Total Users 27 | 28 | {loading ? "Loading..." : data.userCount} 29 | 30 | 31 | users found 32 | 33 | 34 | ) 35 | } 36 | export default UserCount 37 | -------------------------------------------------------------------------------- /app/grandstack-demo/components/UserCount/__snapshots__/UserCount.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`GRANDstack UserCount component should render without an error when loading data 1`] = ` 4 | Array [ 5 |

8 | Total Users 9 |

, 10 |

13 | Loading... 14 |

, 15 |

18 | users found 19 |

, 20 | ] 21 | `; 22 | -------------------------------------------------------------------------------- /app/grandstack-demo/layout/Footer/Footer.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react" 2 | 3 | // Material UI 4 | import { Box, Link as MUILink, Typography } from "@material-ui/core" 5 | 6 | const Footer: FC = () => { 7 | return ( 8 | 9 | 10 | {"Copyright © "} 11 | {new Date().getFullYear()}{" "} 12 | 13 | Robert J Brennan 14 | 15 | {". All rights reserved."} 16 | 17 | 18 | ) 19 | } 20 | export default Footer 21 | -------------------------------------------------------------------------------- /app/grandstack-demo/layout/Header/Header.styles.tsx: -------------------------------------------------------------------------------- 1 | // Material UI 2 | import { makeStyles } from "@material-ui/core/styles" 3 | 4 | // Styling 5 | export const useStyles = makeStyles((theme) => ({ 6 | toolbar: { 7 | paddingRight: 24, 8 | }, 9 | appBar: { 10 | transition: theme.transitions.create(["width", "margin"], { 11 | easing: theme.transitions.easing.sharp, 12 | duration: theme.transitions.duration.leavingScreen, 13 | }), 14 | }, 15 | title: { 16 | flexGrow: 1, 17 | }, 18 | appBarImage: { 19 | maxHeight: "75px", 20 | paddingRight: "20px", 21 | }, 22 | })) 23 | -------------------------------------------------------------------------------- /app/grandstack-demo/layout/Header/Header.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react" 2 | import Head from "next/head" 3 | import Link from "next/link" 4 | import clsx from "clsx" 5 | 6 | import { dependencies } from "../../../package.json" 7 | 8 | // Material UI 9 | import { useStyles } from "./Header.styles" 10 | import { AppBar, CssBaseline, Toolbar, Typography } from "@material-ui/core" 11 | 12 | const Header: FC = () => { 13 | const classes = useStyles() 14 | 15 | const APP_TITLE = `Next.js GRANDstack Starter with @neo4j/graphql ${dependencies["@neo4j/graphql"]} and TypeScript` 16 | const APP_DESCRIPTION = `A sample ${APP_TITLE}` 17 | const APP_URL = "https://nextjs-grandstack-starter-typescript.vercel.app" 18 | const APP_TYPE = "website" 19 | const APP_LOGO = "img/grandstack.png" 20 | const APP_LOGO_URL = `${APP_URL}/${APP_LOGO}` 21 | const APP_TWITTER_ACCOUNT = "therobbrennan" 22 | const KEYWORDS = "nextjs, react, grandstack, neo4j, typescript, apollo" 23 | 24 | return ( 25 | <> 26 | 27 | {APP_TITLE} 28 | 33 | 34 | 35 | 36 | {/* Open Graph */} 37 | 42 | 43 | 44 | 45 | 46 | {/* Twitter */} 47 | 52 | 57 | 62 | 67 | 68 | 69 | 70 | 71 | 72 | GRANDstack logo 77 | 78 | 85 | {APP_TITLE} 86 | 87 | 88 | 89 | 90 | ) 91 | } 92 | export default Header 93 | -------------------------------------------------------------------------------- /app/grandstack-demo/layout/Layout.styles.tsx: -------------------------------------------------------------------------------- 1 | // Material UI 2 | import { makeStyles } from "@material-ui/core/styles" 3 | 4 | // Styling 5 | export const useStyles = makeStyles((theme) => ({ 6 | root: { 7 | display: "flex", 8 | }, 9 | appBarSpacer: theme.mixins.toolbar, 10 | content: { 11 | flexGrow: 1, 12 | height: "100vh", 13 | overflow: "auto", 14 | }, 15 | container: { 16 | paddingTop: theme.spacing(4), 17 | paddingBottom: theme.spacing(4), 18 | }, 19 | })) 20 | -------------------------------------------------------------------------------- /app/grandstack-demo/layout/Layout.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react" 2 | 3 | // Material UI 4 | import { useStyles } from "./Layout.styles" 5 | import { Container } from "@material-ui/core" 6 | import Header from "./Header/Header" 7 | import Footer from "./Footer/Footer" 8 | 9 | interface ILayout { 10 | children?: JSX.Element[] | JSX.Element | string | null 11 | } 12 | 13 | const Layout: FC = ({ children }) => { 14 | const classes = useStyles() 15 | 16 | return ( 17 |
18 |
19 |
20 |
21 | 22 | {children} 23 |
24 | 25 |
26 |
27 | ) 28 | } 29 | export default Layout 30 | -------------------------------------------------------------------------------- /app/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: [""], 3 | moduleFileExtensions: ["js", "ts", "tsx", "json"], 4 | setupFiles: ["/jest.setup.js", "dotenv/config"], 5 | testPathIgnorePatterns: [ 6 | "[/\\\\](build|docs|node_modules|.next|.vercel|coverage)[/\\\\]", 7 | ], 8 | transformIgnorePatterns: ["[/\\\\]node_modules[/\\\\].+\\.(ts|tsx)$"], 9 | transform: { 10 | "^.+\\.(ts|tsx)$": "babel-jest", 11 | }, 12 | } 13 | -------------------------------------------------------------------------------- /app/jest.setup.js: -------------------------------------------------------------------------------- 1 | const enzyme = require("enzyme") 2 | const Adapter = require("enzyme-adapter-react-16") 3 | 4 | enzyme.configure({ adapter: new Adapter() }) 5 | -------------------------------------------------------------------------------- /app/neo4j/__seed__/db.cypher: -------------------------------------------------------------------------------- 1 | // OPTIONAL: Clear previously defined constraints 2 | // DROP CONSTRAINT ON (node:User) ASSERT (node.userId) IS UNIQUE; 3 | // DROP CONSTRAINT ON (node:Category) ASSERT (node.name) IS UNIQUE; 4 | // DROP CONSTRAINT ON (node:Review) ASSERT (node.reviewId) IS UNIQUE; 5 | // DROP CONSTRAINT ON (node:Business) ASSERT (node.businessId) IS UNIQUE; 6 | 7 | // Generated using "CALL apoc.export.cypher.all(null);" in Cypher to export all Cypher statements in a single row in the cypherStatements column based on the original GRANDstack Database Seed creation 8 | CREATE CONSTRAINT ON (node:User) ASSERT (node.userId) IS UNIQUE; 9 | CREATE CONSTRAINT ON (node:Category) ASSERT (node.name) IS UNIQUE; 10 | CREATE CONSTRAINT ON (node:Review) ASSERT (node.reviewId) IS UNIQUE; 11 | CREATE CONSTRAINT ON (node:Business) ASSERT (node.businessId) IS UNIQUE; 12 | 13 | CALL db.awaitIndexes(300); 14 | 15 | UNWIND [{reviewId:"r4", properties:{date:date('2017-11-13'), text:"", stars:5}}, 16 | {reviewId:"r8", properties:{date:date('2018-08-11'), text:"", stars:5}}, 17 | {reviewId:"r11", properties:{date:date('2016-03-04'), text:"Awesome full-service car wash. Love Ducky's!", stars:5}}, 18 | {reviewId:"r7", properties:{date:date('2015-08-29'), text:"Not a great selection of books, but fortunately the inter-library loan system is good. Wifi is quite slow. Not many comfortable places to site and read. Looking forward to the new building across the street in 2020!", stars:3}}, 19 | {reviewId:"r3", properties:{date:date('2018-09-10'), text:"", stars:3}}, 20 | {reviewId:"r12", properties:{date:date('2015-09-01'), text:"The world's leading graph database HQ!", stars:5}}, 21 | {reviewId:"r9", properties:{date:date('2016-11-21'), text:"", stars:5}}, 22 | {reviewId:"r2", properties:{date:date('2016-07-14'), text:"", stars:5}}, 23 | {reviewId:"r5", properties:{date:date('2018-01-03'), text:"Best breakfast sandwich at the Farmer's Market. Always get the works.", stars:4}}, 24 | {reviewId:"r1", properties:{date:date('2016-01-03'), text:"Great IPA selection!", stars:4}}, 25 | {reviewId:"r10", properties:{date:date('2015-12-15'), text:"", stars:4}}, 26 | {reviewId:"r6", properties:{date:date('2018-03-24'), text:"", stars:4}}] 27 | AS row 28 | CREATE (n:Review{reviewId: row.reviewId}) SET n += row.properties; 29 | 30 | UNWIND [{businessId:"b2", properties:{address:"1151 W Broadway St", city:"Missoula", name:"Imagine Nation Brewing", location:point({x: -114.009628, y: 46.876672, crs: 'wgs-84'}), state:"MT"}}, 31 | {businessId:"b1", properties:{address:"313 N 1st St W", city:"Missoula", name:"KettleHouse Brewing Co.", location:point({x: -113.995297, y: 46.877981, crs: 'wgs-84'}), state:"MT"}}, 32 | {businessId:"b6", properties:{address:"121 W Broadway St", city:"Missoula", name:"Zootown Brew", location:point({x: -113.995018, y: 46.873985, crs: 'wgs-84'}), state:"MT"}}, 33 | {businessId:"b3", properties:{address:"200 W Pine St", city:"Missoula", name:"Ninja Mike's", location:point({x: -113.995057, y: 46.874029, crs: 'wgs-84'}), state:"MT"}}, 34 | {businessId:"b7", properties:{address:"723 California Dr", city:"Burlingame", name:"Hanabi", location:point({x: -122.351519, y: 37.582598, crs: 'wgs-84'}), state:"CA"}}, 35 | {businessId:"b4", properties:{address:"201 E Front St", city:"Missoula", name:"Market on Front", location:point({x: -113.993633, y: 46.869824, crs: 'wgs-84'}), state:"MT"}}, 36 | {businessId:"b8", properties:{address:"716 N San Mateo Dr", city:"San Mateo", name:"Ducky's Car Wash", location:point({x: -122.336041, y: 37.575968, crs: 'wgs-84'}), state:"CA"}}, 37 | {businessId:"b5", properties:{address:"301 E Main St", city:"Missoula", name:"Missoula Public Library", location:point({x: -113.990976, y: 46.870035, crs: 'wgs-84'}), state:"MT"}}, 38 | {businessId:"b9", properties:{address:"111 E 5th Ave", city:"San Mateo", name:"Neo4j", location:point({x: -122.322269, y: 37.563534, crs: 'wgs-84'}), state:"CA"}}] 39 | AS row 40 | CREATE (n:Business{businessId: row.businessId}) SET n += row.properties; 41 | 42 | UNWIND [{name:"Car Wash", properties:{}}, {name:"Brewery", properties:{}}, 43 | {name:"Beer", properties:{}}, {name:"Breakfast", properties:{}}, {name:"Deli", properties:{}}, 44 | {name:"Cafe", properties:{}}, {name:"Coffee", properties:{}}, {name:"Graph Database", properties:{}}, 45 | {name:"Library", properties:{}}, {name:"Ramen", properties:{}}, {name:"Restaurant", properties:{}}] 46 | AS row 47 | CREATE (n:Category{name: row.name}) SET n += row.properties; 48 | 49 | UNWIND [{userId:"u4", properties:{name:"Angie"}}, 50 | {userId:"u2", properties:{name:"Bob"}}, 51 | {userId:"u3", properties:{name:"Jenny"}}, 52 | {userId:"u1", properties:{name:"Will"}}] 53 | AS row 54 | CREATE (n:User{userId: row.userId}) SET n += row.properties; 55 | 56 | UNWIND [{start: {businessId:"b9"}, end: {name:"Graph Database"}, properties:{}}, 57 | {start: {businessId:"b2"}, end: {name:"Brewery"}, properties:{}}, 58 | {start: {businessId:"b2"}, end: {name:"Beer"}, properties:{}}, 59 | {start: {businessId:"b1"}, end: {name:"Brewery"}, properties:{}}, 60 | {start: {businessId:"b1"}, end: {name:"Beer"}, properties:{}}, 61 | {start: {businessId:"b7"}, end: {name:"Ramen"}, properties:{}}, 62 | {start: {businessId:"b7"}, end: {name:"Restaurant"}, properties:{}}, 63 | {start: {businessId:"b8"}, end: {name:"Car Wash"}, properties:{}}, 64 | {start: {businessId:"b6"}, end: {name:"Coffee"}, properties:{}}, 65 | {start: {businessId:"b4"}, end: {name:"Breakfast"}, properties:{}}, 66 | {start: {businessId:"b4"}, end: {name:"Deli"}, properties:{}}, 67 | {start: {businessId:"b4"}, end: {name:"Cafe"}, properties:{}}, 68 | {start: {businessId:"b4"}, end: {name:"Restaurant"}, properties:{}}, 69 | {start: {businessId:"b4"}, end: {name:"Coffee"}, properties:{}}, 70 | {start: {businessId:"b5"}, end: {name:"Library"}, properties:{}}, 71 | {start: {businessId:"b3"}, end: {name:"Breakfast"}, properties:{}}, 72 | {start: {businessId:"b3"}, end: {name:"Restaurant"}, properties:{}}] 73 | AS row 74 | MATCH (start:Business{businessId: row.start.businessId}) 75 | MATCH (end:Category{name: row.end.name}) 76 | CREATE (start)-[r:IN_CATEGORY]->(end) SET r += row.properties; 77 | 78 | UNWIND [{start: {userId:"u2"}, end: {reviewId:"r10"}, properties:{}}, 79 | {start: {userId:"u4"}, end: {reviewId:"r8"}, properties:{}}, 80 | {start: {userId:"u1"}, end: {reviewId:"r7"}, properties:{}}, 81 | {start: {userId:"u3"}, end: {reviewId:"r2"}, properties:{}}, 82 | {start: {userId:"u1"}, end: {reviewId:"r5"}, properties:{}}, 83 | {start: {userId:"u2"}, end: {reviewId:"r6"}, properties:{}}, 84 | {start: {userId:"u3"}, end: {reviewId:"r9"}, properties:{}}, 85 | {start: {userId:"u4"}, end: {reviewId:"r3"}, properties:{}}, 86 | {start: {userId:"u1"}, end: {reviewId:"r12"}, properties:{}}, 87 | {start: {userId:"u3"}, end: {reviewId:"r4"}, properties:{}}, 88 | {start: {userId:"u1"}, end: {reviewId:"r1"}, properties:{}}, 89 | {start: {userId:"u1"}, end: {reviewId:"r11"}, properties:{}}] 90 | AS row 91 | MATCH (start:User{userId: row.start.userId}) 92 | MATCH (end:Review{reviewId: row.end.reviewId}) 93 | CREATE (start)-[r:WROTE]->(end) SET r += row.properties; 94 | 95 | UNWIND [{start: {reviewId:"r1"}, end: {businessId:"b1"}, properties:{}}, 96 | {start: {reviewId:"r7"}, end: {businessId:"b5"}, properties:{}}, 97 | {start: {reviewId:"r10"}, end: {businessId:"b2"}, properties:{}}, 98 | {start: {reviewId:"r6"}, end: {businessId:"b4"}, properties:{}}, 99 | {start: {reviewId:"r3"}, end: {businessId:"b2"}, properties:{}}, 100 | {start: {reviewId:"r5"}, end: {businessId:"b3"}, properties:{}}, 101 | {start: {reviewId:"r8"}, end: {businessId:"b6"}, properties:{}}, 102 | {start: {reviewId:"r11"}, end: {businessId:"b8"}, properties:{}}, 103 | {start: {reviewId:"r4"}, end: {businessId:"b3"}, properties:{}}, 104 | {start: {reviewId:"r9"}, end: {businessId:"b7"}, properties:{}}, 105 | {start: {reviewId:"r12"}, end: {businessId:"b9"}, properties:{}}, 106 | {start: {reviewId:"r2"}, end: {businessId:"b1"}, properties:{}}] 107 | AS row 108 | MATCH (start:Review{reviewId: row.start.reviewId}) 109 | MATCH (end:Business{businessId: row.end.businessId}) 110 | CREATE (start)-[r:REVIEWS]->(end) SET r += row.properties; 111 | -------------------------------------------------------------------------------- /app/neo4j/db.test.ts: -------------------------------------------------------------------------------- 1 | import { DEFAULT_NEO4J_DESKTOP, driver } from "./db" 2 | 3 | describe("Neo4j database driver", () => { 4 | describe("should have Neo4j Desktop settings", () => { 5 | it("exported as DEFAULT_NEO4J_DESKTOP", () => { 6 | expect(DEFAULT_NEO4J_DESKTOP).toBeDefined() 7 | }) 8 | describe("with settings defined for", () => { 9 | const { URI, USER, PASSWORD } = DEFAULT_NEO4J_DESKTOP 10 | it("the URI of the Neo4j database", () => { 11 | expect(URI).toBeDefined() 12 | }) 13 | it("the user account to access the Neo4j database", () => { 14 | expect(USER).toBeDefined() 15 | }) 16 | it("the password associated with the user account", () => { 17 | expect(PASSWORD).toBeDefined() 18 | }) 19 | }) 20 | }) 21 | describe("should export a Neo4j driver that", () => { 22 | it("has been defined", () => { 23 | expect(driver).toBeDefined() 24 | }) 25 | describe("has been configured with the intended settings for", () => { 26 | const expectedURI = process.env.NEO4J_URI.replace("bolt://", "") 27 | const expectedUser = process.env.NEO4J_USER 28 | const expectedPassword = process.env.NEO4J_PASSWORD 29 | 30 | const { _address, _authToken, _config } = driver() 31 | 32 | it("the URI of the Neo4j database", () => { 33 | expect(_address._stringValue).toEqual(expectedURI) 34 | }) 35 | it("the user account to access the Neo4j database", () => { 36 | expect(_authToken.principal).toEqual(expectedUser) 37 | }) 38 | it("the password associated with the user account", () => { 39 | expect(_authToken.credentials).toEqual(expectedPassword) 40 | }) 41 | it("encryption has been defined", () => { 42 | expect(_config.encrypted).toBeDefined() 43 | }) 44 | }) 45 | describe("has been configured with Neo4j Desktop settings", () => { 46 | const { URI, USER, PASSWORD, isEncrypted } = DEFAULT_NEO4J_DESKTOP 47 | const expectedURI = URI.replace("bolt://", "") 48 | const expectedUser = USER 49 | const expectedPassword = PASSWORD 50 | 51 | const { _address, _authToken, _config } = driver( 52 | URI, 53 | USER, 54 | PASSWORD, 55 | isEncrypted 56 | ) 57 | 58 | it("the URI of the Neo4j database", () => { 59 | expect(_address._stringValue).toEqual(expectedURI) 60 | }) 61 | it("the user account to access the Neo4j database", () => { 62 | expect(_authToken.principal).toEqual(expectedUser) 63 | }) 64 | it("the password associated with the user account", () => { 65 | expect(_authToken.credentials).toEqual(expectedPassword) 66 | }) 67 | describe("when the neo4jEncryptedConnection parameter", () => { 68 | describe("is undefined", () => { 69 | const { _config } = driver(URI, USER, PASSWORD) 70 | it("encryption has been set to ENCRYPTION_OFF", () => { 71 | expect(_config.encrypted).toEqual("ENCRYPTION_OFF") 72 | }) 73 | }) 74 | describe("is set to the string value 'false'", () => { 75 | const { _config } = driver(URI, USER, PASSWORD, "false") 76 | it("encryption has been set to ENCRYPTION_OFF", () => { 77 | expect(_config.encrypted).toEqual("ENCRYPTION_OFF") 78 | }) 79 | }) 80 | describe("is set to the boolean value 'false'", () => { 81 | const { _config } = driver(URI, USER, PASSWORD, false) 82 | it("encryption has been set to ENCRYPTION_OFF", () => { 83 | expect(_config.encrypted).toEqual("ENCRYPTION_OFF") 84 | }) 85 | }) 86 | describe("is set to the string value 'true'", () => { 87 | const { _config } = driver(URI, USER, PASSWORD, "true") 88 | it("encryption has been set to ENCRYPTION_ON", () => { 89 | expect(_config.encrypted).toEqual("ENCRYPTION_ON") 90 | }) 91 | }) 92 | describe("is set to the boolean value 'true'", () => { 93 | const { _config } = driver(URI, USER, PASSWORD, true) 94 | it("encryption has been set to ENCRYPTION_ON", () => { 95 | expect(_config.encrypted).toEqual("ENCRYPTION_ON") 96 | }) 97 | }) 98 | }) 99 | }) 100 | }) 101 | }) 102 | -------------------------------------------------------------------------------- /app/neo4j/db.ts: -------------------------------------------------------------------------------- 1 | import * as neo4j from "neo4j-driver" 2 | 3 | // Neo4j Desktop settings 4 | export const DEFAULT_NEO4J_DESKTOP = { 5 | URI: "bolt://localhost:7687", 6 | USER: "neo4j", 7 | PASSWORD: "neo4j", 8 | isEncrypted: false, // v3.5 does not use encrypted connections, but 4.0 does 9 | } 10 | 11 | // Create a configured neo4j driver instance (this doesn't start a session) 12 | export const driver = ( 13 | neo4jURI = process.env.NEO4J_URI, 14 | neo4jUser = process.env.NEO4J_USER, 15 | neo4jPassword = process.env.NEO4J_PASSWORD, 16 | neo4jEncryptedConnection = process.env.NEO4J_ENCRYPTED 17 | ) => { 18 | // REMEMBER: !!('false') IS true; we need to explicitly check for a false string value 19 | const isEncrypted = 20 | !!neo4jEncryptedConnection && neo4jEncryptedConnection != "false" 21 | 22 | return neo4j.driver(neo4jURI, neo4j.auth.basic(neo4jUser, neo4jPassword), { 23 | encrypted: isEncrypted ? "ENCRYPTION_ON" : "ENCRYPTION_OFF", 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /app/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /app/next.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path") 2 | 3 | // Plug-ins 4 | const withPlugins = require("next-compose-plugins") 5 | const withBundleAnalyzer = require("@next/bundle-analyzer")({ 6 | enabled: process.env.ANALYZE === "true", 7 | }) 8 | const withImages = require("next-images") 9 | 10 | // Combine an array of plug-ins with a Next.js configuration object 11 | module.exports = withPlugins([[withImages], [withBundleAnalyzer]], { 12 | // Custom webpack configuration goes here 🤓 13 | webpack: (config, { isServer }) => { 14 | // Fixes npm packages that depend on `fs` module 15 | if (!isServer) { 16 | config.node = { 17 | fs: "empty", 18 | } 19 | } 20 | 21 | // Necessary for resolving our styles, etc 22 | config.resolve.modules.push(path.resolve("./")) 23 | 24 | // Return our custom webpack configuration 25 | return config 26 | }, 27 | }) 28 | -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs-grandstack-starter", 3 | "version": "0.0.0", 4 | "description": "GRANDstack starter powered by Next.js", 5 | "keywords": [], 6 | "author": "Rob Brennan (therobbrennan.com)", 7 | "license": "ISC", 8 | "scripts": { 9 | "analyze": "ANALYZE=true npm run build", 10 | "dev": "next dev", 11 | "build": "next build", 12 | "start": "next start", 13 | "test": "jest", 14 | "test:ci": "jest --ci", 15 | "test:coverage": "jest --coverage", 16 | "test:coverage:view": "jest --coverage && open coverage/lcov-report/index.html", 17 | "test:verbose": "jest --verbose", 18 | "test:watch": "jest --watch", 19 | "test:watchAll": "jest --watchAll" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/TheRobBrennan/nextjs-grandstack-starter.git" 24 | }, 25 | "bugs": { 26 | "url": "https://github.com/TheRobBrennan/nextjs-grandstack-starter/issues" 27 | }, 28 | "homepage": "https://github.com/TheRobBrennan/nextjs-grandstack-starter#readme", 29 | "dependencies": { 30 | "@apollo/client": "^3.1.3", 31 | "@apollo/react-hooks": "^4.0.0", 32 | "@material-ui/core": "^4.11.0", 33 | "@material-ui/icons": "^4.9.1", 34 | "@neo4j/graphql": "^1.0.0-alpha.2", 35 | "@next/bundle-analyzer": "^9.5.2", 36 | "apollo-server-micro": "^2.16.1", 37 | "clsx": "^1.1.1", 38 | "cross-fetch": "^3.0.5", 39 | "graphql-tag": "^2.11.0", 40 | "graphql-tools": "^6.0.18", 41 | "neo4j-driver": "^4.1.1", 42 | "next": "^10.0.5", 43 | "next-compose-plugins": "^2.2.0", 44 | "next-images": "^1.4.1", 45 | "react": "^17.0.1", 46 | "react-dom": "^17.0.1", 47 | "recharts": "^1.8.5" 48 | }, 49 | "devDependencies": { 50 | "@types/enzyme": "^3.10.5", 51 | "@types/jest": "^26.0.10", 52 | "@types/node": "^14.6.0", 53 | "@types/react": "^16.9.47", 54 | "apollo-server-testing": "^2.17.0", 55 | "dotenv": "^8.2.0", 56 | "enzyme": "^3.11.0", 57 | "enzyme-adapter-react-16": "^1.15.3", 58 | "jest": "^26.4.2", 59 | "node-mocks-http": "^1.9.0", 60 | "react-test-renderer": "^17.0.1", 61 | "typescript": "^4.1.3" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { ApolloProvider } from "@apollo/client" 2 | import { useApollo } from "../apollo/client" 3 | 4 | export default function App({ Component, pageProps }) { 5 | const apolloClient = useApollo(pageProps.initialApolloState) 6 | 7 | return ( 8 | 9 | 10 | 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /app/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import Document, { Html, Head, Main, NextScript } from "next/document" 3 | import { ServerStyleSheets } from "@material-ui/styles" 4 | 5 | import { GOOGLE_ANALYTICS_TRACKING_ID } from "../analytics/google/googleAnalytics" 6 | 7 | class MyDocument extends Document { 8 | render() { 9 | // Fixed missing document components as advised in https://github.com/vercel/next.js/blob/master/errors/missing-document-component.md 10 | return ( 11 | 12 | 13 | 14 | 15 | {/* Global Site Tag (gtag.js) - Google Analytics */} 16 |