├── .env.example ├── .gitignore ├── LICENSE.md ├── README.md ├── components ├── articles │ ├── ArticleDetails.tsx │ ├── ArticleList.tsx │ ├── ArticleListItem.tsx │ └── CreateArticleForm.tsx ├── common │ ├── Icon.tsx │ └── InputWithLabel.tsx ├── page │ ├── Footer.tsx │ ├── GoogleAnalytics.tsx │ ├── Header.tsx │ ├── Notifications.tsx │ └── PageHead.tsx └── user │ ├── SigninWithEmailForm.tsx │ └── SigninWithGoogleButton.tsx ├── config └── config.ts ├── docs ├── demo.jpg └── lighthouse_score.png ├── hooks ├── useArticles.tsx └── useUser.ts ├── jsconfig.json ├── lib ├── data │ └── firebase.ts ├── formatDate.ts ├── handleRestRequest.ts ├── isClientSide.ts ├── isDevelopment.ts ├── makeRestRequest.ts ├── showNotification.ts └── toSlug.ts ├── next-env.d.ts ├── next.config.js ├── package.json ├── pages ├── _app.tsx ├── _document.tsx ├── about.tsx ├── api │ ├── notifications.tsx │ └── revalidate.tsx ├── articles │ └── [slug].tsx ├── index.tsx ├── robots.txt.tsx ├── signin │ ├── authenticate.tsx │ └── index.tsx └── sitemap.xml.tsx ├── public ├── app.css ├── favicon.ico ├── favicon.png ├── icons │ ├── feedback.svg │ ├── help.svg │ ├── home.svg │ ├── menu.svg │ └── person.svg ├── images │ └── google_g.svg ├── manifest.json └── share_preview.jpg ├── tsconfig.json ├── types └── global.d.ts ├── vercel.json └── yarn.lock /.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_APP_URL=https://nextjs-pwa-firebase-boilerplate.vercel.app/ 2 | 3 | NEXT_PUBLIC_FIREBASE_PROJECT_ID=nextjs-pwa-firebase-boilerplate 4 | NEXT_PUBLIC_FIREBASE_API_KEY=AIza... -------------------------------------------------------------------------------- /.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 | 32 | .vercel -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020, Tom Söderlund 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 4 | 5 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 6 | 7 | Source: http://opensource.org/licenses/ISC 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![App icon](public/favicon.png) 2 | 3 | # Next.js Firebase PWA 4 | 5 | **Next.js serverless PWA with Firebase and React Hooks** 6 | 7 | ![nextjs-pwa-firebase-boilerplate demo on phone](public/share_preview.jpg) 8 | 9 | _Note 1: This boilerplate is being converted to TypeScript. For pure JavaScript, see branch `old-javascript`_ 10 | 11 | _Note 2: this is my v4 boilerplate for React web apps. See also my [GraphQL + Postgres SQL boilerplate](https://github.com/tomsoderlund/nextjs-pwa-graphql-sql-boilerplate), [Redux + REST + Postgres SQL boilerplate](https://github.com/tomsoderlund/nextjs-sql-rest-api-boilerplate) and [Redux + REST + MongoDB boilerplate](https://github.com/tomsoderlund/nextjs-express-mongoose-crudify-boilerplate). For a simple Next.js landing page, see [nextjs-generic-landing-page](https://github.com/tomsoderlund/nextjs-generic-landing-page)._ 12 | 13 | ## Support this project 14 | 15 | Did you or your company find `nextjs-pwa-firebase-boilerplate` useful? Please consider giving a small donation, it helps me spend more time on open-source projects: 16 | 17 | [![Support Tom on Ko-Fi.com](https://www.tomsoderlund.com/ko-fi_tomsoderlund_50.png)](https://ko-fi.com/tomsoderlund) 18 | 19 | ## Why is this awesome? 20 | 21 | This is a great template for a any project where you want **React (with Hooks)** (with **static site generation (SSG)** or **server-side rendering (SSR)**, powered by [Next.js](https://github.com/zeit/next.js)) as frontend and **Firebase** as backend. *Lightning fast, all JavaScript.* 22 | 23 | - Great starting point for a [PWA (Progressive Web App)](https://en.wikipedia.org/wiki/Progressive_web_applications), which you can add to your Home Screen and use as a full-screen app. 24 | - PWA features such as `manifest.json` and offline support (`next-offline`). 25 | - Can be deployed as [serverless functions on Vercel/Zeit Now](#deploying). 26 | - Uses the new Firebase [Firestore](https://firebase.google.com/docs/firestore) database, but easy to replace/remove database. 27 | - Login/Signup with Firebase Authentication. 28 | - Can use SSG `getStaticProps` or SSR `getServerSideProps`. 29 | - React Hooks and Context for state management and business logic. 30 | - Free-form database model. No GraphQL or REST API, just add React Hooks and modify `getStaticProps`/`getServerSideProps` when changing/adding database tables. 31 | - Easy to style the visual theme using CSS (e.g. using [Design Profile Generator](https://tomsoderlund.github.io/design-profile-generator/)). 32 | - SEO support with `sitemap.xml` and `robots.txt`. 33 | - Google Analytics and `google-site-verification` support (see `config/config.ts`). 34 | - Flexible configuration with `config/config.ts` and `.env.local` file. 35 | - Code linting and formatting with StandardJS (`yarn lint`/`yarn fix`). 36 | - Unit testing with Jasmine (`yarn unit`, not yet included). 37 | - Great page speed, see [Lighthouse](https://developers.google.com/web/tools/lighthouse) score: 38 | 39 | ![Lighthouse score](docs/lighthouse_score.png) 40 | 41 | ## Demo 42 | 43 | See [**nextjs-pwa-firebase-boilerplate** running on Vercel here](https://nextjs-pwa-firebase-boilerplate.vercel.app/). 44 | 45 | ![nextjs-pwa-firebase-boilerplate demo on phone](docs/demo.jpg) 46 | 47 | ## How to use 48 | 49 | Clone this repository: 50 | 51 | git clone https://github.com/tomsoderlund/nextjs-pwa-firebase-boilerplate.git [MY_APP] 52 | cd [MY_APP] 53 | 54 | Remove the `.git` folder since you want to create a new repository 55 | 56 | rm -rf .git 57 | 58 | Install dependencies: 59 | 60 | yarn # or npm install 61 | 62 | At this point, you need to [set up Firebase Firestore, see below](#set-up-firebase-database-firestore). 63 | 64 | When Firebase is set up, start the web app by: 65 | 66 | yarn dev 67 | 68 | In production: 69 | 70 | yarn build 71 | yarn start 72 | 73 | If you navigate to `http://localhost:3004/` you will see a web page with a list of articles (or an empty list if you haven’t added one). 74 | 75 | ## Modifying the app to your needs 76 | 77 | ### Change app name and description 78 | 79 | - Do search/replace for the `name`’s “Next.js Firebase PWA”, “nextjs-pwa-firebase-boilerplate” and `description` “Next.js serverless PWA with Firebase and React Hooks” to something else. 80 | - Change the `version` in `package.json` to `0.1.0` or similar. 81 | - Change the `license` in `package.json` to whatever suits your project. 82 | 83 | ### Renaming “Article” to something else 84 | 85 | The main database item is called `Article`, but you probably want something else in your app. 86 | 87 | Rename the files: 88 | 89 | git mv hooks/useArticles.tsx hooks/use{NewName}s.tsx 90 | 91 | mkdir -p components/{newName}s 92 | git mv components/articles/CreateArticleForm.tsx components/{newName}s/Create{NewName}Form.tsx 93 | git mv components/articles/ArticleDetails.tsx components/{newName}s/{NewName}Details.tsx 94 | git mv components/articles/ArticleList.tsx components/{newName}s/{NewName}List.tsx 95 | git mv components/articles/ArticleListItem.tsx components/{newName}s/{NewName}ListItem.tsx 96 | rm -r components/articles 97 | 98 | mkdir pages/{newName}s 99 | git mv "pages/articles/[slug].tsx" "pages/{newName}s/[slug].tsx" 100 | rm -r pages/articles 101 | 102 | Then, do search/replace inside the files for different casing: `article`, `Article`, `ARTICLE`. 103 | 104 | ### Change port number 105 | 106 | Do search/replace for `3004` to something else. 107 | 108 | ### Set up Firebase database (Firestore) 109 | 110 | Set up the database (if you don’t need a database, see “How to remove/replace Firebase as database” below): 111 | 112 | 1. Go to https://console.firebase.google.com/ and create a new project, a new web app, and a new Cloud Firestore database. 113 | 2. Copy the `firebaseConfig` (from when setting up the Firebase web app) to `lib/data/firebase.ts` 114 | 3. Edit the `.env.local` file, setting the `NEXT_PUBLIC_FIREBASE_API_KEY` value. 115 | 116 | ### How to remove the Firebase dependency 117 | 118 | - Run `yarn remove firebase` 119 | - Delete `lib/data/firebase.ts` and modify `hooks/useArticles.tsx`. 120 | 121 | ### Replace Firebase with Postgres SQL 122 | 123 | - Use a Postgres hosting provider (e.g. https://www.elephantsql.com/) 124 | - Use [`createSqlRestRoutesServerless` in `sql-wizard`](https://github.com/tomsoderlund/sql-wizard#creating-rest-routes-serverless-eg-for-nextjs-and-vercel) to set up your own API routes. 125 | 126 | ### Replace Firebase with Supabase (Postgres SQL, real-time updates) 127 | 128 | - Remove Firebase: `yarn remove firebase` 129 | - Add Supabase: `yarn add @supabase/supabase-js` 130 | - Add `NEXT_PUBLIC_SUPABASE_API_KEY` to `.env.local` 131 | - Create a `lib/data/supabase.ts`: 132 | ``` 133 | import { createClient } from '@supabase/supabase-js' 134 | const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL 135 | const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_API_KEY 136 | export const supabase = createClient(supabaseUrl, supabaseKey) 137 | ``` 138 | - Update the JS files that reference `lib/data/firebase` 139 | 140 | ### Change visual theme (CSS) 141 | 142 | 1. Change included CSS files in `pages/_app.tsx` 143 | 2. Change CSS in `public/app.css` 144 | 3. Change font(s) in `PageHead.tsx` 145 | 4. Change colors in `public/manifest.json` 146 | 147 | ### Login/Signup with Firebase Authentication 148 | 149 | You need to enable Email/Password authentication in https://console.firebase.google.com/project/YOURAPP/authentication/providers 150 | 151 | ## Deploying on Vercel 152 | 153 | > Note: If you set up your project using the Deploy button, you need to clone your own repo instead of this repository. 154 | 155 | Setup and deploy your own project using this template with [Vercel](https://vercel.com). All you’ll need is your Firebase Public API Key. 156 | 157 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/import/git?s=https%3A%2F%2Fgithub.com%2Ftomsoderlund%2Fnextjs-pwa-firebase-boilerplate&env=NEXT_PUBLIC_FIREBASE_API_KEY&envDescription=Enter%20your%20public%20Firebase%20API%20Key&envLink=https://github.com/tomsoderlund/nextjs-pwa-firebase-boilerplate#deploying-with-vercel) 158 | -------------------------------------------------------------------------------- /components/articles/ArticleDetails.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { Article } from 'hooks/useArticles' 4 | 5 | const ArticleDetails = ({ article }: { article: Article }) => { 6 | return ( 7 | <> 8 |

{article.name}

9 |

{article.dateCreated.toString()}

10 |

{article.content}

11 | 12 | ) 13 | } 14 | export default ArticleDetails 15 | -------------------------------------------------------------------------------- /components/articles/ArticleList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { useArticles } from 'hooks/useArticles' 4 | 5 | import ArticleListItem from './ArticleListItem' 6 | 7 | const ArticleList = () => { 8 | const { articles } = useArticles() 9 | 10 | if (articles === null) return 'Loading...' 11 | 12 | return ( 13 |
14 | {articles?.map(article => ( 15 | 20 | ))} 21 |
22 | ) 23 | } 24 | export default ArticleList 25 | -------------------------------------------------------------------------------- /components/articles/ArticleListItem.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | 3 | import showNotification from 'lib/showNotification' 4 | import { Article, useArticles, articlePath } from 'hooks/useArticles' 5 | 6 | interface ArticleListItemProps { 7 | article: Article 8 | inProgress: boolean 9 | } 10 | 11 | const ArticleListItem = ({ article, inProgress = false }: ArticleListItemProps) => { 12 | const promptAndUpdateArticle = usePromptAndUpdateArticle(article, 'name') 13 | const promptAndDeleteArticle = usePromptAndDeleteArticle(article) 14 | 15 | return ( 16 |
20 | 21 | {article.name} 22 | 23 | 24 | 25 | 29 | Update 30 | 31 | 35 | Delete 36 | 37 | 38 | 39 | 70 |
71 | ) 72 | } 73 | export default ArticleListItem 74 | 75 | const usePromptAndUpdateArticle = (article: Article, fieldName: keyof Article) => { 76 | const { updateArticle } = useArticles() 77 | 78 | const handleUpdate = async () => { 79 | const newValue = window.prompt(`New value for ${fieldName}?`, article[fieldName] as string) 80 | if (newValue !== null) { 81 | const notificationId = showNotification('Updating article...') 82 | await updateArticle?.({ 83 | id: article.id, 84 | [fieldName]: (newValue === '' ? null : newValue) 85 | }) 86 | showNotification('Article updated', 'success', { notificationId }) 87 | } 88 | } 89 | 90 | return handleUpdate 91 | } 92 | 93 | const usePromptAndDeleteArticle = (article: Article) => { 94 | const { deleteArticle } = useArticles() 95 | 96 | const handleDelete = async () => { 97 | if (window.confirm(`Delete ${article.name}?`)) { 98 | const notificationId = showNotification('Deleting article...') 99 | await deleteArticle?.(article.id as string) 100 | showNotification('Article deleted', 'success', { notificationId }) 101 | } 102 | } 103 | 104 | return handleDelete 105 | } 106 | -------------------------------------------------------------------------------- /components/articles/CreateArticleForm.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | 3 | import showNotification from 'lib/showNotification' 4 | import { useArticles } from 'hooks/useArticles' 5 | import InputWithLabel from 'components/common/InputWithLabel' 6 | 7 | const DEFAULT_INPUTS = { name: '' } 8 | 9 | const useCreateArticleForm = () => { 10 | const [inputs, setInputs] = useState(DEFAULT_INPUTS) 11 | const { createArticle } = useArticles() 12 | const [inProgress, setInProgress] = useState(false) 13 | 14 | const handleInputChange = ({ target }: React.ChangeEvent) => { 15 | const value = (target instanceof HTMLInputElement && target.type === 'checkbox') ? target.checked : target.value 16 | setInputs({ ...inputs, [target.name]: value }) 17 | } 18 | 19 | const handleSubmit = async (event: React.FormEvent) => { 20 | if (event) event.preventDefault() 21 | setInProgress(true) 22 | const notificationId = showNotification('Creating new article...') 23 | await createArticle?.(inputs) 24 | // Clear input form when done 25 | setInputs(DEFAULT_INPUTS) 26 | setInProgress(false) 27 | showNotification('Article created', 'success', { notificationId }) 28 | } 29 | 30 | return { inputs, inProgress, handleInputChange, handleSubmit } 31 | } 32 | 33 | const CreateArticleForm = () => { 34 | const { inputs, inProgress, handleInputChange, handleSubmit } = useCreateArticleForm() 35 | return ( 36 |
37 | 48 | 49 | 56 | 57 | 63 | 64 | ) 65 | } 66 | export default CreateArticleForm 67 | -------------------------------------------------------------------------------- /components/common/Icon.tsx: -------------------------------------------------------------------------------- 1 | // Setup: 2 | // 1. yarn add react-svg-inline raw-loader 3 | // 2. Uncomment 'webpack' section in next.config.js 4 | import React from 'react' 5 | import SVGInline from 'react-svg-inline' 6 | 7 | const DEFAULT_SIZE = '16' 8 | 9 | interface IconProps { 10 | type?: string 11 | width?: string 12 | height?: string 13 | color?: string 14 | rotation?: number 15 | } 16 | 17 | const Icon = ({ type = 'home', width = DEFAULT_SIZE, height = DEFAULT_SIZE, color = 'rgba(0, 0, 0, 0.85)', rotation }: IconProps) => { 18 | return ( 19 | 33 | ) 34 | } 35 | export default Icon 36 | -------------------------------------------------------------------------------- /components/common/InputWithLabel.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | interface InputWithLabelProps { 4 | name: string 5 | label?: string 6 | placeholder?: string 7 | value?: string 8 | type?: string 9 | autoComplete?: string 10 | onChange?: (event: React.ChangeEvent | React.ChangeEvent) => void 11 | required?: boolean 12 | disabled?: boolean 13 | } 14 | 15 | const InputWithLabel: React.FC = ({ 16 | name, 17 | label, 18 | placeholder, 19 | value, 20 | type, 21 | autoComplete, 22 | onChange, 23 | required, 24 | disabled 25 | }) => { 26 | return ( 27 |
28 | 29 | {type === 'multiline' 30 | ? ( 31 |