├── .prettierrc
├── .yarnclean
├── .prettierignore
├── web
├── components
│ ├── Heading
│ │ └── index.tsx
│ └── WithApollo
│ │ ├── client.ts
│ │ └── index.tsx
├── next-env.d.ts
├── graphql
│ ├── fragments
│ │ └── userInfo.graphql
│ ├── queries
│ │ └── getViewer.graphql
│ ├── schema.generated.graphql
│ └── schema
│ │ └── index.ts
├── database
│ └── datamodel.prisma
├── types
│ ├── graphql.ts
│ ├── process.d.ts
│ ├── styled.d.ts
│ └── next-nprogress.d.ts
├── .babelrc
├── prisma.yml
├── pages
│ ├── api
│ │ ├── auth
│ │ │ ├── logout
│ │ │ │ └── index.ts
│ │ │ └── google
│ │ │ │ ├── index.ts
│ │ │ │ └── callback
│ │ │ │ └── index.ts
│ │ └── graphql.ts
│ ├── _document.tsx
│ ├── _app.tsx
│ └── index.tsx
├── next.config.js
├── docker-compose.yml
├── tsconfig.json
├── utils
│ └── middlewares.ts
├── auth
│ └── passport.ts
├── package.json
└── theme.tsx
├── .example.env
├── .gitignore
├── now.json
├── .github
└── workflows
│ └── main.yml
├── graphql-codegen.yml
├── package.json
└── README.md
/.prettierrc:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/.yarnclean:
--------------------------------------------------------------------------------
1 | @types/react-native
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | web/.next
2 | .vscode
3 |
4 | *.generated.*
5 | **/generated/**
6 |
--------------------------------------------------------------------------------
/web/components/Heading/index.tsx:
--------------------------------------------------------------------------------
1 | import { Heading } from "rebass";
2 |
3 | export default Heading;
4 |
--------------------------------------------------------------------------------
/web/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
--------------------------------------------------------------------------------
/web/graphql/fragments/userInfo.graphql:
--------------------------------------------------------------------------------
1 | fragment userInfo on User {
2 | id
3 | avatarUrl
4 | name
5 | }
6 |
--------------------------------------------------------------------------------
/web/graphql/queries/getViewer.graphql:
--------------------------------------------------------------------------------
1 | #import "../fragments/userInfo"
2 |
3 | query getViewer {
4 | viewer {
5 | ...userInfo
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/web/database/datamodel.prisma:
--------------------------------------------------------------------------------
1 | type User {
2 | id: ID! @id
3 | name: String!
4 | googleId: ID! @unique
5 | email: String
6 | avatarUrl: String
7 | }
8 |
--------------------------------------------------------------------------------
/web/types/graphql.ts:
--------------------------------------------------------------------------------
1 | import { Prisma } from "../database/generated/client";
2 |
3 | export interface Context {
4 | viewerId?: string;
5 | prisma: Prisma;
6 | }
7 |
--------------------------------------------------------------------------------
/web/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "next/babel",
4 | "@zeit/next-typescript/babel"
5 | ],
6 | "plugins": [
7 | ["styled-components", { "ssr": true }]
8 | ]
9 | }
--------------------------------------------------------------------------------
/web/prisma.yml:
--------------------------------------------------------------------------------
1 | endpoint: http://localhost:4466
2 | datamodel: ./database/datamodel.prisma
3 | generate:
4 | - generator: typescript-client
5 | output: ./database/generated/client/
--------------------------------------------------------------------------------
/web/types/process.d.ts:
--------------------------------------------------------------------------------
1 | declare namespace NodeJS {
2 | interface Process {
3 | browser: boolean;
4 | }
5 |
6 | export interface Global {
7 | fetch: typeof fetch;
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/.example.env:
--------------------------------------------------------------------------------
1 | # Authentication related secrets
2 | GOOGLE_CLIENT_ID= # Get from the Google dev console
3 | GOOGLE_CLIENT_SECRET= # Get from the Google dev console
4 | SESSION_SECRET= # A random, long string
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .pnp
3 | .pnp.js
4 |
5 | .DS_Store
6 | .env*
7 | .next
8 |
9 | *.generated.*
10 | web/database/generated
11 | !web/graphql/schema.generated.graphql
12 |
13 | npm-debug.log*
14 | yarn-debug.log*
15 | yarn-error.log*
16 |
--------------------------------------------------------------------------------
/web/pages/api/auth/logout/index.ts:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import middlewares from "../../../../utils/middlewares";
3 |
4 | const app = express();
5 |
6 | middlewares(app);
7 |
8 | app.get("*", (req, res) => {
9 | req.logout();
10 | res.redirect("/");
11 | });
12 |
13 | export default app;
14 |
--------------------------------------------------------------------------------
/now.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 2,
3 | "builds": [
4 | {
5 | "src": "web/next.config.js",
6 | "use": "@now/next"
7 | }
8 | ],
9 | "routes": [
10 | {
11 | "src": "/(.*)",
12 | "dest": "/web/$1"
13 | }
14 | ],
15 | "github": {
16 | "silent": true
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/web/graphql/schema.generated.graphql:
--------------------------------------------------------------------------------
1 | ### This file was autogenerated by GraphQL Nexus
2 | ### Do not make changes to this file directly
3 |
4 |
5 | type Query {
6 | user(where: UserWhereUniqueInput!): User
7 | viewer: User
8 | }
9 |
10 | type User {
11 | avatarUrl: String
12 | id: ID!
13 | name: String!
14 | }
15 |
16 | input UserWhereUniqueInput {
17 | googleId: ID
18 | id: ID
19 | }
20 |
--------------------------------------------------------------------------------
/web/types/styled.d.ts:
--------------------------------------------------------------------------------
1 | // Strongly type the styled-components theme
2 | import {} from "styled-components";
3 | import { CSSProp } from "styled-components";
4 | import theme from "../theme";
5 |
6 | // Enable css prop support globally
7 | declare module "react" {
8 | interface Attributes {
9 | css?: CSSProp | CSSObject;
10 | }
11 | }
12 |
13 | declare module "styled-components" {
14 | type Theme = typeof theme;
15 | export interface DefaultTheme extends Theme {}
16 | }
17 |
--------------------------------------------------------------------------------
/web/next.config.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 | const Dotenv = require("dotenv-webpack");
3 |
4 | module.exports = {
5 | serverless: true,
6 | webpack: config => {
7 | config.plugins = config.plugins || [];
8 |
9 | config.plugins = [
10 | ...config.plugins,
11 |
12 | // Read the .env file
13 | new Dotenv({
14 | path: path.join(__dirname, ".env"),
15 | systemvars: true
16 | })
17 | ];
18 |
19 | return config;
20 | }
21 | };
22 |
--------------------------------------------------------------------------------
/web/types/next-nprogress.d.ts:
--------------------------------------------------------------------------------
1 | declare module "next-nprogress" {
2 | import * as React from "react";
3 | import { NProgressConfigureOptions } from "nprogress";
4 |
5 | const withNprogress = (
6 | duration: number,
7 | options: NProgressConfigureOptions
8 | ) => (Component: React.ReactNode) => React.ReactNode;
9 |
10 | export default withNprogress;
11 | }
12 |
13 | declare module "next-nprogress/component" {
14 | import * as React from "react";
15 | const Component: React.FunctionComponent<{ color: string }>;
16 | export default Component;
17 | }
18 |
--------------------------------------------------------------------------------
/web/pages/api/auth/google/index.ts:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import passport from "passport";
3 | import middlewares from "../../../../utils/middlewares";
4 |
5 | const app = express();
6 |
7 | middlewares(app);
8 |
9 | app.get(
10 | "*",
11 | // Store the redirectUrl from the ?r query param
12 | (req, _, next) => {
13 | if (req.query && req.session && req.query.r)
14 | req.session.redirectUrl = req.query.r;
15 | next();
16 | },
17 | passport.authenticate("google", {
18 | scope: ["profile", "email"]
19 | })
20 | );
21 |
22 | export default app;
23 |
--------------------------------------------------------------------------------
/web/pages/api/auth/google/callback/index.ts:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import passport from "passport";
3 | import middlewares from "../../../../../utils/middlewares";
4 |
5 | const app = express();
6 |
7 | middlewares(app);
8 |
9 | app.get(
10 | "*",
11 | passport.authenticate("google", {
12 | failureRedirect: "/"
13 | }),
14 | (req, res) => {
15 | const redirectUrl = (req.session && req.session.redirectUrl) || "/";
16 | if (req.session) delete req.session.redirectUrl;
17 | res.redirect(typeof redirectUrl === "string" ? redirectUrl : "/");
18 | }
19 | );
20 |
21 | export default app;
22 |
--------------------------------------------------------------------------------
/web/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 | services:
3 | prisma:
4 | image: prismagraphql/prisma:1.34
5 | restart: always
6 | ports:
7 | - '4466:4466'
8 | environment:
9 | PRISMA_CONFIG: |
10 | port: 4466
11 | databases:
12 | default:
13 | connector: mysql
14 | host: mysql
15 | port: 3306
16 | user: root
17 | password: prisma
18 | mysql:
19 | image: mysql:5.7
20 | restart: always
21 | environment:
22 | MYSQL_ROOT_PASSWORD: prisma
23 | volumes:
24 | - mysql:/var/lib/mysql
25 | volumes:
26 | mysql: ~
27 |
--------------------------------------------------------------------------------
/web/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowJs": true,
4 | "allowSyntheticDefaultImports": true,
5 | "jsx": "preserve",
6 | "lib": ["dom", "es2017"],
7 | "module": "esnext",
8 | "moduleResolution": "node",
9 | "noEmit": true,
10 | "noUnusedLocals": true,
11 | "noUnusedParameters": true,
12 | "preserveConstEnums": true,
13 | "removeComments": false,
14 | "sourceMap": true,
15 | "strict": true,
16 | "target": "esnext",
17 | "skipLibCheck": true,
18 | "forceConsistentCasingInFileNames": true,
19 | "esModuleInterop": true,
20 | "resolveJsonModule": true,
21 | "isolatedModules": true
22 | },
23 | "exclude": ["node_modules"],
24 | "include": ["**/*.ts", "**/*.tsx"]
25 | }
26 |
--------------------------------------------------------------------------------
/web/pages/api/graphql.ts:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import { ApolloServer } from "apollo-server-express";
3 | import middlewares from "../../utils/middlewares";
4 | import schema from "../../graphql/schema";
5 | import { prisma } from "../../database/generated/client";
6 |
7 | const app = express();
8 |
9 | middlewares(app);
10 |
11 | const server = new ApolloServer({
12 | schema,
13 | introspection: true,
14 | playground: true,
15 | context: ({ req }) => {
16 | return {
17 | prisma,
18 | viewerId: req.user ? req.user : undefined
19 | };
20 | }
21 | });
22 |
23 | server.applyMiddleware({ app, path: "*" });
24 |
25 | export const config = {
26 | api: {
27 | bodyParser: false
28 | }
29 | };
30 |
31 | export default app;
32 |
--------------------------------------------------------------------------------
/web/pages/_document.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Document, { DocumentContext } from "next/document";
3 | import { ServerStyleSheet } from "styled-components";
4 |
5 | export default class MyDocument extends Document {
6 | static async getInitialProps(ctx: DocumentContext) {
7 | const sheet = new ServerStyleSheet();
8 |
9 | const originalRenderPage = ctx.renderPage;
10 | ctx.renderPage = () =>
11 | originalRenderPage({
12 | enhanceApp: App => props => sheet.collectStyles()
13 | });
14 |
15 | const initialProps = await Document.getInitialProps(ctx);
16 | return {
17 | ...initialProps,
18 | styles: [
19 | ...(Array.isArray(initialProps.styles) ? initialProps.styles : []),
20 | ...sheet.getStyleElement()
21 | ]
22 | };
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on: [push]
4 |
5 | jobs:
6 | test:
7 | name: All the checks
8 | runs-on: ubuntu-latest
9 |
10 | steps:
11 | - name: Checkout repository
12 | uses: actions/checkout@master
13 |
14 | - name: Install Node.js 12.x
15 | uses: actions/setup-node@master
16 | with:
17 | version: 12.x
18 |
19 | - name: Install yarn 1.17.3
20 | run: npm install -g yarn@1.17.3
21 |
22 | - name: Install dependencies
23 | run: yarn install --frozen-lockfile
24 |
25 | - name: Check formatting
26 | run: yarn prettier --check '**/*.{js,json,ts,tsx,graphql,md}'
27 |
28 | - name: Lint
29 | run: yarn lint
30 |
31 | - name: Check types
32 | run: yarn web tsc
33 |
34 | # TODO: Add tests
35 | # - name: Run tests
36 | # run: yarn web test:ci
37 |
38 | - name: Build
39 | run: yarn web build
40 |
--------------------------------------------------------------------------------
/web/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | // @flow
2 | import React from "react";
3 | import App, { Container } from "next/app";
4 | import { ApolloProvider } from "react-apollo";
5 | import NProgress from "next-nprogress/component";
6 | import withNProgress from "next-nprogress";
7 | import { BaseStyles } from "@nice-boys/components";
8 | import withApollo, { ApolloAppProps } from "../components/WithApollo";
9 | import theme from "../theme";
10 |
11 | class MyApp extends App {
12 | render() {
13 | const { Component, pageProps, apolloClient } = this.props;
14 |
15 | return (
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | );
24 | }
25 | }
26 |
27 | // @ts-ignore
28 | export default withNProgress(1000, { showSpinner: false })(withApollo(MyApp));
29 |
--------------------------------------------------------------------------------
/web/utils/middlewares.ts:
--------------------------------------------------------------------------------
1 | import { Express } from "express";
2 | import session from "cookie-session";
3 | import passport from "../auth/passport";
4 |
5 | const TWENTY_FOUR_HOURS = 24 * 60 * 60 * 1000;
6 |
7 | const SESSION_SECRET = process.env.SESSION_SECRET;
8 | if (!SESSION_SECRET)
9 | throw new Error(
10 | "Please provide the SESSION_SECRET env variable via a .env file in the root of the project."
11 | );
12 |
13 | export default (app: Express) => {
14 | app.use(
15 | session({
16 | name: "session",
17 | keys: [SESSION_SECRET],
18 | secure: process.env.NODE_ENV === "production",
19 | signed: true,
20 | sameSite: "strict",
21 | httpOnly: true,
22 | maxAge: TWENTY_FOUR_HOURS
23 | })
24 | );
25 | app.use(passport.initialize());
26 | app.use(passport.session());
27 | // Refresh session on every request
28 | app.use((req, _, next) => {
29 | if (req.session && req.user) {
30 | req.session.lastRequest = Date.now();
31 | }
32 | next();
33 | });
34 | };
35 |
--------------------------------------------------------------------------------
/graphql-codegen.yml:
--------------------------------------------------------------------------------
1 | overwrite: true
2 | schema: web/graphql/schema/index.ts
3 | documents: "web/**/*.graphql"
4 | require:
5 | - ts-node/register
6 | generates:
7 | web/graphql/introspection-result.generated.ts:
8 | plugins:
9 | - add: |
10 | // eslint-disable
11 | // ⚠️ DO NOT EDIT ⚠️
12 | // This file is automatically generated, run yarn run graphql:codegen to update
13 | - "fragment-matcher"
14 | web/graphql/schema-types.generated.ts:
15 | plugins:
16 | - typescript
17 | config:
18 | immutableTypes: false
19 | web/:
20 | preset: near-operation-file
21 | presetConfig:
22 | baseTypesPath: "graphql/schema-types.generated.ts"
23 | extension: ".generated.tsx"
24 | plugins:
25 | - add: |
26 | // eslint-disable
27 | // ⚠️ DO NOT EDIT ⚠️
28 | // This file is automatically generated, run yarn run graphql:codegen to update
29 | - "typescript-operations"
30 | - "typescript-react-apollo"
31 | config:
32 | immutableTypes: false
33 | withHOC: false
34 | withComponent: true
35 | withHooks: false
36 | web/graphql/types.generated.d.ts:
37 | plugins:
38 | - add: |
39 | // eslint-disable
40 | // ⚠️ DO NOT EDIT ⚠️
41 | // This file is automatically generated, run yarn run graphql:codegen to update
42 | - "typescript-graphql-files-modules"
--------------------------------------------------------------------------------
/web/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Flex, Box } from "rebass";
3 | import { PrimaryButton } from "@nice-boys/components";
4 | import { GetViewerComponent } from "../graphql/queries/getViewer.generated";
5 | import Heading from "../components/Heading";
6 |
7 | export default () => {
8 | return (
9 |
10 | {({ data, loading, error }) => {
11 | if (data) {
12 | return (
13 |
14 | {data.viewer && data.viewer.name ? (
15 |
16 |
17 | 👋 Hello, {data.viewer ? data.viewer.name : "anonymous"}!
18 |
19 |
20 |
21 | Log out
22 |
23 |
24 |
25 | ) : (
26 |
27 | Please log in.
28 |
29 | )}
30 |
31 | );
32 | }
33 |
34 | if (loading) return Loading...
;
35 |
36 | if (error) return Error :(
;
37 |
38 | return null;
39 | }}
40 |
41 | );
42 | };
43 |
--------------------------------------------------------------------------------
/web/components/WithApollo/client.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ApolloClient,
3 | InMemoryCache,
4 | HttpLink,
5 | NormalizedCacheObject
6 | } from "apollo-boost";
7 |
8 | let apolloClient: ApolloClient | null = null;
9 |
10 | // Polyfill fetch() on the server (used by apollo-client)
11 | if (!process.browser) {
12 | global.fetch = require("isomorphic-unfetch");
13 | }
14 |
15 | function create(
16 | initialState?: NormalizedCacheObject,
17 | cookie?: string,
18 | host?: string
19 | ) {
20 | return new ApolloClient({
21 | connectToDevTools: process.browser,
22 | ssrMode: !process.browser, // Disables forceFetch on the server (so queries are only run once)
23 | link: new HttpLink({
24 | uri: `${host || ""}/api/graphql`,
25 | credentials: "include",
26 | headers: cookie && {
27 | Cookie: cookie
28 | }
29 | }),
30 | cache: new InMemoryCache().restore(initialState || {})
31 | });
32 | }
33 |
34 | export function getClient(
35 | initialState?: NormalizedCacheObject,
36 | cookie?: string,
37 | host?: string
38 | ) {
39 | // Make sure to create a new client for every server-side request so that data
40 | // isn't shared between connections
41 | if (!process.browser) {
42 | return create(initialState, cookie, host);
43 | }
44 |
45 | // Reuse client on the client-side
46 | if (!apolloClient) {
47 | apolloClient = create(initialState, cookie, host);
48 | }
49 |
50 | return apolloClient;
51 | }
52 |
--------------------------------------------------------------------------------
/web/graphql/schema/index.ts:
--------------------------------------------------------------------------------
1 | import { prisma } from "../../database/generated/client";
2 | import datamodelInfo from "../../database/generated/nexus-prisma";
3 | import { prismaObjectType, makePrismaSchema } from "nexus-prisma";
4 | const path = require("path");
5 |
6 | // @ts-ignore
7 | const User = prismaObjectType({
8 | name: "User",
9 | definition(t) {
10 | t.prismaFields(["name", "id", "avatarUrl"]);
11 | }
12 | });
13 |
14 | const Query = prismaObjectType({
15 | name: "Query",
16 | definition(t) {
17 | t.prismaFields([
18 | {
19 | name: "user",
20 | args: ["where"]
21 | }
22 | ]);
23 | t.field("viewer", {
24 | type: "User",
25 | nullable: true,
26 | resolve: (_, __, ctx) =>
27 | ctx.viewerId ? ctx.prisma.user({ id: ctx.viewerId }) : null
28 | });
29 | }
30 | });
31 |
32 | const outputs = process.env.GENERATE
33 | ? {
34 | schema: path.join(__dirname, "../schema.generated.graphql"),
35 | typegen: path.join(__dirname, "../nexus-schema-types.generated.ts")
36 | }
37 | : {
38 | schema: false,
39 | typegen: false
40 | };
41 |
42 | const schema = makePrismaSchema({
43 | types: [Query, User],
44 | prisma: {
45 | datamodelInfo,
46 | client: prisma
47 | },
48 | outputs,
49 | typegenAutoConfig: {
50 | sources: [
51 | {
52 | source: path.join(__dirname, "../../types/graphql.ts"),
53 | alias: "types"
54 | }
55 | ],
56 | contextType: "types.Context"
57 | }
58 | });
59 |
60 | export default schema;
61 |
--------------------------------------------------------------------------------
/web/auth/passport.ts:
--------------------------------------------------------------------------------
1 | // @flow
2 | import passport from "passport";
3 | import { Strategy as GoogleStrategy } from "passport-google-oauth20";
4 | import { prisma } from "../database/generated/client";
5 |
6 | // Only store the user ID in the cookie and in the req.user property
7 | passport.serializeUser((user: { id: string }, done) => done(null, user.id));
8 | passport.deserializeUser((data, done) => {
9 | done(null, data);
10 | });
11 |
12 | if (!process.env.GOOGLE_CLIENT_ID || !process.env.GOOGLE_CLIENT_SECRET)
13 | throw new Error(
14 | "Please provide the GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET env variables in the .env file in the root of the project."
15 | );
16 |
17 | passport.use(
18 | new GoogleStrategy(
19 | {
20 | clientID: process.env.GOOGLE_CLIENT_ID,
21 | clientSecret: process.env.GOOGLE_CLIENT_SECRET,
22 | callbackURL: "/api/auth/google/callback"
23 | },
24 | async (_, __, profile, done) => {
25 | const existing = await prisma.user({
26 | googleId: profile.id
27 | });
28 | if (existing) return done(undefined, existing);
29 |
30 | return prisma
31 | .createUser({
32 | name:
33 | profile.displayName ||
34 | (profile.name &&
35 | (profile.name.givenName || "") +
36 | (profile.name.middleName || "") +
37 | (profile.name.familyName || "")) ||
38 | "Anonymous",
39 | googleId: profile.id,
40 | email:
41 | Array.isArray(profile.emails) && profile.emails.length > 0
42 | ? profile.emails[0].value
43 | : undefined,
44 | avatarUrl:
45 | Array.isArray(profile.photos) && profile.photos.length > 0
46 | ? profile.photos[0].value
47 | : undefined
48 | })
49 | .then(user => done(undefined, user))
50 | .catch(err => done(err));
51 | }
52 | )
53 | );
54 |
55 | export default passport;
56 |
--------------------------------------------------------------------------------
/web/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "web",
3 | "version": "1.0.0",
4 | "private": true,
5 | "license": "MIT",
6 | "dependencies": {
7 | "@nice-boys/components": "0.0.4",
8 | "@types/next": "^8.0.5",
9 | "@types/nprogress": "^0.2.0",
10 | "@types/react": "^16.8.18",
11 | "@types/react-dom": "^16.8.4",
12 | "@types/rebass": "^3.0.4",
13 | "@types/styled-components": "4.1.18",
14 | "@zeit/next-typescript": "^1.1.1",
15 | "apollo-boost": "^0.4.4",
16 | "apollo-server-micro": "^2.6.9",
17 | "babel-plugin-styled-components": "^1.10.6",
18 | "fork-ts-checker-webpack-plugin": "^1.5.0",
19 | "graphql-tag": "^2.10.1",
20 | "isomorphic-unfetch": "^3.0.0",
21 | "next": "^9.0.1",
22 | "next-nprogress": "^1.4.0",
23 | "react": "^16.8.6",
24 | "react-apollo": "^2.5.6",
25 | "react-dom": "^16.8.6",
26 | "rebass": "^3.1.1",
27 | "styled-components": "^4.3.2",
28 | "styled-normalize": "^8.0.6",
29 | "apollo-server-express": "^2.8.2",
30 | "body-parser": "^1.19.0",
31 | "cookie-parser": "^1.4.4",
32 | "cookie-session": "^1.3.3",
33 | "express": "^4.17.1",
34 | "graphql": "^14.3.1",
35 | "nexus": "^0.12.0-beta.6",
36 | "nexus-prisma": "^0.3.7",
37 | "passport": "^0.4.0",
38 | "passport-google-oauth20": "^2.0.0",
39 | "prisma": "^1.34.1",
40 | "@types/body-parser": "^1.17.1",
41 | "@types/cookie-parser": "^1.4.2",
42 | "@types/cookie-session": "^2.0.37",
43 | "@types/express": "^4.16.1",
44 | "@types/graphql": "^14.2.3",
45 | "@types/passport": "^1.0.0",
46 | "@types/passport-google-oauth20": "^2.0.2",
47 | "nexus-prisma-generate": "^0.3.7"
48 | },
49 | "resolutions": {
50 | "@types/styled-components": "4.1.18",
51 | "styled-jsx": "^3.0.0"
52 | },
53 | "scripts": {
54 | "dev": "next",
55 | "build": "next build",
56 | "db": "docker-compose up",
57 | "db:deploy": "prisma deploy",
58 | "generate:db": "prisma generate && nexus-prisma-generate --client ./database/generated/client --output ./database/generated/nexus-prisma"
59 | },
60 | "devDependencies": {
61 | "@types/rebass": "^3.0.4",
62 | "dotenv-webpack": "^1.7.0"
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/web/theme.tsx:
--------------------------------------------------------------------------------
1 | const darks = {
2 | zero: "#000",
3 | one: "#161718",
4 | two: "#2C2E30",
5 | three: "#44474A",
6 | four: "#5B5F63",
7 | five: "#73787D",
8 | six: "#8A9096",
9 | seven: "#A1A9B0"
10 | };
11 |
12 | const lights = {
13 | one: "#BBC2C9",
14 | two: "#D3DBE3",
15 | three: "#E9ECF0",
16 | four: "#F2F5F7",
17 | five: "#FAFBFC",
18 | six: "#FFF"
19 | };
20 |
21 | const brand = {
22 | one: "#D4E8FC",
23 | two: "#ABD3FC",
24 | three: "#83BFFC",
25 | four: "#5BABFC",
26 | five: "#1A8CFF",
27 | six: "#007EFC",
28 | seven: "#0071E3",
29 | eight: "#0065C9",
30 | nine: "#0058B0",
31 | ten: "#004B96",
32 | eleven: "#003E7D"
33 | };
34 |
35 | const error = {
36 | one: "#FFE3E3",
37 | two: "#F7C3C3",
38 | three: "#F09C9C",
39 | four: "#E87171",
40 | five: "#F05151",
41 | six: "#E84444",
42 | seven: "#DF4040",
43 | eight: "#B53535",
44 | nine: "#9C2E2E",
45 | ten: "#822626",
46 | eleven: "#691F1F"
47 | };
48 |
49 | const success = {
50 | one: "#E4F5EC",
51 | two: "#C5EBD6",
52 | three: "#A2DBBD",
53 | four: "#75D9A3",
54 | five: "#5BCF90",
55 | six: "#44C881",
56 | seven: "#40BD79",
57 | eight: "#32945F",
58 | nine: "#2D8556",
59 | ten: "#28754C",
60 | eleven: "#21613F"
61 | };
62 |
63 | const warn = {
64 | one: "#FFF0D4",
65 | two: "#FFE2AB",
66 | three: "#FFD687",
67 | four: "#FFCE6E",
68 | five: "#FFC554",
69 | six: "#FFBC3B",
70 | seven: "#E5A935",
71 | eight: "#CC972F",
72 | nine: "#B28429",
73 | ten: "#996C14",
74 | eleven: "#66480D"
75 | };
76 |
77 | export default {
78 | darks,
79 | lights,
80 | brand: {
81 | ...brand,
82 | default: brand.six,
83 | text: brand.eleven,
84 | wash: brand.one,
85 | border: brand.two
86 | },
87 | text: {
88 | primary: darks.one,
89 | secondary: darks.two,
90 | tertiary: darks.three
91 | },
92 | ui: {
93 | wash: lights.four,
94 | cardWash: lights.five,
95 | border: lights.three
96 | },
97 | accent: {
98 | error: {
99 | ...error,
100 | default: error.six,
101 | text: error.eleven,
102 | wash: error.one,
103 | border: error.two
104 | },
105 | success: {
106 | ...success,
107 | default: success.six,
108 | text: success.eleven,
109 | wash: success.one,
110 | border: success.two
111 | },
112 | warn: {
113 | ...warn,
114 | default: warn.six,
115 | text: warn.eleven,
116 | wash: warn.one,
117 | border: warn.two
118 | }
119 | },
120 | shadow: {
121 | small: "0px 1px 2px rgba(0, 0, 0, 0.02)",
122 | medium: "0px 1px 4px rgba(0, 0, 0, 0.04)",
123 | large: "0px 1px 8px rgba(0, 0, 0, 0.08)"
124 | },
125 | animation: {
126 | in: "0.2s ease-in",
127 | out: "0.2s ease-out"
128 | }
129 | };
130 |
--------------------------------------------------------------------------------
/web/components/WithApollo/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { getClient } from "./client";
3 | import Head from "next/head";
4 | import { default as AppComponentType } from "next/app";
5 | import { AppContext, AppProps, AppInitialProps } from "next/app";
6 | import { getDataFromTree } from "react-apollo";
7 | import { ApolloClient, NormalizedCacheObject } from "apollo-boost";
8 |
9 | export interface ApolloAppProps extends AppProps, AppInitialProps {
10 | apolloClient: ApolloClient;
11 | apolloState?: NormalizedCacheObject;
12 | cookie?: string;
13 | }
14 |
15 | export default (App: {
16 | new (): AppComponentType;
17 | getInitialProps: Function;
18 | }) => {
19 | return class Apollo extends React.Component {
20 | static displayName = "withApollo(App)";
21 | apolloClient: ApolloClient;
22 | static async getInitialProps(ctx: AppContext) {
23 | const {
24 | Component,
25 | router,
26 | ctx: { req }
27 | } = ctx;
28 |
29 | let appProps = {};
30 | if (App.getInitialProps) {
31 | appProps = await App.getInitialProps(ctx);
32 | }
33 |
34 | // Run all GraphQL queries in the component tree
35 | // and extract the resulting data
36 | const cookie = req && req.headers.cookie;
37 | const host = req && (req.headers["x-forwarded-host"] || req.headers.host);
38 | const apollo = getClient(
39 | undefined,
40 | cookie,
41 | `${process.env.NODE_ENV === "development" ? "http" : "https"}://${host}`
42 | );
43 | if (!process.browser) {
44 | try {
45 | // Run all GraphQL queries
46 | await getDataFromTree(
47 |
55 | );
56 | } catch (error) {
57 | // Prevent Apollo Client GraphQL errors from crashing SSR.
58 | // Handle them in components via the data.error prop:
59 | // https://www.apollographql.com/docs/react/api/react-apollo.html#graphql-query-data-error
60 | console.error("Error while running `getDataFromTree`", error);
61 | }
62 |
63 | // getDataFromTree does not call componentWillUnmount
64 | // head side effect therefore need to be cleared manually
65 | Head.rewind();
66 | }
67 |
68 | // Extract query data from the Apollo store
69 | const apolloState = apollo.cache.extract();
70 |
71 | return {
72 | ...appProps,
73 | apolloState,
74 | cookie
75 | };
76 | }
77 |
78 | constructor(props: ApolloAppProps) {
79 | super(props);
80 | this.apolloClient = getClient(props.apolloState, props.cookie);
81 | }
82 |
83 | render() {
84 | // @ts-ignore
85 | return ;
86 | }
87 | };
88 | };
89 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "product",
3 | "version": "1.0.0",
4 | "private": true,
5 | "repository": "https://github.com/nice-boys/product-boilerplate",
6 | "author": "Max Stoiber ",
7 | "workspaces": {
8 | "packages": [
9 | "web"
10 | ]
11 | },
12 | "scripts": {
13 | "prettier": "prettier",
14 | "prettify": "prettier --write",
15 | "lint": "eslint --ignore-path .gitignore --ext .js,.ts,.tsx web",
16 | "dev": "concurrently -n web,generate:web,generate:schema,generate:db \"yarn workspace web dev\" \"yarn generate:web --watch\" \"nodemon -w web/graphql -x 'yarn generate:schema'\" \"nodemon -w web/database/datamodel.prisma -x 'yarn run generate:db'\"",
17 | "web": "yarn workspace web",
18 | "db": "yarn workspace web run db",
19 | "db:deploy": "yarn workspace web run db:deploy",
20 | "generate": "yarn run generate:db && yarn run generate:schema && yarn run generate:web",
21 | "generate:web": "graphql-codegen --config graphql-codegen.yml",
22 | "generate:schema": "GENERATE=true ts-node --skip-project web/graphql/schema/index.ts",
23 | "generate:db": "yarn workspace web generate:db",
24 | "postinstall": "yarn run generate"
25 | },
26 | "eslintConfig": {
27 | "parser": "@typescript-eslint/parser",
28 | "parserOptions": {
29 | "project": "./web/tsconfig.json"
30 | },
31 | "extends": [
32 | "eslint:recommended",
33 | "plugin:@typescript-eslint/recommended",
34 | "prettier",
35 | "prettier/@typescript-eslint",
36 | "react-app"
37 | ],
38 | "plugins": [
39 | "react-hooks",
40 | "clean-styled-components"
41 | ],
42 | "rules": {
43 | "@typescript-eslint/explicit-function-return-type": 0,
44 | "@typescript-eslint/explicit-member-accessibility": 0,
45 | "@typescript-eslint/no-empty-interface": 0,
46 | "@typescript-eslint/no-var-requires": 0,
47 | "@typescript-eslint/no-explicit-any": 2,
48 | "react-hooks/rules-of-hooks": "error",
49 | "react-hooks/exhaustive-deps": "warn",
50 | "clean-styled-components/single-component-per-file": 2,
51 | "no-console": [
52 | "error",
53 | {
54 | "allow": [
55 | "warn",
56 | "error"
57 | ]
58 | }
59 | ]
60 | }
61 | },
62 | "husky": {
63 | "hooks": {
64 | "pre-commit": "lint-staged && yarn run generate:schema && git add web/graphql/schema.generated.graphql",
65 | "post-checkout": "yarn run generate && yarn run db:deploy"
66 | }
67 | },
68 | "lint-staged": {
69 | "*.{js,ts,tsx}": [
70 | "eslint --fix",
71 | "prettier --write",
72 | "git add"
73 | ],
74 | "*.{css,json,md,mdx}": [
75 | "yarn run prettify --write",
76 | "git add"
77 | ]
78 | },
79 | "devDependencies": {
80 | "@graphql-codegen/add": "1.2.0",
81 | "@graphql-codegen/cli": "^1.2.0",
82 | "@graphql-codegen/core": "1.6.1",
83 | "@graphql-codegen/fragment-matcher": "1.6.1",
84 | "@graphql-codegen/introspection": "1.6.1",
85 | "@graphql-codegen/near-operation-file-preset": "1.2.0",
86 | "@graphql-codegen/typescript": "1.2.0",
87 | "@graphql-codegen/typescript-graphql-files-modules": "1.6.1",
88 | "@graphql-codegen/typescript-operations": "1.2.0",
89 | "@graphql-codegen/typescript-react-apollo": "1.2.0",
90 | "@typescript-eslint/eslint-plugin": "^1.6.0",
91 | "@typescript-eslint/parser": "^1.6.0",
92 | "babel-eslint": "^10.0.2",
93 | "concurrently": "^4.1.0",
94 | "eslint": "^5.16.0",
95 | "eslint-config-prettier": "^4.1.0",
96 | "eslint-config-react-app": "^3.0.8",
97 | "eslint-loader": "2.1.1",
98 | "eslint-plugin-clean-styled-components": "^0.0.2",
99 | "eslint-plugin-flowtype": "2.50.3",
100 | "eslint-plugin-import": "2.18.2",
101 | "eslint-plugin-jsx-a11y": "6.2.1",
102 | "eslint-plugin-react": "7.14.3",
103 | "eslint-plugin-react-hooks": "^1.6.0",
104 | "husky": ">=1",
105 | "lint-staged": ">=9",
106 | "nodemon": "^1.19.1",
107 | "now": "^15.3.0",
108 | "prettier": "^1.18.2",
109 | "react-dev-utils": "^9.0.1",
110 | "ts-node": "^8.2.0"
111 | },
112 | "resolutions": {
113 | "styled-jsx": "3.0.0",
114 | "@types/styled-components": "4.1.8",
115 | "graphql": "14.3.1"
116 | },
117 | "license": "MIT"
118 | }
119 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # REPLACED BY [BEDROCK](https://bedrock.mxstbr.com)
2 |
3 | While you can still use this boilerplate, it's very out of date. [Bedrock](https://bedrock.mxstbr.com) is @mxstbr's attempt at building a sustainable business out of a boilerplate in order to allow it to stay up to date.
4 |
5 | If you're looking to build a SaaS product, check it out!
6 |
7 | ----
8 |
9 | Quickly ship your apps with the power of code generation.
10 |
11 | ## Philosophy
12 |
13 | > "Speed of developer iteration is the single most important factor in how quickly a technology company can move."
14 | >
15 | > — Paul Biggar, cofounder CircleCI
16 |
17 | Product boilerplate is [@brianlovin](https://github.com/brianlovin) and [@mxstbr](https://github.com/mxstbr)'s personal setup to quickly build new apps. It aims to get all the common stuff out of the way and make shipping and subsequently iterating on a product as quick as possible.
18 |
19 | There are three important traits in all the included tooling that we pay attention to:
20 |
21 | 1. **Mastery**: We know most of these tools inside and out and have a lot of experience with them.
22 | 2. **Code generation**: The less boilerplate code we need to write, the more we can focus on our app. Code generation allows us to automate the tedious parts.
23 | 3. **Type safety**: Counterintuitively, having strict type safety lets you move faster because you get warnings about bugs right in your IDE without having to recompile.
24 |
25 | Note that this is our personal boilerplate. You are more than welcome to use it (that's why it's open source), but we cannot give any support when using it. Make sure you understand the tools in it before using it!
26 |
27 | ## Stack
28 |
29 | The entire app (front- and backend) uses [Next.js](https://nextjs.org) in serverless-mode for development, and deployment is powered by [Now](https://now.sh).
30 |
31 | ### Tools
32 |
33 | - [TypeScript](https://typescriptlang.org)
34 | - [Prettier](https://prettier.io)
35 | - [ESLint](https://eslint.org)
36 | - [yarn](https://yarnpkg.com) workspaces for the monorepo support
37 | - [GraphQL Codegen](https://graphql-code-generator.com)
38 |
39 | ### Frontend
40 |
41 | - [React](https://github.com/facebook/react)
42 | - [Next.js](https://github.com/zeit/next.js)
43 | - [styled-components](https://github.com/styled-components/styled-components)
44 | - [rebass](https://rebassjs.org)
45 | - [@nice-boys/components](https://github.com/nice-boys/components)
46 |
47 | ### Backend
48 |
49 | - [GraphQL](https://graphql.org)
50 | - [Apollo Server](http://apollographql.com/docs/apollo-server) for exposing said GraphQL schema through the API
51 | - [GraphQL Nexus](https://nexus.js.org) for creating a type-safe GraphQL API
52 | - [Prisma](https://prisma.io) for type-safe database access
53 | - [Passport](http://www.passportjs.org/) for authentication
54 |
55 | ## Working with the boilerplate
56 |
57 | ### Code Generation
58 |
59 | There are three code generators at work in this boilerplate:
60 |
61 | - [GraphQL Codegen](https://graphql-code-generator.com) automatically generates React components for the frontend that fetch from the API. They are fully typed too, so you know exactly what the shape of the returned data is.
62 | - [Prisma](https://prisma.io) generates a fully type-safe database client from our datamodel.
63 | - [GraphQL Nexus](https://nexus.js.org) (in combination with Prisma) lets us expose a production-ready GraphQL API from the database client.
64 |
65 | ### Routing
66 |
67 | All routing happens via the folder structure in the `web/pages/` folder. Any route under `web/pages/api` will be treated as an [API route](https://nextjs.org/docs#api-routes), all others as React routes.
68 |
69 | To add dynamic routing use the Next.js [dynamic routes feature](https://nextjs.org/docs#dynamic-routes).
70 |
71 | ### Commands
72 |
73 | To start the app locally you have to run these two commands:
74 |
75 | - `yarn run dev`: Stars the development process for the serverless front- and backend, as well as all generation processes.
76 | - `yarn run db`: Starts the database locally (note: requires Docker to be up and running and docker-compose to be installed)
77 |
78 | Further, you will frequently use these commands while developing and they are automatically run whenever you switch branches or pull new code to make sure your generated files are up to date:
79 |
80 | - `yarn run db:deploy`: Update your local database with changes you made to the datamodel.
81 | - `yarn run generate`: Runs all the codegeneration commands in sequence. You can also run them manually if necessary:
82 | - `yarn run generate:db`: Generate the Prisma database client for the server.
83 | - `yarn run generate:server`: Generate the `schema.graphql` file for the backend from the Nexus schema
84 | - `yarn run generate:web`: Generate the fetching hooks and types for the frontend from the `.graphql` files contained within it
85 |
86 | These are automatically run in a pre-commit hook, so don't worry about calling them manually unless you have a reason to:
87 |
88 | - `yarn run prettify`: Prettifies the `src` folder with [Prettier](https://prettier.io).
89 | - `yarn run lint`: Lints the `src` folder with [ESLint](https://eslint.org).
90 |
91 | ### Deployment
92 |
93 | To deploy the database, look at the Prisma docs, e.g. their tutorial on [deploying the database to AWS fargate](https://www.prisma.io/tutorials/deploy-prisma-to-aws-fargate-ct14).
94 |
95 | Once that is up and running, to deploy the app you simply run `now`, that's it! We recommend enabling the Now GitHub integration for CD.
96 |
97 | ## License
98 |
99 | Licensed under the MIT License.
100 |
--------------------------------------------------------------------------------