├── .env.example ├── .gitignore ├── .prettierrc.json ├── .sequelizerc ├── LICENSE ├── README.md ├── components ├── Button.tsx ├── Header.tsx └── Loader.tsx ├── db ├── index.ts ├── migrations │ ├── 20200917162058-create-user.js │ └── 20200917185257-create-post.js └── models │ ├── helpers.ts │ ├── index.ts │ ├── post.ts │ └── user.ts ├── docs ├── aws-rds-example.png ├── env-vars.png └── stack.png ├── lib └── auth │ ├── jwt.ts │ └── privateRoute.tsx ├── next-env.d.ts ├── package.json ├── pages ├── _app.tsx ├── api-demo.tsx ├── api │ ├── auth │ │ └── [...nextauth].ts │ ├── post │ │ ├── [id] │ │ │ └── delete.ts │ │ ├── create.ts │ │ └── index.ts │ ├── private.ts │ └── public.ts ├── crud.tsx ├── index.tsx ├── private.tsx └── public.tsx ├── postcss.config.js ├── public ├── favicon.ico └── vercel.svg ├── styles └── index.css ├── tailwind.config.js ├── tsconfig.json └── yarn.lock /.env.example: -------------------------------------------------------------------------------- 1 | API_URL=http://localhost:3000 2 | NEXTAUTH_URL=http://localhost:3000 3 | AUTH_SECRET=a-random-string 4 | JWT_SECRET=a-random-string 5 | 6 | DATABASE_URL= 7 | GOOGLE_CLIENT_ID= 8 | GOOGLE_CLIENT_SECRET= 9 | GITHUB_CLIENT_ID= 10 | GITHUB_CLIENT_SECRET= -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | 21 | # debug 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | 26 | # local env files 27 | .env.local 28 | .env.development.local 29 | .env.test.local 30 | .env.production.local 31 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "endOfLine": "lf", 3 | "semi": true, 4 | "tabWidth": 2, 5 | "trailingComma": "es5", 6 | "arrowParens": "always" 7 | } 8 | -------------------------------------------------------------------------------- /.sequelizerc: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | require("dotenv").config({ path: path.resolve(process.cwd(), '.env.local') }); 4 | 5 | module.exports = { 6 | "url": process.env.DATABASE_URL, 7 | "models-path": path.resolve("db", "models"), 8 | "seeders-path": path.resolve("db", "seeders"), 9 | "migrations-path": path.resolve("db", "migrations") 10 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 Belay Inc. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

🚲 next-fullstack

3 |

4 | A lightweight boilerplate for developing full-stack applications with Next.js 5 |

6 |
7 |
8 | 9 | **[Check out the demo 📎](https://next-fullstack-demo.vercel.app/)** 10 | 11 | This full-stack boilerplates comes with [Sequelize](https://sequelize.org/master/) (a Node.js ORM), [Tailwind CSS](https://tailwindcss.com/) (utility-first CSS framework), and basic authentication with [NextAuth.js](https://next-auth.js.org/). Minimal setup is needed to deploy a basic CRUD application. 12 | 13 | 14 | 15 | The entire boilerplate is written in [Typescript](https://www.typescriptlang.org/) and is set up with pre-commit hooks that compiles Typescript and runs [prettier](https://prettier.io/) (code formatter). 16 | 17 |
18 | 19 | ## From cloning to deploying, a step by step guide 20 | Follow along to get your own version of this boilerplate deployed on Vercel. 21 | 22 |
23 | 24 | #### Fork and clone repository 25 | [Fork the repository](https://guides.github.com/activities/forking/) into your own account then clone the fork to your local development environment. 26 | ``` 27 | git clone git@github.com:[USERNAME]/next-fullstack.git 28 | ``` 29 |
30 | 31 | #### Install dependencies 32 | ``` 33 | yarn install 34 | ``` 35 |
36 | 37 | #### Set up local environment variable 38 | The environment variables required by this boilerplate can be seen in `.env.example`. Create a local environment variable file: 39 | 40 | ``` 41 | cp .env.example .env.local 42 | ``` 43 | 44 | We'll be setting up a database and also OAuth providers in upcoming steps to get values for these variables. 45 | 46 |
47 | 48 | #### Create a Postgres database 49 | You can create a Postgres database with any service provider. Make sure it is publicly accessible and is password authenticated. 50 | 51 | 👉 [See example settings for creating an AWS RDS Postgres database.](docs/aws-rds-example.png) 52 | 53 | After creation, compose the database URL and update your local environment variable file (`.env.local`) 54 | ``` 55 | DATABASE_URL=postgres://[USERNAME]:[PASSWORD]@[HOST]:[PORT]/postgres 56 | ``` 57 |
58 | 59 | #### Run migrations 60 | Create tables `users` and `posts`. 61 | 62 | ``` 63 | yarn sequelize-cli db:migrate 64 | ``` 65 | 66 | These are example models and tables. Feel free to roll back (`yarn sequelize-cli db:migrate:undo`) and write your own migrations after you have the basic boilerplate up and running. 67 | 68 |
69 | 70 | #### Set up OAuth providers 71 | The boilerplate comes set up with Github and Google as OAuth providers, however you are free to [remove or add your own](https://next-auth.js.org/providers/github) by editing the provider entries in the [`[next-auth].ts` file](https://github.com/belay-labs/next-fullstack/blob/master/pages/api/auth/%5B...nextauth%5D.ts#L11) and adding the relevant environment variables. 72 | 73 | 👉 [Setting up Google OAuth](https://support.google.com/cloud/answer/6158849?hl=en)\ 74 | 👉 [Setting up Github OAuth](https://docs.github.com/en/free-pro-team@latest/developers/apps/creating-an-oauth-app) 75 | 76 | 77 | 78 | Update environment variables with your OAuth client ID and secret. e.g. for Github: 79 | ``` 80 | GITHUB_CLIENT_ID=[GITHUB_CLIENT_ID] 81 | GITHUB_CLIENT_SECRET=[GITHUB_CLIENT_SECRET] 82 | ``` 83 | 84 |
85 | 86 | #### Run locally 87 | ``` 88 | yarn dev 89 | ``` 90 | 🚀 Go to [localhost:3000](http://localhost:3000/)! 91 | 92 |
93 | 94 | #### Deploy to Vercel 95 | Applications developed from this boilerplate can be hosted anywhere. These instructions are for deploying via Vercel. 96 | 97 | 1. [Import](https://vercel.com/import) your project from Github 98 | 2. Set your environment variables - you won't know what `API_URL` and `NEXTAUTH_URL` will be until after your first deploy. Vercel will issue your project a unique domain. 99 | 100 | 101 | 102 | 3. After deployment grab the domain and update the `API_URL` and `NEXTAUTH_URL` environment variables. 103 | 4. Redeploy for new variable to take effect (you can trigger this by pushing to master). 104 | 105 |
106 | 107 | ## Contributing 108 | 109 | **[🐛 Submit a bug](https://github.com/belay-labs/next-fullstack/issues/new?labels=bug&template=bug_report.md)** | **[🐥 Submit a feature request](https://github.com/belay-labs/next-fullstack/issues/new?labels=feature-request&template=feature_request.md)** 110 | 111 | #### Review & deployment 112 | 113 | Create a PR describing the change you've made and someone will be along to review it and get it merged to master. After changes are merged to `master`, we'll trigger a production deployment to https://next-fullstack-demo.vercel.app/. 114 | 115 |
116 | 117 | ## Maintainers 118 | Hi! We're [Cathy](https://github.com/cathykc), [Stedman](https://github.com/stedmanblake), and [Zain](https://github.com/tarzain). Feel free email us at cathy@belaylabs.com! 👋 119 | 120 |
121 | 122 | ## License 123 | [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) 124 | 125 | 126 | This project is licensed under the terms of the [Apache-2.0](LICENSE). 127 | -------------------------------------------------------------------------------- /components/Button.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | interface Props { 4 | className?: string; 5 | disabled?: boolean; 6 | hoverClassName?: string; 7 | iconPath: JSX.Element; 8 | loading?: boolean; 9 | onClick: () => void; 10 | text: string; 11 | } 12 | 13 | const Button = ({ 14 | className, 15 | disabled, 16 | hoverClassName, 17 | iconPath, 18 | loading, 19 | onClick, 20 | text, 21 | }: Props) => { 22 | const normalStyle = className ? className : "bg-purple-500 text-white"; 23 | const hoverStyle = hoverClassName ? hoverClassName : "hover:bg-purple-700"; 24 | const disabledStyle = "opacity-50 cursor-not-allowed"; 25 | 26 | const handleClick = () => { 27 | if (disabled || loading) return; 28 | onClick(); 29 | }; 30 | 31 | return ( 32 | 61 | ); 62 | }; 63 | 64 | export default Button; 65 | -------------------------------------------------------------------------------- /components/Header.tsx: -------------------------------------------------------------------------------- 1 | import { signIn, signOut, useSession } from "next-auth/client"; 2 | import Link from "next/link"; 3 | import { useRouter } from "next/router"; 4 | 5 | interface Props { 6 | session: any; 7 | } 8 | 9 | interface ButtonProps { 10 | active?: boolean; 11 | onClick?: () => void; 12 | text: string; 13 | } 14 | 15 | const HeaderButton = ({ active, onClick, text }: ButtonProps) => { 16 | return ( 17 | 25 | ); 26 | }; 27 | 28 | const Header = () => { 29 | const router = useRouter(); 30 | const [session, loading] = useSession(); 31 | 32 | return ( 33 |
34 |
35 | 36 | 37 | 46 | 52 | 53 | Demo 54 | 55 | 56 | 57 | 58 | 62 | 63 | 64 | 65 | 66 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 |
83 |
84 | {session ? ( 85 | <> 86 | 87 | 88 | 89 | ) : ( 90 | <> 91 | 92 | 93 | )} 94 |
95 |
96 | ); 97 | }; 98 | 99 | export default Header; 100 | -------------------------------------------------------------------------------- /components/Loader.tsx: -------------------------------------------------------------------------------- 1 | const Loader = () => { 2 | return ( 3 |
4 | 10 | 16 | 17 |
18 | ); 19 | }; 20 | 21 | export default Loader; 22 | -------------------------------------------------------------------------------- /db/index.ts: -------------------------------------------------------------------------------- 1 | import db from "./models"; 2 | 3 | export default db; 4 | -------------------------------------------------------------------------------- /db/migrations/20200917162058-create-user.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | module.exports = { 3 | up: async (queryInterface, Sequelize) => { 4 | await queryInterface.createTable("users", { 5 | id: { 6 | allowNull: false, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | type: Sequelize.INTEGER, 10 | }, 11 | email: { 12 | allowNull: false, 13 | type: Sequelize.STRING, 14 | unique: true, 15 | }, 16 | name: { 17 | allowNull: false, 18 | type: Sequelize.STRING, 19 | }, 20 | imgUrl: { 21 | allowNull: false, 22 | type: Sequelize.STRING, 23 | }, 24 | createdAt: { 25 | allowNull: false, 26 | type: Sequelize.DATE, 27 | }, 28 | updatedAt: { 29 | allowNull: false, 30 | type: Sequelize.DATE, 31 | }, 32 | }); 33 | }, 34 | down: async (queryInterface, Sequelize) => { 35 | await queryInterface.dropTable("users"); 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /db/migrations/20200917185257-create-post.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | module.exports = { 3 | up: async (queryInterface, Sequelize) => { 4 | await queryInterface.createTable("posts", { 5 | id: { 6 | allowNull: false, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | type: Sequelize.INTEGER, 10 | }, 11 | text: { 12 | type: Sequelize.STRING, 13 | allowNull: false, 14 | }, 15 | createdById: { 16 | type: Sequelize.INTEGER, 17 | references: { 18 | model: "users", 19 | key: "id", 20 | }, 21 | onUpdate: "CASCADE", 22 | onDelete: "RESTRICT", 23 | }, 24 | createdAt: { 25 | allowNull: false, 26 | type: Sequelize.DATE, 27 | }, 28 | updatedAt: { 29 | allowNull: false, 30 | type: Sequelize.DATE, 31 | }, 32 | }); 33 | }, 34 | down: async (queryInterface, Sequelize) => { 35 | await queryInterface.dropTable("posts"); 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /db/models/helpers.ts: -------------------------------------------------------------------------------- 1 | import { Model } from "sequelize"; 2 | 3 | /** 4 | * Because of https://github.com/sequelize/sequelize/issues/10579, the classes transpiled 5 | * by Babel are resetting the sequelize properties getters & setters. This helper fixes these properties 6 | * by re-defining them. Based on https://github.com/RobinBuschmann/sequelize-typescript/issues/612#issuecomment-491890977. 7 | * 8 | * Must be called from the constructor. 9 | */ 10 | export function restoreSequelizeAttributesOnClass( 11 | newTarget: any, 12 | self: Model 13 | ): void { 14 | Object.keys(newTarget.rawAttributes).forEach((propertyKey: any) => { 15 | Object.defineProperty(self, propertyKey, { 16 | get() { 17 | return self.getDataValue(propertyKey); 18 | }, 19 | set(value) { 20 | self.setDataValue(propertyKey, value); 21 | }, 22 | }); 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /db/models/index.ts: -------------------------------------------------------------------------------- 1 | import { DataTypes, Sequelize } from "sequelize"; 2 | 3 | import { PostFactory } from "./post"; 4 | import { UserFactory } from "./user"; 5 | 6 | interface DbInterface { 7 | sequelize: Sequelize; 8 | Sequelize: any; 9 | User: any; 10 | Post: any; 11 | } 12 | 13 | const sequelize = new Sequelize(process.env.DATABASE_URL!, { 14 | // https://github.com/vercel/ncc/issues/345#issuecomment-487404520 15 | dialect: "postgres", 16 | dialectModule: require("pg"), 17 | }); 18 | 19 | const db: DbInterface = { 20 | sequelize, 21 | Sequelize, 22 | Post: PostFactory(sequelize, DataTypes), 23 | User: UserFactory(sequelize, DataTypes), 24 | }; 25 | 26 | Object.keys(db).forEach((modelName) => { 27 | // @ts-ignore: TS7053 28 | if (db[modelName].associate) db[modelName].associate(db); 29 | }); 30 | 31 | export default db; 32 | -------------------------------------------------------------------------------- /db/models/post.ts: -------------------------------------------------------------------------------- 1 | import { DataTypes, Model, Optional, Sequelize } from "sequelize"; 2 | 3 | import { restoreSequelizeAttributesOnClass } from "./helpers"; 4 | 5 | export interface PostAttributes { 6 | id: number; 7 | text: string; 8 | createdById: number; 9 | } 10 | 11 | export interface PostCreationAttributes 12 | extends Optional {} 13 | 14 | export class Post 15 | extends Model 16 | implements PostAttributes { 17 | public id!: number; 18 | public text!: string; 19 | public createdById!: number; 20 | 21 | public readonly createdAt!: Date; 22 | public readonly updatedAt!: Date; 23 | 24 | constructor(...args: any[]) { 25 | super(...args); 26 | restoreSequelizeAttributesOnClass(new.target, this); 27 | } 28 | 29 | /** 30 | * Helper method for defining associations. 31 | * This method is not a part of Sequelize lifecycle. 32 | * The `models/index` file will call this method automatically. 33 | */ 34 | static associate(models: any) { 35 | Post.belongsTo(models.User, { 36 | as: "createdBy", 37 | foreignKey: "createdById", 38 | }); 39 | } 40 | } 41 | 42 | export const PostFactory = (sequelize: Sequelize, DataTypes: any) => { 43 | Post.init( 44 | { 45 | id: { 46 | type: DataTypes.INTEGER.UNSIGNED, 47 | autoIncrement: true, 48 | primaryKey: true, 49 | }, 50 | text: { 51 | type: DataTypes.STRING, 52 | allowNull: false, 53 | unique: true, 54 | }, 55 | createdById: { 56 | type: DataTypes.INTEGER.UNSIGNED, 57 | allowNull: false, 58 | }, 59 | }, 60 | { 61 | sequelize, 62 | modelName: "Post", 63 | tableName: "posts", 64 | } 65 | ); 66 | 67 | return Post; 68 | }; 69 | -------------------------------------------------------------------------------- /db/models/user.ts: -------------------------------------------------------------------------------- 1 | import { DataTypes, Model, Optional, Sequelize } from "sequelize"; 2 | 3 | import { restoreSequelizeAttributesOnClass } from "./helpers"; 4 | import { Post } from "./post"; 5 | 6 | export interface UserAttributes { 7 | id: number; 8 | email: string; 9 | name: string; 10 | imgUrl: string; 11 | } 12 | 13 | export interface UserCreationAttributes 14 | extends Optional {} 15 | 16 | export class User 17 | extends Model 18 | implements UserAttributes { 19 | public id!: number; 20 | public email!: string; 21 | public name!: string; 22 | public imgUrl!: string; 23 | 24 | public readonly createdAt!: Date; 25 | public readonly updatedAt!: Date; 26 | 27 | public readonly posts!: Post[]; 28 | 29 | constructor(...args: any[]) { 30 | super(...args); 31 | restoreSequelizeAttributesOnClass(new.target, this); 32 | } 33 | 34 | /** 35 | * Helper method for defining associations. 36 | * This method is not a part of Sequelize lifecycle. 37 | * The `models/index` file will call this method automatically. 38 | */ 39 | static associate(models: any) { 40 | User.hasMany(models.Post, { 41 | foreignKey: "createdById", 42 | as: "posts", 43 | }); 44 | } 45 | } 46 | 47 | export const UserFactory = (sequelize: Sequelize, DataTypes: any) => { 48 | User.init( 49 | { 50 | id: { 51 | type: DataTypes.INTEGER.UNSIGNED, 52 | autoIncrement: true, 53 | primaryKey: true, 54 | }, 55 | email: { 56 | type: DataTypes.STRING, 57 | allowNull: false, 58 | unique: true, 59 | }, 60 | name: { 61 | type: DataTypes.STRING, 62 | allowNull: false, 63 | }, 64 | imgUrl: { 65 | type: DataTypes.STRING, 66 | allowNull: false, 67 | }, 68 | }, 69 | { 70 | sequelize, 71 | modelName: "User", 72 | tableName: "users", 73 | } 74 | ); 75 | 76 | return User; 77 | }; 78 | -------------------------------------------------------------------------------- /docs/aws-rds-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/belay-labs/next-fullstack/71727c55d5a0194f71fa7411d356dc14178a6ffe/docs/aws-rds-example.png -------------------------------------------------------------------------------- /docs/env-vars.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/belay-labs/next-fullstack/71727c55d5a0194f71fa7411d356dc14178a6ffe/docs/env-vars.png -------------------------------------------------------------------------------- /docs/stack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/belay-labs/next-fullstack/71727c55d5a0194f71fa7411d356dc14178a6ffe/docs/stack.png -------------------------------------------------------------------------------- /lib/auth/jwt.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from "next"; 2 | import jwt from "next-auth/jwt"; 3 | 4 | const SECRET = process.env.JWT_SECRET!; 5 | 6 | export const getToken = (req: NextApiRequest) => { 7 | return jwt.getToken({ req, secret: SECRET }); 8 | }; 9 | 10 | export const requestWrapper = async ( 11 | req: NextApiRequest, 12 | res: NextApiResponse, 13 | action: (token: any) => void 14 | ) => { 15 | try { 16 | const jwtToken = await getToken(req); 17 | 18 | // Add conditions here on data in JWT Token (e.g. team, user) 19 | if (!jwtToken) { 20 | res.status(403).json({ message: "Not signed in." }); 21 | } else { 22 | await action(jwtToken); 23 | } 24 | } catch (e) { 25 | console.error(e); 26 | res.status(500).json({ error: JSON.stringify(e) }); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /lib/auth/privateRoute.tsx: -------------------------------------------------------------------------------- 1 | import { signIn, useSession } from "next-auth/client"; 2 | import Router from "next/router"; 3 | import React from "react"; 4 | 5 | import Loader from "../../components/Loader"; 6 | 7 | const privateRoute = (WrappedComponent: any) => (props: any) => { 8 | const [session, loading] = useSession(); 9 | 10 | if (!loading && !session) signIn(); 11 | 12 | if (session) { 13 | return ; 14 | } else { 15 | return ; 16 | } 17 | }; 18 | 19 | export default privateRoute; 20 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-fullstack", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start" 9 | }, 10 | "dependencies": { 11 | "lodash": "^4.17.20", 12 | "next": "9.5.3", 13 | "next-auth": "^3.1.0", 14 | "pg": "^8.3.3", 15 | "pg-hstore": "^2.3.3", 16 | "react": "16.13.1", 17 | "react-dom": "16.13.1", 18 | "sequelize": "^6.3.5", 19 | "tailwindcss": "^1.8.10" 20 | }, 21 | "devDependencies": { 22 | "@types/lodash": "^4.14.161", 23 | "@types/next-auth": "^3.1.4", 24 | "@types/node": "^14.10.2", 25 | "@types/react": "^16.9.49", 26 | "@types/validator": "^13.1.0", 27 | "dotenv": "^8.2.0", 28 | "husky": ">=4", 29 | "lint-staged": ">=10", 30 | "postcss-preset-env": "^6.7.0", 31 | "prettier": "^2.1.2", 32 | "sequelize-cli": "^6.2.0", 33 | "typescript": "^4.0.2" 34 | }, 35 | "husky": { 36 | "hooks": { 37 | "pre-commit": "yarn tsc && lint-staged" 38 | } 39 | }, 40 | "lint-staged": { 41 | "*.{js,css,md,jsx,ts,tsx}": "prettier --write" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { Provider } from "next-auth/client"; 2 | import { AppProps } from "next/app"; 3 | 4 | import Header from "../components/Header"; 5 | 6 | import "../styles/index.css"; 7 | 8 | export default function App({ Component, pageProps }: AppProps) { 9 | return ( 10 | <> 11 | 12 |
13 |
14 | 15 |
16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /pages/api-demo.tsx: -------------------------------------------------------------------------------- 1 | import { useSession } from "next-auth/client"; 2 | 3 | const ApiDemo = () => { 4 | const [session, loading] = useSession(); 5 | 6 | return ( 7 | <> 8 |
9 |

10 | You are {session ? "signed in" : "not signed in"}. 11 |

12 |
13 |
14 |
Public API endpoint
15 |