├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── README.md ├── next-env.d.ts ├── next.config.js ├── package.json ├── postcss.config.js ├── public ├── favicon.ico ├── favicon │ ├── android-chrome-192x192.png │ ├── android-chrome-256x256.png │ ├── apple-touch-icon.png │ ├── browserconfig.xml │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── mstile-150x150.png │ ├── safari-pinned-tab.svg │ └── site.webmanifest ├── fonts │ ├── Inter-italic.var-latin.woff2 │ ├── Inter-italic.var.woff2 │ ├── Inter-roman.var-latin.woff2 │ └── Inter-roman.var.woff2 ├── img │ ├── annie-spratt-QckxruozjRg-unsplash.jpg │ ├── ayo-ogunseinde-sibvworyqs0-unsplash.jpg │ ├── foto-sushi-6anudmpilw4-unsplash.jpg │ ├── harps-joseph-tavpde7fxgy-unsplash.jpg │ ├── headway-jfR5wu2hMI0-unsplash.jpg │ ├── kal-visuals-b1hg7qi-zcc-unsplash.jpg │ ├── logo-cms.jpg │ ├── logo.png │ ├── nesa-by-makers-IgUR1iX0mqM-unsplash.jpg │ ├── scott-graham-5fNmWej4tAA-unsplash.jpg │ ├── screenshot.png │ ├── serverless-saas.jpg │ └── xps-ezyq1hol5_8-unsplash.jpg └── vercel.svg ├── src ├── cms │ ├── config.ts │ └── previews │ │ ├── HomePreview.tsx │ │ └── PostPreview.tsx ├── components │ ├── elements │ │ └── Button.tsx │ ├── forms │ │ ├── LoginForm.tsx │ │ ├── ResetPasswordForm.tsx │ │ └── SignUpForm.tsx │ ├── home │ │ ├── BlogCard.tsx │ │ ├── BlogSection │ │ │ ├── Posts1.tsx │ │ │ ├── Posts2.tsx │ │ │ ├── Posts3.tsx │ │ │ └── index.tsx │ │ ├── FeatureSection │ │ │ ├── Features1.tsx │ │ │ ├── Features2.tsx │ │ │ ├── Features3.tsx │ │ │ └── index.tsx │ │ ├── Footer.tsx │ │ ├── Header.tsx │ │ ├── HeroSection │ │ │ ├── Hero1.tsx │ │ │ ├── Hero2.tsx │ │ │ ├── Hero3.tsx │ │ │ └── index.tsx │ │ ├── Layout.tsx │ │ ├── PricingSection │ │ │ └── index.tsx │ │ ├── SEO.js │ │ ├── StepsSection │ │ │ ├── Steps1.tsx │ │ │ ├── Steps2.tsx │ │ │ ├── Steps3.tsx │ │ │ └── index.tsx │ │ └── TeamSection │ │ │ ├── Team1.tsx │ │ │ ├── Team2.tsx │ │ │ ├── Team3.tsx │ │ │ └── index.tsx │ └── icons │ │ └── Spinner.tsx ├── content │ ├── pages │ │ └── home.md │ └── posts │ │ ├── code-splitting-by-routes-and-components.md │ │ ├── here-is-what-i-learned-at-the-worlds-biggest-react-conference.md │ │ ├── interesting-web-technologies-to-follow-in-2019.md │ │ └── why-side-projects-are-so-damn-important.md ├── css │ └── tailwind.css ├── interfaces │ └── home.ts ├── pages │ ├── _app.tsx │ ├── admin.tsx │ ├── blog │ │ ├── [slug].tsx │ │ └── index.tsx │ ├── index.tsx │ ├── login.tsx │ ├── reset-password.tsx │ └── signup.tsx └── utils │ └── getIcon.tsx ├── tailwind.config.js ├── tsconfig.json └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "extends": [ 4 | "plugin:@typescript-eslint/recommended", 5 | "plugin:react/recommended", 6 | "plugin:prettier/recommended" 7 | ], 8 | "plugins": ["@typescript-eslint", "react", "react-hooks", "prettier"], 9 | "rules": { 10 | "react/react-in-jsx-scope": "off", 11 | "@typescript-eslint/no-explicit-any": "off", 12 | "react/prop-types": "off", 13 | "react-hooks/rules-of-hooks": "warn", 14 | "react-hooks/exhaustive-deps": "warn" 15 | }, 16 | "globals": { 17 | "React": "writable" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.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 | firestore-debug.log 26 | ui-debug.log 27 | 28 | # local env files 29 | .env.local 30 | .env.development.local 31 | .env.test.local 32 | .env.production.local 33 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NextWind - A flexible landing page & blog template 2 | 3 | Made with [Next.js](https://nextjs.org/docs), [TypeScript](https://www.typescriptlang.org/), [Netlify CMS](https://www.netlifycms.org/), [Tailwind CSS](https://tailwindcss.com/) and [Tailblocks](https://mertjf.github.io/tailblocks/). 4 | 5 |
6 | 7 |
8 | 9 | ## Getting Started 10 | 11 | #### Netlify CMS 12 | 13 | Netlify CMS is an open-source git-based content management library. Content is stored in your Git repository alongside your code for easier versioning, multi-channel publishing, and the option to handle content updates directly in Git. It's like a UI for editing your markdown files that we use to show the landing page and the blog posts. 14 | 15 | - Create a new repository on [Github](https://github.com/) 16 | - Open `cms/config.js` in this project and update `backend.repo` with your new repository name. 17 | - Push the code of this project to this new repo. 18 | 19 | You can now start the project with `yarn dev` or `npm run dev` and navigate to `/admin`. You can now login with Github and manage the content of the landing page of blog posts with a nice UI. When you make a change you can hit the "Publish" button, this will result in making a commit to your repository with the changes made to the corresponding markdown file. 20 | 21 | Before deploying your application you need to enable basic GitHub authentication. Follow the authentication provider setup steps in the [Netlify docs](https://docs.netlify.com/visitor-access/oauth-provider-tokens/#setup-and-settings). 22 | 23 | #### Test 24 | 25 | You can use the `test-repo` backend to try out Netlify CMS without connecting to a Git repo. With this backend, you can write and publish content normally, but any changes will disappear when you reload the page. 26 | 27 | Note: The test-repo backend can't access your local file system, nor does it connect to a Git repo, thus you won't see any existing files while using it. 28 | 29 | To enable this backend, add the `test-repo` string to your `cms/config.js` file: 30 | 31 | ``` 32 | backend: { 33 | name: 'test-repo', 34 | ... 35 | }, 36 | ``` 37 | 38 | Note: It's recommended to connect your git repo with Vercel or Netlify for automatic deployments. When you hit the "publish" button inside the CMS, a commit will be made to your repo that includes the changes you made to the page. With automatic deployments activated this means a deployment will be triggered after you publish any changes. After this deployment, your changes will be live. 39 | 40 | #### Deploy 41 | 42 | Because Github requires a server for authentication, Netlify facilitates basic GitHub authentication. 43 | 44 | To enable basic GitHub authentication follow the authentication provider setup steps in the [Netlify docs](https://docs.netlify.com/visitor-access/oauth-provider-tokens/#setup-and-settings). 45 | 46 | --- 47 | 48 | ## Need more features? 49 | 50 | [Serverless SaaS](https://serverless.page) is a modern starter-kit that aims to be the perfect starting point for your next React app to build full-stack SaaS applications. Save time and skip implementing authentication, payments, teams, and more. 51 | 52 | --- 53 | 54 | ## Learn More 55 | 56 | To learn more about Next.js, take a look at the following resources: 57 | 58 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API 59 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial 60 | - [Netlify CMS](https://www.netlifycms.org/) - learn about Netlify CMS 61 | - [Tailwind CSS](https://tailwindcss.com/) - learn about Tailwind CSS 62 | - [Tailblocks](https://mertjf.github.io/tailblocks/) - ready-to-use Tailwind CSS blocks 63 | - [Serverless SaaS](https://serverless.page) - premium SaaS boilerplate 64 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | webpack: (cfg) => { 3 | cfg.module.rules.push({ 4 | test: /\.md$/, 5 | loader: 'frontmatter-markdown-loader', 6 | }); 7 | return cfg; 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextwind", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "build": "next build", 6 | "dev": "next dev", 7 | "export": "next export", 8 | "lint": "eslint --ext .ts,.tsx", 9 | "lint:fix": "eslint --ext .ts,.tsx --fix", 10 | "start": "next start" 11 | }, 12 | "husky": { 13 | "hooks": { 14 | "pre-commit": "pretty-quick --staged && npm run lint" 15 | } 16 | }, 17 | "dependencies": { 18 | "@stripe/stripe-js": "1.9.0", 19 | "@tailwindcss/typography": "0.2.0", 20 | "autoprefixer": "10.0.2", 21 | "netlify-cms-app": "2.12.25", 22 | "next": "10.0.0", 23 | "postcss": "8.1.8", 24 | "react": "16.13.1", 25 | "react-dom": "16.13.1", 26 | "react-hook-form": "6.9.2", 27 | "tailwindcss": "2.0.1" 28 | }, 29 | "devDependencies": { 30 | "@types/node": "14.11.5", 31 | "@types/react": "16.9.51", 32 | "@types/webpack-env": "1.15.3", 33 | "@typescript-eslint/eslint-plugin": "4.4.0", 34 | "@typescript-eslint/parser": "4.4.0", 35 | "eslint": "7.10.0", 36 | "eslint-config-prettier": "6.12.0", 37 | "eslint-plugin-prettier": "3.1.4", 38 | "eslint-plugin-react": "7.21.3", 39 | "eslint-plugin-react-hooks": "4.1.2", 40 | "frontmatter-markdown-loader": "3.6.1", 41 | "husky": "4.3.0", 42 | "lint-staged": "10.4.0", 43 | "prettier": "^2.2.1", 44 | "pretty-quick": "3.0.2", 45 | "typescript": "4.0.3" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeprins/nextwind/ca7843c4405a27dbc9f904af209831e5f35fded4/public/favicon.ico -------------------------------------------------------------------------------- /public/favicon/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeprins/nextwind/ca7843c4405a27dbc9f904af209831e5f35fded4/public/favicon/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/favicon/android-chrome-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeprins/nextwind/ca7843c4405a27dbc9f904af209831e5f35fded4/public/favicon/android-chrome-256x256.png -------------------------------------------------------------------------------- /public/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeprins/nextwind/ca7843c4405a27dbc9f904af209831e5f35fded4/public/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /public/favicon/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #2b5797 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /public/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeprins/nextwind/ca7843c4405a27dbc9f904af209831e5f35fded4/public/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeprins/nextwind/ca7843c4405a27dbc9f904af209831e5f35fded4/public/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeprins/nextwind/ca7843c4405a27dbc9f904af209831e5f35fded4/public/favicon/favicon.ico -------------------------------------------------------------------------------- /public/favicon/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeprins/nextwind/ca7843c4405a27dbc9f904af209831e5f35fded4/public/favicon/mstile-150x150.png -------------------------------------------------------------------------------- /public/favicon/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 55 | 56 | 58 | 59 | 60 | 62 | 64 | 65 | 67 | 68 | 70 | 72 | 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /public/favicon/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "NextWind", 3 | "short_name": "NextWind", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-256x256.png", 12 | "sizes": "256x256", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /public/fonts/Inter-italic.var-latin.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeprins/nextwind/ca7843c4405a27dbc9f904af209831e5f35fded4/public/fonts/Inter-italic.var-latin.woff2 -------------------------------------------------------------------------------- /public/fonts/Inter-italic.var.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeprins/nextwind/ca7843c4405a27dbc9f904af209831e5f35fded4/public/fonts/Inter-italic.var.woff2 -------------------------------------------------------------------------------- /public/fonts/Inter-roman.var-latin.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeprins/nextwind/ca7843c4405a27dbc9f904af209831e5f35fded4/public/fonts/Inter-roman.var-latin.woff2 -------------------------------------------------------------------------------- /public/fonts/Inter-roman.var.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeprins/nextwind/ca7843c4405a27dbc9f904af209831e5f35fded4/public/fonts/Inter-roman.var.woff2 -------------------------------------------------------------------------------- /public/img/annie-spratt-QckxruozjRg-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeprins/nextwind/ca7843c4405a27dbc9f904af209831e5f35fded4/public/img/annie-spratt-QckxruozjRg-unsplash.jpg -------------------------------------------------------------------------------- /public/img/ayo-ogunseinde-sibvworyqs0-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeprins/nextwind/ca7843c4405a27dbc9f904af209831e5f35fded4/public/img/ayo-ogunseinde-sibvworyqs0-unsplash.jpg -------------------------------------------------------------------------------- /public/img/foto-sushi-6anudmpilw4-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeprins/nextwind/ca7843c4405a27dbc9f904af209831e5f35fded4/public/img/foto-sushi-6anudmpilw4-unsplash.jpg -------------------------------------------------------------------------------- /public/img/harps-joseph-tavpde7fxgy-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeprins/nextwind/ca7843c4405a27dbc9f904af209831e5f35fded4/public/img/harps-joseph-tavpde7fxgy-unsplash.jpg -------------------------------------------------------------------------------- /public/img/headway-jfR5wu2hMI0-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeprins/nextwind/ca7843c4405a27dbc9f904af209831e5f35fded4/public/img/headway-jfR5wu2hMI0-unsplash.jpg -------------------------------------------------------------------------------- /public/img/kal-visuals-b1hg7qi-zcc-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeprins/nextwind/ca7843c4405a27dbc9f904af209831e5f35fded4/public/img/kal-visuals-b1hg7qi-zcc-unsplash.jpg -------------------------------------------------------------------------------- /public/img/logo-cms.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeprins/nextwind/ca7843c4405a27dbc9f904af209831e5f35fded4/public/img/logo-cms.jpg -------------------------------------------------------------------------------- /public/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeprins/nextwind/ca7843c4405a27dbc9f904af209831e5f35fded4/public/img/logo.png -------------------------------------------------------------------------------- /public/img/nesa-by-makers-IgUR1iX0mqM-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeprins/nextwind/ca7843c4405a27dbc9f904af209831e5f35fded4/public/img/nesa-by-makers-IgUR1iX0mqM-unsplash.jpg -------------------------------------------------------------------------------- /public/img/scott-graham-5fNmWej4tAA-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeprins/nextwind/ca7843c4405a27dbc9f904af209831e5f35fded4/public/img/scott-graham-5fNmWej4tAA-unsplash.jpg -------------------------------------------------------------------------------- /public/img/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeprins/nextwind/ca7843c4405a27dbc9f904af209831e5f35fded4/public/img/screenshot.png -------------------------------------------------------------------------------- /public/img/serverless-saas.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeprins/nextwind/ca7843c4405a27dbc9f904af209831e5f35fded4/public/img/serverless-saas.jpg -------------------------------------------------------------------------------- /public/img/xps-ezyq1hol5_8-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeprins/nextwind/ca7843c4405a27dbc9f904af209831e5f35fded4/public/img/xps-ezyq1hol5_8-unsplash.jpg -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /src/cms/config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | cms_manual_init: true, 3 | backend: { 4 | name: 'github', 5 | repo: 'jakeprins/nextwind', 6 | branch: 'master', 7 | }, 8 | media_folder: 'public/img', 9 | public_folder: 'img', 10 | logo_url: 'https://demo.serverless.page/img/logo-cms.jpg', 11 | site_url: 'https://demo.serverless.page', 12 | collections: [ 13 | { 14 | name: 'pages', 15 | label: 'Pages', 16 | files: [ 17 | { 18 | label: 'Home', 19 | name: 'home', 20 | file: 'src/content/pages/home.md', 21 | fields: [ 22 | { 23 | label: 'Hero Section Version', 24 | name: 'hero_version', 25 | widget: 'number', 26 | value_type: 'int', 27 | min: 1, 28 | max: 3, 29 | }, 30 | { 31 | label: 'Hero Title', 32 | name: 'hero_title', 33 | widget: 'string', 34 | }, 35 | { 36 | label: 'Hero Description', 37 | name: 'hero_description', 38 | widget: 'markdown', 39 | }, 40 | { 41 | label: 'Hero Image', 42 | name: 'hero_image', 43 | widget: 'image', 44 | }, 45 | { 46 | label: 'Feature Section Version', 47 | name: 'feature_version', 48 | widget: 'number', 49 | value_type: 'int', 50 | min: 1, 51 | max: 3, 52 | }, 53 | { 54 | label: 'Feature Title', 55 | name: 'feature_title', 56 | widget: 'string', 57 | }, 58 | { 59 | label: 'Feature Description', 60 | name: 'feature_description', 61 | widget: 'string', 62 | }, 63 | { 64 | label: 'Features', 65 | name: 'features', 66 | widget: 'list', 67 | fields: [ 68 | { 69 | label: 'Name', 70 | name: 'name', 71 | widget: 'string', 72 | }, 73 | { 74 | label: 'Description', 75 | name: 'description', 76 | widget: 'text', 77 | }, 78 | ], 79 | }, 80 | { 81 | label: 'Steps Section Version', 82 | name: 'steps_version', 83 | widget: 'number', 84 | value_type: 'int', 85 | min: 1, 86 | max: 3, 87 | }, 88 | { 89 | label: 'Steps', 90 | name: 'steps', 91 | widget: 'list', 92 | fields: [ 93 | { 94 | label: 'Name', 95 | name: 'name', 96 | widget: 'string', 97 | }, 98 | { 99 | label: 'Description', 100 | name: 'description', 101 | widget: 'text', 102 | }, 103 | ], 104 | }, 105 | { 106 | label: 'Steps Image', 107 | name: 'steps_image', 108 | widget: 'image', 109 | }, 110 | { 111 | label: 'Pricing Title', 112 | name: 'pricing_title', 113 | widget: 'string', 114 | }, 115 | { 116 | label: 'Pricing Description', 117 | name: 'pricing_description', 118 | widget: 'string', 119 | }, 120 | { 121 | label: 'Plans', 122 | name: 'plans', 123 | widget: 'list', 124 | fields: [ 125 | { 126 | label: 'Name', 127 | name: 'name', 128 | widget: 'string', 129 | }, 130 | { 131 | label: 'Description', 132 | name: 'description', 133 | widget: 'text', 134 | }, 135 | { 136 | label: 'Price', 137 | name: 'price', 138 | widget: 'string', 139 | }, 140 | { 141 | label: 'USPs', 142 | name: 'usps', 143 | widget: 'list', 144 | }, 145 | ], 146 | }, 147 | { 148 | label: 'Team Section Version', 149 | name: 'team_version', 150 | widget: 'number', 151 | value_type: 'int', 152 | min: 1, 153 | max: 3, 154 | }, 155 | { 156 | label: 'Team Title', 157 | name: 'team_title', 158 | widget: 'string', 159 | }, 160 | { 161 | label: 'Team Description', 162 | name: 'team_description', 163 | widget: 'markdown', 164 | }, 165 | { 166 | label: 'Team', 167 | name: 'team', 168 | widget: 'list', 169 | fields: [ 170 | { 171 | label: 'Name', 172 | name: 'name', 173 | widget: 'string', 174 | }, 175 | { 176 | label: 'Description', 177 | name: 'description', 178 | widget: 'text', 179 | }, 180 | { 181 | label: 'Position', 182 | name: 'position', 183 | widget: 'string', 184 | }, 185 | { 186 | label: 'Image', 187 | name: 'image', 188 | widget: 'image', 189 | }, 190 | ], 191 | }, 192 | { 193 | label: 'Blog Section Version', 194 | name: 'blog_version', 195 | widget: 'number', 196 | value_type: 'int', 197 | min: 1, 198 | max: 3, 199 | }, 200 | { 201 | label: 'Blog Title', 202 | name: 'blog_title', 203 | widget: 'string', 204 | }, 205 | { 206 | label: 'Blog Description', 207 | name: 'blog_description', 208 | widget: 'string', 209 | }, 210 | { 211 | label: 'Featured Posts', 212 | name: 'posts', 213 | widget: 'relation', 214 | collection: 'posts', 215 | searchFields: ['title'], 216 | valueField: '{{slug}}', 217 | displayFields: ['title'], 218 | multiple: true, 219 | }, 220 | ], 221 | }, 222 | ], 223 | }, 224 | { 225 | name: 'posts', 226 | label: 'Posts', 227 | folder: 'src/content/posts', 228 | create: true, 229 | slug: '{{slug}}', 230 | preview_path: 'posts/{{fields.slug}}', 231 | fields: [ 232 | { 233 | label: 'Title', 234 | name: 'title', 235 | widget: 'string', 236 | }, 237 | { 238 | label: 'Slug', 239 | name: 'slug', 240 | widget: 'string', 241 | }, 242 | { 243 | label: 'Draft', 244 | name: 'draft', 245 | widget: 'boolean', 246 | default: true, 247 | }, 248 | { 249 | label: 'Publish Date', 250 | name: 'date', 251 | widget: 'datetime', 252 | }, 253 | { 254 | label: 'Description', 255 | name: 'description', 256 | widget: 'text', 257 | }, 258 | { 259 | label: 'Category', 260 | name: 'category', 261 | widget: 'string', 262 | }, 263 | { 264 | label: 'Image', 265 | name: 'image', 266 | widget: 'image', 267 | }, 268 | { 269 | label: 'Body', 270 | name: 'body', 271 | widget: 'markdown', 272 | }, 273 | { 274 | label: 'Tags', 275 | name: 'tags', 276 | widget: 'list', 277 | }, 278 | ], 279 | }, 280 | ], 281 | }; 282 | -------------------------------------------------------------------------------- /src/cms/previews/HomePreview.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentType } from 'react'; 2 | import HeroSection from 'components/home/HeroSection'; 3 | import FeatureSection from 'components/home/FeatureSection'; 4 | import StepsSection from 'components/home/StepsSection'; 5 | import TeamSection from 'components/home/TeamSection'; 6 | import PricingSection from 'components/home/PricingSection'; 7 | import BlogSection from 'components/home/BlogSection'; 8 | 9 | const HomePreview: ComponentType = ({ entry, widgetsFor }) => { 10 | const features = []; 11 | widgetsFor('features').map((feature) => { 12 | features.push({ 13 | name: feature?.getIn(['data', 'name']), 14 | description: feature?.getIn(['data', 'description']), 15 | }); 16 | }); 17 | 18 | const steps = []; 19 | widgetsFor('steps').map((step) => { 20 | steps.push({ 21 | name: step?.getIn(['data', 'name']), 22 | description: step?.getIn(['data', 'description']), 23 | }); 24 | }); 25 | 26 | const plans = []; 27 | widgetsFor('plans').map((plan) => { 28 | plans.push({ 29 | name: plan?.getIn(['data', 'name']), 30 | description: plan?.getIn(['data', 'description']), 31 | price: plan?.getIn(['data', 'price']), 32 | usps: plan?.getIn(['data', 'usps']), 33 | }); 34 | }); 35 | 36 | const slugs = []; 37 | widgetsFor('posts').map((post) => { 38 | slugs.push(post?.getIn(['data'])); 39 | }); 40 | 41 | const team = []; 42 | widgetsFor('team').map((member) => { 43 | team.push({ 44 | name: member?.getIn(['data', 'name']), 45 | description: member?.getIn(['data', 'description']), 46 | position: member?.getIn(['data', 'position']), 47 | image: member?.getIn(['data', 'image']), 48 | }); 49 | }); 50 | 51 | return ( 52 | <> 53 | 59 | 65 | 70 | 75 | 81 | 87 | 88 | ); 89 | }; 90 | 91 | export default HomePreview; 92 | -------------------------------------------------------------------------------- /src/cms/previews/PostPreview.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentType } from 'react'; 2 | import BlogCard from 'components/home/BlogCard'; 3 | 4 | const PostPreview: ComponentType = ({ entry, widgetFor }) => { 5 | const post = { 6 | title: entry.getIn(['data', 'title']), 7 | description: entry.getIn(['data', 'description']), 8 | image: entry.getIn(['data', 'image']), 9 | category: entry.getIn(['data', 'category']), 10 | body: widgetFor('body'), 11 | }; 12 | 13 | return ( 14 | <> 15 |
16 |
17 |
18 |
19 |

20 | {post.title} 21 |

22 |
23 |
24 |
25 | 26 |
27 |
28 |
29 | {post.body} 30 |
31 |
32 |
33 |
34 |
35 |

Preview

36 |
37 |
38 | 39 |
40 |
41 |
42 | 43 | ); 44 | }; 45 | 46 | export default PostPreview; 47 | -------------------------------------------------------------------------------- /src/components/elements/Button.tsx: -------------------------------------------------------------------------------- 1 | import Spinner from 'components/icons/Spinner'; 2 | 3 | interface ButtonProps { 4 | title?: string; 5 | isLoading?: boolean; 6 | } 7 | 8 | const Button = ({ 9 | isLoading = false, 10 | title, 11 | children, 12 | ...buttonProps 13 | }: ButtonProps & 14 | React.ButtonHTMLAttributes): JSX.Element => { 15 | return ( 16 | 27 | ); 28 | }; 29 | 30 | export default Button; 31 | -------------------------------------------------------------------------------- /src/components/forms/LoginForm.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { useForm } from 'react-hook-form'; 3 | import Link from 'next/link'; 4 | 5 | import Button from 'components/elements/Button'; 6 | 7 | interface LoginData { 8 | email: string; 9 | password: string; 10 | } 11 | 12 | const LoginForm: React.FC = () => { 13 | const { register, errors, handleSubmit } = useForm(); 14 | const [isLoading, setIsLoading] = useState(false); 15 | 16 | const onSubmit = (data: LoginData) => { 17 | console.log(data); 18 | setIsLoading(true); 19 | setTimeout(() => (window.location.href = 'https://serverless.page'), 3000); 20 | }; 21 | 22 | return ( 23 |
24 |
25 | 31 |
32 | ()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/, 41 | message: 'Not a valid email', 42 | }, 43 | })} 44 | /> 45 | {errors.email && ( 46 |
47 | {errors.email.message} 48 |
49 | )} 50 |
51 |
52 |
53 | 59 |
60 | 73 | {errors.password && ( 74 |
75 | {errors.password.message} 76 |
77 | )} 78 |
79 |
80 | 81 |
82 | 92 |
93 | 94 |
95 | 96 |
99 |
100 | ); 101 | }; 102 | 103 | export default LoginForm; 104 | -------------------------------------------------------------------------------- /src/components/forms/ResetPasswordForm.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { useForm } from 'react-hook-form'; 3 | 4 | import Button from 'components/elements/Button'; 5 | 6 | const ResetPasswordForm: React.FC = () => { 7 | const { register, errors, handleSubmit } = useForm(); 8 | const [isLoading, setIsLoading] = useState(false); 9 | 10 | const onSubmit = (data: { email: string }) => { 11 | console.log(data); 12 | setIsLoading(true); 13 | setTimeout((window.location.href = 'https://serverless.page'), 3000); 14 | }; 15 | 16 | return ( 17 |
18 |
19 | 25 |
26 | ()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/, 35 | message: 'Not a valid email', 36 | }, 37 | })} 38 | /> 39 | {errors.email && ( 40 |
41 | {errors.email.message} 42 |
43 | )} 44 |
45 |
46 | 47 |
48 | 49 |
52 |
53 | ); 54 | }; 55 | 56 | export default ResetPasswordForm; 57 | -------------------------------------------------------------------------------- /src/components/forms/SignUpForm.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { useForm } from 'react-hook-form'; 3 | 4 | import Button from 'components/elements/Button'; 5 | 6 | export interface SignUpData { 7 | name: string; 8 | email: string; 9 | password: string; 10 | } 11 | 12 | const SignUpForm: React.FC = () => { 13 | const { register, errors, handleSubmit } = useForm(); 14 | const [isLoading, setIsLoading] = useState(false); 15 | 16 | const onSubmit = (data: SignUpData): void => { 17 | console.log(data); 18 | setIsLoading(true); 19 | setTimeout(() => (window.location.href = 'https://serverless.page'), 3000); 20 | }; 21 | 22 | return ( 23 |
24 |
25 | 31 | 44 | {errors.name && ( 45 |

{errors.name.message}

46 | )} 47 |
48 |
49 | 55 |
56 | ()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/, 65 | message: 'Not a valid email', 66 | }, 67 | })} 68 | /> 69 | {errors.email && ( 70 |
71 | {errors.email.message} 72 |
73 | )} 74 |
75 |
76 |
77 | 83 |
84 | 97 | {errors.password && ( 98 |
99 | {errors.password.message} 100 |
101 | )} 102 |
103 |
104 | 105 |
106 | 107 |
110 |
111 | ); 112 | }; 113 | 114 | export default SignUpForm; 115 | -------------------------------------------------------------------------------- /src/components/home/BlogCard.tsx: -------------------------------------------------------------------------------- 1 | interface Post { 2 | attributes: { 3 | title: string; 4 | image: string; 5 | category: string; 6 | description: string; 7 | date?: Date; 8 | slug?: string; 9 | }; 10 | } 11 | 12 | const BlogCard: React.FC<{ post: Post; version: number }> = ({ 13 | post, 14 | version, 15 | }) => { 16 | const { attributes } = post; 17 | 18 | if (version === 2) { 19 | return ( 20 |
21 |
22 | 23 | {attributes.category} 24 | 25 | 26 | {attributes.date && 27 | new Date(attributes.date).toLocaleDateString().split('T')[0]} 28 | 29 |
30 |
31 |

32 | {attributes.title} 33 |

34 |

{attributes.description}

35 | 36 | 40 | Learn More 41 | 50 | 51 | 52 | 53 | 54 |
55 |
56 | ); 57 | } 58 | 59 | if (version === 3) { 60 | return ( 61 |
62 |
63 |

64 | {attributes.category} 65 |

66 |

67 | {attributes.title} 68 |

69 |

{attributes.description}

70 | 74 | Learn More 75 | 84 | 85 | 86 | 87 | 88 |
89 |
90 | ); 91 | } 92 | 93 | return ( 94 |
95 | blog 101 |
102 |

103 | {attributes.category} 104 |

105 |

106 | {attributes.title} 107 |

108 |

{attributes.description}

109 | 129 |
130 |
131 | ); 132 | }; 133 | 134 | export default BlogCard; 135 | -------------------------------------------------------------------------------- /src/components/home/BlogSection/Posts1.tsx: -------------------------------------------------------------------------------- 1 | const Posts1: React.FC<{ 2 | posts: { attributes: any }[]; 3 | }> = ({ posts }) => { 4 | return ( 5 |
6 | {posts?.map((post, i) => { 7 | return ( 8 |
9 |
10 | blog 16 |
17 |

18 | {post.attributes.category} 19 |

20 |

21 | {post.attributes.title} 22 |

23 |

24 | {post.attributes.description} 25 |

26 | 46 |
47 |
48 |
49 | ); 50 | })} 51 |
52 | ); 53 | }; 54 | 55 | export default Posts1; 56 | -------------------------------------------------------------------------------- /src/components/home/BlogSection/Posts2.tsx: -------------------------------------------------------------------------------- 1 | const Posts2: React.FC<{ 2 | posts: { attributes: any }[]; 3 | }> = ({ posts }) => { 4 | return ( 5 |
6 | {posts?.map((post, i) => { 7 | return ( 8 |
9 |
10 | 11 | {post.attributes.category} 12 | 13 | 14 | {post.attributes.date && 15 | new Date(post.attributes.date) 16 | .toLocaleDateString() 17 | .split('T')[0]} 18 | 19 |
20 |
21 |

22 | {post.attributes.title} 23 |

24 |

{post.attributes.description}

25 | 29 | Learn More 30 | 39 | 40 | 41 | 42 | 43 |
44 |
45 | ); 46 | })} 47 |
48 | ); 49 | }; 50 | 51 | export default Posts2; 52 | -------------------------------------------------------------------------------- /src/components/home/BlogSection/Posts3.tsx: -------------------------------------------------------------------------------- 1 | const Posts3: React.FC<{ 2 | posts: { attributes: any }[]; 3 | }> = ({ posts }) => { 4 | return ( 5 |
6 | {posts?.map((post, i) => { 7 | return ( 8 |
9 |
10 |
11 |

12 | {post.attributes.category} 13 |

14 |

15 | {post.attributes.title} 16 |

17 |

18 | {post.attributes.description} 19 |

20 | 24 | Learn More 25 | 34 | 35 | 36 | 37 | 38 |
39 |
40 |
41 | ); 42 | })} 43 |
44 | ); 45 | }; 46 | 47 | export default Posts3; 48 | -------------------------------------------------------------------------------- /src/components/home/BlogSection/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import Posts1 from './Posts1'; 3 | import Posts2 from './Posts2'; 4 | import Posts3 from './Posts3'; 5 | 6 | const BlogSection: React.FC<{ 7 | version: number; 8 | title: string; 9 | description: string; 10 | slugs: string[]; 11 | }> = ({ title, description, slugs, version }) => { 12 | const [posts, setPosts] = useState([]); 13 | 14 | useEffect(() => { 15 | const getPosts = async () => { 16 | const postsPromises = await slugs.map(async (slug) => { 17 | return import(`../../../content/posts/${slug}.md`); 18 | }); 19 | 20 | Promise.all(postsPromises).then(setPosts); 21 | }; 22 | 23 | getPosts(); 24 | }, [slugs]); 25 | 26 | // List of different component versions. You can easily switch between versions from the CMS. 27 | const components = { 28 | 1: Posts1, 29 | 2: Posts2, 30 | 3: Posts3, 31 | }; 32 | 33 | // Use the version prop to determine which component to render. Fallback to 1. 34 | const BlogPostList = components[version] || components[1]; 35 | 36 | return ( 37 |
38 |
39 |
40 |

41 | {title} 42 |

43 |

44 | {description} 45 |

46 |
47 | 48 |
49 |
50 | ); 51 | }; 52 | 53 | export default BlogSection; 54 | -------------------------------------------------------------------------------- /src/components/home/FeatureSection/Features1.tsx: -------------------------------------------------------------------------------- 1 | import { getIcon } from 'utils/getIcon'; 2 | import { Feature } from 'interfaces/home'; 3 | 4 | interface Props { 5 | features: Feature[]; 6 | } 7 | 8 | const Features1: React.FC = ({ features }) => { 9 | return ( 10 |
11 | {features?.map((feature: Feature, i: number) => { 12 | return ( 13 |
17 |
18 | {getIcon(i, 8)} 19 |
20 |
21 |

22 | {feature.name} 23 |

24 |

{feature.description}

25 |
26 |
27 | ); 28 | })} 29 |
30 | ); 31 | }; 32 | 33 | export default Features1; 34 | -------------------------------------------------------------------------------- /src/components/home/FeatureSection/Features2.tsx: -------------------------------------------------------------------------------- 1 | import { getIcon } from 'utils/getIcon'; 2 | import { Feature } from 'interfaces/home'; 3 | 4 | interface Props { 5 | features: Feature[]; 6 | } 7 | 8 | const Features2: React.FC = ({ features }) => { 9 | return ( 10 |
11 | {features?.map((feature: Feature, i: number) => { 12 | return ( 13 |
14 |
15 |
16 |
17 | {getIcon(i, 5)} 18 |
19 |

20 | {feature.name} 21 |

22 |
23 |
24 |

25 | {feature.description} 26 |

27 | 28 | Learn More 29 | 38 | 39 | 40 | 41 |
42 |
43 |
44 | ); 45 | })} 46 |
47 | ); 48 | }; 49 | 50 | export default Features2; 51 | -------------------------------------------------------------------------------- /src/components/home/FeatureSection/Features3.tsx: -------------------------------------------------------------------------------- 1 | import { getIcon } from 'utils/getIcon'; 2 | import { Feature } from 'interfaces/home'; 3 | 4 | interface Props { 5 | features: Feature[]; 6 | } 7 | 8 | const Features3: React.FC = ({ features }) => { 9 | return ( 10 |
11 | {features?.map((feature: Feature, i: number) => { 12 | return ( 13 |
14 |
15 | {getIcon(i, 10)} 16 |
17 |
18 |

19 | {feature.name} 20 |

21 |

{feature.description}

22 | 23 | Learn More 24 | 33 | 34 | 35 | 36 |
37 |
38 | ); 39 | })} 40 |
41 | ); 42 | }; 43 | 44 | export default Features3; 45 | -------------------------------------------------------------------------------- /src/components/home/FeatureSection/index.tsx: -------------------------------------------------------------------------------- 1 | import { Feature } from 'interfaces/home'; 2 | import Features1 from './Features1'; 3 | import Features2 from './Features2'; 4 | import Features3 from './Features3'; 5 | 6 | interface Props { 7 | title: string; 8 | description: string; 9 | features: Feature[]; 10 | version: number; 11 | } 12 | 13 | const FeatureSection: React.FC = ({ 14 | title, 15 | description, 16 | features, 17 | version, 18 | }) => { 19 | // List of different component versions. You can easily switch between versions from the CMS. 20 | const components = { 21 | 1: Features1, 22 | 2: Features2, 23 | 3: Features3, 24 | }; 25 | 26 | // Use the version prop to determine which component to render. Fallback to 1. 27 | const FeatureList = components[version] || components[1]; 28 | 29 | return ( 30 |
31 |
32 |
33 |

34 | {title} 35 |

36 |

37 | {description} 38 |

39 |
40 |
41 |
42 |
43 | 44 | 47 |
48 |
49 | ); 50 | }; 51 | 52 | export default FeatureSection; 53 | -------------------------------------------------------------------------------- /src/components/home/Footer.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | 3 | const Footer = (): JSX.Element => { 4 | return ( 5 | 83 | ); 84 | }; 85 | 86 | export default Footer; 87 | -------------------------------------------------------------------------------- /src/components/home/Header.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | 3 | const Header = (): JSX.Element => { 4 | return ( 5 |
6 |
7 | 8 | 9 | Serverless SaaS Boilerplate 14 | 15 | 16 | 41 |
42 | 43 | 46 | 47 | 48 | 51 | 52 |
53 |
54 |
55 | ); 56 | }; 57 | 58 | export default Header; 59 | -------------------------------------------------------------------------------- /src/components/home/HeroSection/Hero1.tsx: -------------------------------------------------------------------------------- 1 | interface Props { 2 | title: string; 3 | description: string; 4 | image: string; 5 | } 6 | 7 | const Hero1: React.FC = ({ title, description, image }): JSX.Element => { 8 | return ( 9 |
10 |
11 |
12 |

13 | {title} 14 |

15 |

{description}

16 |
17 | 20 | 23 |
24 |
25 |
26 | hero 32 |
33 |
34 |
35 | ); 36 | }; 37 | 38 | export default Hero1; 39 | -------------------------------------------------------------------------------- /src/components/home/HeroSection/Hero2.tsx: -------------------------------------------------------------------------------- 1 | interface Props { 2 | title: string; 3 | description: string; 4 | image: string; 5 | } 6 | 7 | const Hero2: React.FC = ({ title, description, image }): JSX.Element => { 8 | return ( 9 |
10 |
11 |
12 | hero 18 |
19 |
20 |

21 | {title} 22 |

23 |

{description}

24 |
25 | 28 | 31 |
32 |
33 |
34 |
35 | ); 36 | }; 37 | 38 | export default Hero2; 39 | -------------------------------------------------------------------------------- /src/components/home/HeroSection/Hero3.tsx: -------------------------------------------------------------------------------- 1 | interface Props { 2 | title: string; 3 | description: string; 4 | image: string; 5 | } 6 | 7 | const Hero3: React.FC = ({ title, description, image }): JSX.Element => { 8 | return ( 9 |
10 |
11 | hero 17 |
18 |

19 | {title} 20 |

21 |

{description}

22 |
23 | 26 | 29 |
30 |
31 |
32 |
33 | ); 34 | }; 35 | 36 | export default Hero3; 37 | -------------------------------------------------------------------------------- /src/components/home/HeroSection/index.tsx: -------------------------------------------------------------------------------- 1 | import Hero1 from './Hero1'; 2 | import Hero2 from './Hero2'; 3 | import Hero3 from './Hero3'; 4 | 5 | interface Props { 6 | version: number; 7 | title: string; 8 | description: string; 9 | image: string; 10 | } 11 | 12 | const HeroSection: React.FC = ({ 13 | version, 14 | title, 15 | description, 16 | image, 17 | }): JSX.Element => { 18 | // List of different component versions. You can easily switch between versions from the CMS. 19 | const components = { 20 | 1: Hero1, 21 | 2: Hero2, 22 | 3: Hero3, 23 | }; 24 | 25 | // Use the version prop to determine which component to render. Fallback to 1. 26 | const HeroComponent = components[version] || components[1]; 27 | 28 | // Default return version 1 (with hero image on the right) 29 | return ( 30 | 31 | ); 32 | }; 33 | 34 | export default HeroSection; 35 | -------------------------------------------------------------------------------- /src/components/home/Layout.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | import { NextPage } from 'next'; 3 | 4 | import Header from 'components/home/Header'; 5 | import Footer from 'components/home/Footer'; 6 | 7 | interface Props { 8 | children: ReactNode; 9 | } 10 | 11 | const Layout: NextPage = ({ children }) => { 12 | return ( 13 |
14 |
15 | {children} 16 |
17 |
18 | ); 19 | }; 20 | 21 | export default Layout; 22 | -------------------------------------------------------------------------------- /src/components/home/PricingSection/index.tsx: -------------------------------------------------------------------------------- 1 | interface Props { 2 | title: string; 3 | description: string; 4 | plans: Array<{ 5 | name: string; 6 | description: string; 7 | price: string; 8 | usps: { name: string }[]; 9 | }>; 10 | } 11 | 12 | const PricingSection: React.FC = ({ title, description, plans }) => { 13 | return ( 14 |
15 |
16 |
17 |

18 | {title} 19 |

20 |

21 | {description} 22 |

23 |
24 |
25 |
26 |
27 |
28 | {plans?.map((plan, i) => { 29 | return ( 30 |
31 |
32 |

33 | {plan.name} 34 |

35 |

36 | {plan.price} 37 | 38 | /mo 39 | 40 |

41 |
42 | {plan.usps?.map((usp, i) => { 43 | return ( 44 |

48 | 49 | 58 | 59 | 60 | 61 | {usp} 62 |

63 | ); 64 | })} 65 |
66 | 80 |

81 | {plan.description} 82 |

83 |
84 |
85 | ); 86 | })} 87 |
88 |
89 |
90 | ); 91 | }; 92 | 93 | export default PricingSection; 94 | -------------------------------------------------------------------------------- /src/components/home/SEO.js: -------------------------------------------------------------------------------- 1 | import Head from 'next/head'; 2 | 3 | const SEO = () => ( 4 | 5 | NextWind 6 | 10 | 14 | 15 | 16 | 17 | 21 | 22 | {/* Twitter */} 23 | 24 | 28 | 32 | 33 | 34 | 35 | {/* Favicon */} 36 | 41 | 47 | 53 | 54 | 59 | 60 | 61 | 62 | {/* Google Analytics */} 63 | {/*