├── src ├── style │ ├── index.ts │ └── styled.tsx ├── components │ ├── screens │ │ ├── 404 │ │ │ ├── index.ts │ │ │ ├── 404.models.ts │ │ │ └── 404.parameters.ts │ │ ├── 500 │ │ │ ├── index.ts │ │ │ ├── 500.models.ts │ │ │ └── 500.parameters.ts │ │ ├── api │ │ │ ├── index.ts │ │ │ └── api.models.ts │ │ └── index │ │ │ ├── subcomponents │ │ │ └── welcome │ │ │ │ ├── index.ts │ │ │ │ ├── Welcome.tsx │ │ │ │ └── Welcome.styled.tsx │ │ │ ├── index.ts │ │ │ ├── index.models.ts │ │ │ └── index.parameters.ts │ └── shared │ │ ├── button │ │ ├── index.ts │ │ ├── Button.model.ts │ │ ├── Button.parameters.tsx │ │ ├── Button.tsx │ │ └── Button.style.tsx │ │ ├── index.ts │ │ ├── layout │ │ ├── index.ts │ │ ├── layoutContainer │ │ │ ├── LayoutContainer.styled.tsx │ │ │ └── LayoutContainer.tsx │ │ ├── layout │ │ │ ├── Layout.tsx │ │ │ └── Layout.styled.tsx │ │ └── head │ │ │ └── CustomHead.tsx │ │ └── typography │ │ ├── caption │ │ ├── Captions.parameters.tsx │ │ ├── Captions.tsx │ │ └── Captions.styled.tsx │ │ ├── paragraphs │ │ ├── Paragraphs.parameters.tsx │ │ ├── Paragraphs.tsx │ │ └── Paragraphs.styled.tsx │ │ ├── index.ts │ │ ├── navigation │ │ ├── Navigation.tsx │ │ └── Navigation.styled.tsx │ │ └── heading │ │ ├── Headings.tsx │ │ └── Headings.styled.tsx ├── parameters │ ├── index.ts │ ├── constants.ts │ ├── page.ts │ └── general.ts ├── models │ ├── index.ts │ ├── email.ts │ ├── page.ts │ ├── image.ts │ └── blog.ts ├── utils │ ├── dashToUnderscore.ts │ ├── underscoreToDash.ts │ ├── index.ts │ └── getBase64.ts ├── services │ ├── index.ts │ ├── blog.ts │ ├── email.ts │ ├── mailchimp.ts │ ├── ga.ts │ └── axios.ts ├── pages │ ├── api │ │ ├── rss-blogs.ts │ │ ├── subscribe.js │ │ ├── unsubscribe.js │ │ └── email.ts │ ├── index.tsx │ ├── 500.tsx │ ├── 404.tsx │ ├── _document.tsx │ ├── rss.tsx │ └── _app.tsx └── hooks │ └── useWindowSize.tsx ├── .husky └── pre-commit ├── .prettierignore ├── next.config.js ├── .prettierrc ├── next-env.d.ts ├── .prettierrc.js ├── .babelrc ├── .env.example ├── README.md ├── public └── sitemap.xml ├── .gitignore ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── pull_request_template.md ├── tsconfig.json ├── package.json ├── folder.structure.md ├── eslintrc.js ├── CONTRIBUTING.md └── CODE_OF_CONDUCT.md /src/style/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./styled" -------------------------------------------------------------------------------- /src/components/screens/api/index.ts: -------------------------------------------------------------------------------- 1 | export * from './api.models' 2 | -------------------------------------------------------------------------------- /src/components/shared/button/index.ts: -------------------------------------------------------------------------------- 1 | export { Button } from './Button' -------------------------------------------------------------------------------- /src/components/screens/index/subcomponents/welcome/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Welcome'; -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /src/components/shared/index.ts: -------------------------------------------------------------------------------- 1 | export * from './layout/index' 2 | export * from './typography/index' -------------------------------------------------------------------------------- /src/components/screens/404/index.ts: -------------------------------------------------------------------------------- 1 | export { page404 } from './404.parameters' 2 | export * from './404.models' -------------------------------------------------------------------------------- /src/components/screens/500/index.ts: -------------------------------------------------------------------------------- 1 | export { page500 } from './500.parameters' 2 | export * from './500.models' -------------------------------------------------------------------------------- /src/parameters/index.ts: -------------------------------------------------------------------------------- 1 | export * from './general'; 2 | export * from './page'; 3 | export * from './constants'; 4 | -------------------------------------------------------------------------------- /src/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './page' 2 | export * from './email' 3 | export * from './blog' 4 | export * from './image' -------------------------------------------------------------------------------- /src/utils/dashToUnderscore.ts: -------------------------------------------------------------------------------- 1 | export function dashToUnderscore(slug: string): string { 2 | return slug.replace(/-/g, '_'); 3 | } 4 | -------------------------------------------------------------------------------- /src/utils/underscoreToDash.ts: -------------------------------------------------------------------------------- 1 | export function underscoreToDash(slug: string): string { 2 | return slug.replace(/_/g, '-'); 3 | } 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .env 2 | .prettierignore 3 | .gitignore 4 | *.otf 5 | *.png 6 | *.jpg 7 | *.jpeg 8 | *.svg 9 | yarn.lock 10 | -------------------------------------------------------------------------------- /src/components/screens/index/index.ts: -------------------------------------------------------------------------------- 1 | export { getHome } from './index.parameters' 2 | export * from './index.models' 3 | export * from './subcomponents/welcome' -------------------------------------------------------------------------------- /src/models/email.ts: -------------------------------------------------------------------------------- 1 | export interface ContactFormFinalData { 2 | fullName: string 3 | email: string 4 | message: string 5 | disclaimer: boolean 6 | company: string 7 | } -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export { dashToUnderscore } from './dashToUnderscore'; 2 | export { underscoreToDash } from './underscoreToDash'; 3 | export { getBase64 } from './getBase64'; -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const withBundleAnalyzer = require('@next/bundle-analyzer')({ 2 | enabled: process.env.ANALYZE === 'true' 3 | }); 4 | 5 | module.exports = withBundleAnalyzer({}); 6 | -------------------------------------------------------------------------------- /src/components/screens/index/index.models.ts: -------------------------------------------------------------------------------- 1 | import { WelcomeProps } from "./subcomponents/welcome"; 2 | 3 | export interface HomePageProps { 4 | welcome: WelcomeProps; 5 | } 6 | -------------------------------------------------------------------------------- /src/models/page.ts: -------------------------------------------------------------------------------- 1 | export interface MetaTagsModel { 2 | url: string; 3 | title: string; 4 | description: string; 5 | keywords: string; 6 | image: string; 7 | type?: string; 8 | } 9 | -------------------------------------------------------------------------------- /src/components/shared/layout/index.ts: -------------------------------------------------------------------------------- 1 | export { CustomHead } from './head/CustomHead'; 2 | export { LayoutContainer } from './layoutContainer/LayoutContainer'; 3 | export { Layout } from './layout/Layout'; 4 | -------------------------------------------------------------------------------- /src/components/shared/layout/layoutContainer/LayoutContainer.styled.tsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | 3 | export const Container = styled.div` 4 | max-width: 1120px; 5 | margin: 0 auto; 6 | padding: 0 24px; 7 | `; 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "tabWidth": 2, 4 | "printWidth": 150, 5 | "singleQuote": true, 6 | "trailingComma": "none", 7 | "jsxBracketSameLine": true, 8 | "semi": true, 9 | "arrowParens": "avoid" 10 | } 11 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | useTabs: true, 3 | tabWidth: 2, 4 | printWidth: 150, 5 | singleQuote: true, 6 | trailingComma: 'none', 7 | jsxBracketSameLine: true, 8 | semi: true, 9 | arrowParens: 'avoid' 10 | }; 11 | -------------------------------------------------------------------------------- /src/components/screens/500/500.models.ts: -------------------------------------------------------------------------------- 1 | import { MetaTagsModel } from "models" 2 | export interface I500Page { 3 | title: string 4 | text: string 5 | button: { 6 | text: string 7 | link: string 8 | } 9 | seoTags: MetaTagsModel 10 | } 11 | -------------------------------------------------------------------------------- /src/components/screens/404/404.models.ts: -------------------------------------------------------------------------------- 1 | import { MetaTagsModel } from "models" 2 | 3 | export interface I404Page { 4 | title: string 5 | text: string 6 | button: { 7 | text: string 8 | link: string 9 | } 10 | seoTags: MetaTagsModel 11 | } 12 | -------------------------------------------------------------------------------- /src/services/index.ts: -------------------------------------------------------------------------------- 1 | export { triggerEventGTM, getSource } from './ga'; 2 | export { postEmail } from './email'; 3 | export { postSubscribe } from './mailchimp'; 4 | export { getBlog } from './blog'; 5 | export { localAxios, remoteAxios } from './axios'; 6 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "next/babel", 5 | { 6 | "preset-react": { 7 | "runtime": "automatic", 8 | "importSource": "@emotion/react" 9 | } 10 | } 11 | ] 12 | ], 13 | "plugins": ["@emotion/babel-plugin"] 14 | } 15 | -------------------------------------------------------------------------------- /src/components/shared/typography/caption/Captions.parameters.tsx: -------------------------------------------------------------------------------- 1 | export enum FontSizesCaptions { 2 | S = '10px', 3 | M = '12px' 4 | } 5 | 6 | export enum LineHeightsCaptions { 7 | S = '1.1', 8 | M = '1.3' 9 | } 10 | 11 | export type CaptionSize = 's' | 'm'; 12 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | API_BASE_URL = 'https://{API_BASE_URL}' 2 | APP_BASE_URL = 'https://{APP_BASE_URL}' 3 | GA_TRACKING_ID={GA_TRACKING_ID} 4 | HOST={HOST} 5 | EMAIL_HOST={EMAIL_HOST} 6 | EMAIL_USERNAME={EMAIL_USERNAME} 7 | EMAIL_PASSWORD={EMAIL_PASSWORD} 8 | ANALYZE={'true'/'false'} 9 | -------------------------------------------------------------------------------- /src/components/shared/typography/paragraphs/Paragraphs.parameters.tsx: -------------------------------------------------------------------------------- 1 | export enum FontSizesParagraphs { 2 | S = '15px', 3 | M = '17px' 4 | } 5 | 6 | export enum LineHeightsParagraphs { 7 | S = '1.5', 8 | M = '1.7' 9 | } 10 | 11 | export type ParagraphsSize = 's' | 'm'; -------------------------------------------------------------------------------- /src/components/screens/index/index.parameters.ts: -------------------------------------------------------------------------------- 1 | import { Size } from 'hooks/useWindowSize'; 2 | import { HomePageProps } from './index.models'; 3 | 4 | export const getHome = ({ screenSize }: { screenSize: Size }): HomePageProps => ({ 5 | welcome: { 6 | title: 'Template' 7 | } 8 | }) 9 | -------------------------------------------------------------------------------- /src/parameters/constants.ts: -------------------------------------------------------------------------------- 1 | //STATUS CODES 2 | export const STATUS_CODE_UNAUTHORIZED = 401; 3 | export const STATUS_CODE_BAD_REQUEST = 400; 4 | export const STATUS_CODE_HTTP_204_NO_CONTENT = 204; 5 | 6 | //REQUEST STATUS 7 | export const BAD_REQUEST = "BAD_REQUEST"; 8 | export const GOOD_REQUEST = "GOOD_REQUEST"; 9 | -------------------------------------------------------------------------------- /src/parameters/page.ts: -------------------------------------------------------------------------------- 1 | import { MetaTagsModel } from 'models'; 2 | 3 | export const defaultMetaTags: MetaTagsModel = { 4 | title: 'Meta Title', 5 | description: 'Meta description', 6 | keywords: 'meta keywords', 7 | image: `${process.env.HOST}/images/meta/general.png`, 8 | url: `${process.env.HOST}` 9 | }; 10 | -------------------------------------------------------------------------------- /src/components/shared/typography/index.ts: -------------------------------------------------------------------------------- 1 | export { Heading1, Heading2, Heading3, Heading4, Heading5 } from './heading/Headings'; 2 | export { Tag, Paragraph, SubHeadline } from './paragraphs/Paragraphs'; 3 | export { NavItem, SubNavItem, FooterNavItem } from './navigation/Navigation'; 4 | export { Caption } from './caption/Captions'; -------------------------------------------------------------------------------- /src/utils/getBase64.ts: -------------------------------------------------------------------------------- 1 | export function getBase64(file: File): Promise { 2 | return new Promise((resolve, reject) => { 3 | const reader = new FileReader(); 4 | reader.readAsDataURL(file); 5 | reader.onload = () => resolve(reader.result ? reader.result.toString() : ''); 6 | reader.onerror = error => reject(error); 7 | }); 8 | } 9 | -------------------------------------------------------------------------------- /src/components/screens/api/api.models.ts: -------------------------------------------------------------------------------- 1 | export interface NodemailerFiles { 2 | filename: string; 3 | path: string; 4 | } 5 | 6 | export interface ContactFormFinalData { 7 | whatAreYouBuilding: string; 8 | whatServices: string; 9 | budget: string; 10 | fullName: string; 11 | email: string; 12 | message: string; 13 | disclaimer: boolean; 14 | company: string; 15 | } 16 | -------------------------------------------------------------------------------- /src/components/shared/layout/layoutContainer/LayoutContainer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Container } from './LayoutContainer.styled'; 3 | 4 | interface Props { 5 | children: React.ReactNode; 6 | } 7 | 8 | export const LayoutContainer = ({ children }: Props) => { 9 | return {children}; 10 | }; 11 | 12 | export default LayoutContainer; 13 | -------------------------------------------------------------------------------- /src/components/screens/index/subcomponents/welcome/Welcome.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Container, Title } from './Welcome.styled'; 4 | 5 | export interface WelcomeProps { 6 | title: string; 7 | } 8 | 9 | export const Welcome: React.FunctionComponent = ({ title }) => { 10 | return ( 11 | 12 | {title} 13 | 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /src/components/screens/index/subcomponents/welcome/Welcome.styled.tsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import { colors, devices } from 'parameters'; 3 | 4 | export const Container = styled.div` 5 | background-color: ${colors.background2}; 6 | color: ${colors.primaryBlue2}; 7 | padding: 96px 0; 8 | 9 | @media ${devices.laptop} { 10 | padding: 160px 0; 11 | } 12 | `; 13 | export const Title = styled.h1` 14 | text-align: center; 15 | `; 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Next Template 2 | Starter project made by www.cinnamon.agency 3 | 4 | ## Prequisite 5 | Having Node.js 12.22.0 or later installed. 6 | ## Setup 7 | - git clone the repo with `git clone https://github.com/Cinnamon-Agency/template-nextjs.git` 8 | - install dependencies by running `yarn install` in repo root. 9 | - set up the `.env` file 10 | 11 | ## Scripts 12 | 13 | - `yarn dev` - runs project in development mode 14 | - `yarn build` - builds the application for production 15 | -------------------------------------------------------------------------------- /src/models/image.ts: -------------------------------------------------------------------------------- 1 | import { StaticImageData } from "next/image" 2 | 3 | export interface Image { 4 | src: string 5 | alt: string 6 | width: number 7 | height: number 8 | } 9 | 10 | export interface Image11 { 11 | src: StaticImageData 12 | alt: string 13 | width: number 14 | height: number 15 | } 16 | 17 | export interface Shape { 18 | firstRadius?: number 19 | tl?: number 20 | tr?: number 21 | br?: number 22 | bl?: number 23 | } 24 | 25 | export interface ImageShape extends Image, Shape {} 26 | 27 | export interface Image11Shape extends Image11, Shape {} 28 | -------------------------------------------------------------------------------- /src/components/shared/button/Button.model.ts: -------------------------------------------------------------------------------- 1 | export interface TemplateButtonParameters { 2 | background: TemplateButtonState; 3 | outline: TemplateButtonState; 4 | text: TemplateButtonState; 5 | } 6 | 7 | export type ButtonVariant = 'primary' | 'secondary' | 'tertiary'; 8 | 9 | export type ButtonState = 'normal' | 'hover' | 'disabled'; 10 | 11 | export type ButtonSize = 'small' | 'base' | 'big'; 12 | 13 | export type TemplateButtonColors = { [key in ButtonVariant]: TemplateButtonParameters }; 14 | 15 | export type TemplateButtonState = { [key in ButtonState]: string }; 16 | -------------------------------------------------------------------------------- /public/sitemap.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | https://{APP_BASE_URL}/ 12 | 2022-04-13T11:24:25+00:00 13 | 1.00 14 | 15 | 16 | -------------------------------------------------------------------------------- /.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 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env 29 | .env.local 30 | .env.development* 31 | .env.test.local 32 | .env.production* 33 | 34 | # vercel 35 | .vercel 36 | 37 | # vscode 38 | .vscode 39 | 40 | # git 41 | yarn.lock -------------------------------------------------------------------------------- /src/components/shared/layout/layout/Layout.tsx: -------------------------------------------------------------------------------- 1 | import { MetaTagsModel } from 'models' 2 | import React, { useContext } from 'react'; 3 | import { LayoutWrapper } from './Layout.styled'; 4 | import { AppContext } from 'pages/_app' 5 | import { CustomHead } from 'components/shared'; 6 | 7 | interface LayoutProps { 8 | metaTags: MetaTagsModel 9 | children: React.ReactNode 10 | } 11 | 12 | export const Layout = ({ metaTags, children }: LayoutProps) => { 13 | return ( 14 | 15 | 16 | {children} 17 | 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /src/components/screens/500/500.parameters.ts: -------------------------------------------------------------------------------- 1 | import { routes } from 'parameters'; 2 | import { I500Page } from './500.models'; 3 | 4 | // Page500 5 | export const page500: I500Page = { 6 | title: 'Blooper...
Servers could not handle your awesomeness', 7 | text: 'Please try out later once they cool down.', 8 | button: { 9 | text: 'Back to Homepage', 10 | link: routes.home 11 | }, 12 | seoTags: { 13 | description: 'These are not the droids you are looking for', 14 | title: '404 Title', 15 | image: `${process.env.HOST}/images/meta/404.png`, 16 | url: `${process.env.HOST}/${routes[500]}`, 17 | keywords: 'keywords, missing, here' 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /src/components/shared/layout/layout/Layout.styled.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | import styled from '@emotion/styled'; 3 | 4 | 5 | 6 | interface LayoutWrapperProps { 7 | isFirstLoad: boolean 8 | } 9 | 10 | export const LayoutWrapper = styled.main` 11 | padding-top: 112px; /* height of fixed header */ 12 | 13 | ${props => 14 | props?.isFirstLoad && 15 | css` 16 | animation: MainFadeIn 433ms ease 2000ms 1 normal forwards running; 17 | opacity: 0; 18 | margin: -16px auto 0 auto; 19 | 20 | @keyframes MainFadeIn { 21 | to { 22 | opacity: 1; 23 | margin: 0 auto 0; 24 | } 25 | } 26 | `} 27 | ` 28 | -------------------------------------------------------------------------------- /src/pages/api/rss-blogs.ts: -------------------------------------------------------------------------------- 1 | import { remoteAxios } from 'services'; 2 | 3 | // this must be included, otherwise its not working, it disables nextjs parser 4 | export const config = { 5 | api: { 6 | bodyParser: false 7 | } 8 | }; 9 | 10 | export default async (req: any, res: any) => { 11 | if (req.method === 'GET') { 12 | remoteAxios 13 | .get(`/api/rss-blogs`) 14 | .then((response: any) => { 15 | res.status(200).send({ message: 'OK' }); 16 | return; 17 | }) 18 | .catch((err: Error) => { 19 | res.status(500).send({ err }); 20 | return; 21 | }); 22 | } else { 23 | res.status(403).send({ message: 'Unsupported request type' }); 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /src/hooks/useWindowSize.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | 3 | export interface Size { 4 | width: number; 5 | height: number; 6 | } 7 | 8 | export default function useWindowSize() { 9 | const [windowSize, setWindowSize] = useState({ 10 | width: 0, 11 | height: 0 12 | }); 13 | 14 | useEffect(() => { 15 | function handleResize() { 16 | setWindowSize({ 17 | width: window.innerWidth, 18 | height: window.innerHeight 19 | }); 20 | } 21 | 22 | window.addEventListener('resize', handleResize); 23 | 24 | handleResize(); 25 | 26 | return () => window.removeEventListener('resize', handleResize); 27 | }, []); 28 | 29 | return windowSize; 30 | } 31 | -------------------------------------------------------------------------------- /src/components/screens/404/404.parameters.ts: -------------------------------------------------------------------------------- 1 | import { routes } from 'parameters' 2 | import { I404Page } from './404.models' 3 | 4 | // Page404 5 | export const page404: I404Page = { 6 | title: 'Whoops...
Seems like you’re off the track.', 7 | text: " The specific page you're trying to access can't be reached. Don’t sweat it, we’ve got your back!", 8 | button: { 9 | text: 'Back to Homepage', 10 | link: routes.home 11 | }, 12 | seoTags: { 13 | description: 'These are not the droids you are looking for.', 14 | title: '404 Title', 15 | image: `${process.env.HOST}/images/meta/404.png`, 16 | url: `${process.env.HOST}/${routes[404]}`, 17 | keywords: 'keywords, missing, here' 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /src/services/blog.ts: -------------------------------------------------------------------------------- 1 | import { toast } from 'react-toastify'; 2 | import { localAxios } from 'services'; 3 | 4 | export async function getBlog() { 5 | const res = await localAxios 6 | .get(`/api/rss-blogs`) 7 | .then(response => { 8 | if (response.status === 200) { 9 | const { data } = response; 10 | return data; 11 | } else { 12 | throw new Error('Blog fetch failed.'); 13 | } 14 | }) 15 | .catch(err => { 16 | console.error('error', err.message); 17 | toast.error(err.message, { 18 | position: 'top-right', 19 | autoClose: 5000, 20 | hideProgressBar: true, 21 | closeOnClick: true, 22 | pauseOnHover: true, 23 | draggable: false, 24 | progress: undefined 25 | }); 26 | return []; 27 | }); 28 | return res; 29 | } 30 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "noUnusedLocals": true, 12 | "strict": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "noEmit": true, 15 | "esModuleInterop": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "jsx": "preserve", 21 | "baseUrl": "./src", 22 | "incremental": true, 23 | "paths": { 24 | "public/*": ["../public/*"] 25 | } 26 | }, 27 | "include": [ 28 | "next-env.d.ts", 29 | "**/*.ts", 30 | "**/*.tsx" 31 | ], 32 | "exclude": [ 33 | "node_modules" 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { GetServerSideProps, NextPage } from 'next'; 3 | import useWindowSize from 'hooks/useWindowSize'; 4 | import { Layout } from 'components/shared'; 5 | import { getHome, Welcome } from 'components/screens/index'; 6 | import { defaultMetaTags } from 'parameters'; 7 | 8 | 9 | interface ServerSideProps {} 10 | 11 | const IndexPage: NextPage = (props: ServerSideProps) => { 12 | const screenSize = useWindowSize(); 13 | const home = getHome({ screenSize }); 14 | 15 | return ( 16 | 17 | 18 | 19 | ); 20 | }; 21 | 22 | export default IndexPage; 23 | 24 | export const getServerSideProps: GetServerSideProps = async () => { 25 | return { 26 | props: { 27 | 28 | } 29 | }; 30 | }; 31 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### Resolves 2 | 3 | _What Github issue does this resolve (please include link)?_ 4 | 5 | - Resolves # 6 | 7 | ### Proposed Changes 8 | 9 | _Describe what this Pull Request does_ 10 | 11 | ### Reason for Changes 12 | 13 | _Explain why these changes should be made_ 14 | 15 | ### Test Coverage 16 | 17 | _Please show how you have added tests to cover your changes_ 18 | 19 | ### Browser Coverage 20 | Check the OS/browser combinations tested (At least 2) 21 | 22 | Mac 23 | * [ ] Chrome 24 | * [ ] Firefox 25 | * [ ] Edge 26 | * [ ] Opera 27 | * [ ] Brave 28 | 29 | Windows 30 | * [ ] Chrome 31 | * [ ] Firefox 32 | * [ ] Edge 33 | * [ ] Opera 34 | * [ ] Brave 35 | 36 | iOS 37 | * [ ] Safari 38 | * [ ] Chrome 39 | 40 | Android 41 | * [ ] Firefox 42 | * [ ] Chrome 43 | * [ ] Opera 44 | * [ ] Samsung Internet Browser 45 | * [ ] Mi Browser 46 | -------------------------------------------------------------------------------- /src/models/blog.ts: -------------------------------------------------------------------------------- 1 | export interface IBlogPost { 2 | /** Title */ 3 | title: string 4 | 5 | /** Slug */ 6 | slug: string 7 | 8 | /** Featured */ 9 | isFeatured: boolean 10 | 11 | /** Highlighted */ 12 | highlighted?: boolean | undefined 13 | 14 | /** Topic */ 15 | topic: 'Design' | 'Development' | 'Quality Assurance' | 'Marketing' | 'Human Resources' 16 | 17 | /** Author */ 18 | author: string 19 | 20 | /** Author Email */ 21 | authorEmail: string 22 | 23 | /** Description */ 24 | description: string 25 | 26 | /** Image */ 27 | image: string 28 | 29 | /** Date Written */ 30 | dateWritten: string 31 | 32 | /** Content */ 33 | content: Document 34 | 35 | /** metaTitle */ 36 | metaTitle: string 37 | 38 | /** metaDescription */ 39 | metaDescription: string 40 | 41 | /** metaKeywords */ 42 | metaKeywords: string[] 43 | 44 | /** metaImage */ 45 | metaImage: string 46 | } -------------------------------------------------------------------------------- /src/components/shared/typography/navigation/Navigation.tsx: -------------------------------------------------------------------------------- 1 | import React, { ComponentType, PropsWithChildren } from 'react'; 2 | import { StyledNavItem, StyledSubNavItem, StyledFooterNavItem, StyleProps } from './Navigation.styled'; 3 | import Link from 'next/link'; 4 | 5 | interface Props { 6 | text: string; 7 | color?: string; 8 | href: string; 9 | isActive: boolean; 10 | } 11 | 12 | const handleNavigationWrapping = (Component: ComponentType>, props: Props) => { 13 | const { text, href } = props; 14 | return ( 15 | 16 | 17 | {text} 18 | 19 | 20 | ); 21 | }; 22 | 23 | export const NavItem = (props: Props) => handleNavigationWrapping(StyledNavItem, props); 24 | export const SubNavItem = (props: Props) => handleNavigationWrapping(StyledSubNavItem, props); 25 | export const FooterNavItem = (props: Props) => handleNavigationWrapping(StyledFooterNavItem, props); 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /src/parameters/general.ts: -------------------------------------------------------------------------------- 1 | export const colors = { 2 | // TODO: remove and add colors from your design 3 | background1: '#FFFFFF', 4 | background2: '#F5F3F1', 5 | white: '#FFFFFF', 6 | body: '#313A43', 7 | primaryBlue1: '#29424B', 8 | primaryBlue2: '#163553', 9 | primaryBlue3: '#011E3E', 10 | primaryBlue4: '#203F5E', 11 | primaryGeeen1: '#21CE99', 12 | primaryGeeen2: '#79B3C0', 13 | primaryGeeen3: '#167E8A' 14 | }; 15 | 16 | export const sizes = { 17 | mobileS: 320, 18 | mobileM: 375, 19 | mobileL: 425, 20 | tablet: 768, 21 | laptop: 1024, 22 | laptopL: 1440, 23 | desktop: 2560 24 | }; 25 | 26 | export const devices = { 27 | mobileS: `(min-width: ${sizes.mobileS}px)`, 28 | mobileM: `(min-width: ${sizes.mobileM}px)`, 29 | mobileL: `(min-width: ${sizes.mobileL}px)`, 30 | tablet: `(min-width: ${sizes.tablet}px)`, 31 | laptop: `(min-width: ${sizes.laptop}px)`, 32 | laptopL: `(min-width: ${sizes.laptopL}px)`, 33 | desktop: `(min-width: ${sizes.desktop}px)` 34 | }; 35 | 36 | export const routes = { 37 | home: '/', 38 | 404: '404', 39 | 500: '500' 40 | }; 41 | -------------------------------------------------------------------------------- /src/pages/500.tsx: -------------------------------------------------------------------------------- 1 | import { NextPage } from 'next' 2 | import styled from '@emotion/styled' 3 | import { Layout, LayoutContainer, Heading2, Paragraph } from 'components/shared' 4 | import { Button } from 'components/shared/button' 5 | 6 | const Custom500: NextPage = () => { 7 | return ( 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 |
17 |
18 |
19 | ) 20 | } 21 | 22 | const Section = styled.section` 23 | text-align: center; 24 | margin: 58px auto 0; 25 | display: flex; 26 | flex-direction: column; 27 | align-items: center; 28 | & > * { 29 | margin-bottom: 32px; 30 | max-width: 448px; 31 | } 32 | a { 33 | margin-bottom: 0; 34 | } 35 | ` 36 | const HeadingWrapper = styled.div` 37 | max-width: 545px; 38 | ` 39 | 40 | export default Custom500 41 | -------------------------------------------------------------------------------- /src/pages/404.tsx: -------------------------------------------------------------------------------- 1 | import { NextPage } from 'next' 2 | import styled from '@emotion/styled' 3 | import { Layout, LayoutContainer, Heading2, Paragraph } from 'components/shared' 4 | import { page404 } from 'components/screens/404' 5 | import { Button } from 'components/shared/button' 6 | 7 | const Custom404: NextPage = () => { 8 | return ( 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 |
18 |
19 |
20 | ) 21 | } 22 | 23 | const Section = styled.section` 24 | text-align: center; 25 | margin: 58px auto 0; 26 | display: flex; 27 | flex-direction: column; 28 | align-items: center; 29 | & > * { 30 | margin-bottom: 32px; 31 | max-width: 448px; 32 | } 33 | a { 34 | margin-bottom: 0; 35 | } 36 | ` 37 | const HeadingWrapper = styled.div` 38 | max-width: 545px; 39 | ` 40 | 41 | export default Custom404 42 | -------------------------------------------------------------------------------- /src/services/email.ts: -------------------------------------------------------------------------------- 1 | 2 | import { ContactFormFinalData } from 'models'; 3 | import { toast } from 'react-toastify'; 4 | 5 | export async function postEmail(data: ContactFormFinalData, files: File[]) { 6 | const upload = JSON.stringify(data); 7 | 8 | const formData = new FormData(); 9 | 10 | for (let index = 0; index < files.length; index++) { 11 | formData.append('files', files[index]); 12 | } 13 | formData.append('data', upload); 14 | 15 | const res = await fetch(`/api/email`, { 16 | method: 'POST', 17 | body: formData 18 | // headers: { 19 | // 'Content-Type': 'multipart/form-data' 20 | // } 21 | }) 22 | .then(response => { 23 | if (response.status === 200) { 24 | return true; 25 | } else { 26 | return false; 27 | } 28 | }) 29 | .catch(err => { 30 | console.error('error', err.message); 31 | toast.error(err.message, { 32 | position: 'top-right', 33 | autoClose: 5000, 34 | hideProgressBar: true, 35 | closeOnClick: true, 36 | pauseOnHover: true, 37 | draggable: false, 38 | progress: undefined 39 | }); 40 | return false; 41 | }); 42 | return res; 43 | } 44 | -------------------------------------------------------------------------------- /src/components/shared/typography/caption/Captions.tsx: -------------------------------------------------------------------------------- 1 | import React, { ComponentType, DetailedHTMLProps, HTMLAttributes } from 'react'; 2 | import { CaptionSize } from './Captions.parameters'; 3 | import { StyledCaption, StyledCaptionProps } from './Captions.styled'; 4 | 5 | interface HandleCaptionWrappingProps { 6 | text: string; 7 | color?: string; 8 | size?: CaptionSize; 9 | bold?: boolean; 10 | isDangerouslySet?: boolean; 11 | position?: string; 12 | } 13 | 14 | type PropsHeading = StyledCaptionProps & DetailedHTMLProps, HTMLParagraphElement>; 15 | 16 | const handleCaptionWrapping = ( 17 | Component: ComponentType, 18 | { text, color, size, bold, isDangerouslySet, ...props }: HandleCaptionWrappingProps 19 | ) => { 20 | return isDangerouslySet ? ( 21 | 22 | ) : ( 23 | 24 | {text} 25 | 26 | ); 27 | }; 28 | 29 | export const Caption = (props: HandleCaptionWrappingProps) => handleCaptionWrapping(StyledCaption, props); 30 | -------------------------------------------------------------------------------- /src/services/mailchimp.ts: -------------------------------------------------------------------------------- 1 | import { toast } from 'react-toastify'; 2 | 3 | export async function postSubscribe(email: string) { 4 | const res = await fetch(`/api/subscribe`, { 5 | method: 'POST', 6 | body: email 7 | }) 8 | .then((response: any) => { 9 | return response.json(); 10 | }) 11 | .then(res2 => { 12 | if (!!res2.id) { 13 | return true; 14 | } 15 | if (res2.status === 400 && res2.title === 'Member Exists') { 16 | toast.error('User exists', { 17 | position: 'top-right', 18 | autoClose: 5000, 19 | hideProgressBar: true, 20 | closeOnClick: true, 21 | pauseOnHover: true, 22 | draggable: false, 23 | progress: undefined 24 | }); 25 | return false; 26 | } else { 27 | throw new Error(res2.title); 28 | } 29 | }) 30 | .catch(err => { 31 | console.error('postSubscribe error', err); 32 | toast.error(err.message, { 33 | position: 'top-right', 34 | autoClose: 5000, 35 | hideProgressBar: true, 36 | closeOnClick: true, 37 | pauseOnHover: true, 38 | draggable: false, 39 | progress: undefined 40 | }); 41 | throw new Error(err.message); 42 | }); 43 | 44 | return res; 45 | } 46 | -------------------------------------------------------------------------------- /src/components/shared/layout/head/CustomHead.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Head from 'next/head' 3 | import { MetaTagsModel } from 'models' 4 | 5 | 6 | 7 | interface Props { 8 | metaTags: MetaTagsModel 9 | } 10 | 11 | export const CustomHead = ({ metaTags }: Props) => { 12 | const { title, description, url, image, type } = metaTags 13 | 14 | return ( 15 | 16 | {title} 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | ) 34 | } -------------------------------------------------------------------------------- /src/components/shared/typography/caption/Captions.styled.tsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import { CaptionSize, FontSizesCaptions, LineHeightsCaptions } from './Captions.parameters'; 3 | 4 | 5 | export interface StyledCaptionProps { 6 | textColor?: string; 7 | size?: CaptionSize; 8 | bold?: boolean; 9 | position?: string; 10 | } 11 | 12 | const getFontSize = (size: CaptionSize | undefined): string => { 13 | switch (size) { 14 | case 's': 15 | return FontSizesCaptions.S; 16 | case 'm': 17 | return FontSizesCaptions.M; 18 | default: 19 | return '12px'; 20 | } 21 | }; 22 | 23 | const getLineHeight = (size: CaptionSize | undefined): string => { 24 | switch (size) { 25 | case 's': 26 | return LineHeightsCaptions.S; 27 | case 'm': 28 | return LineHeightsCaptions.M; 29 | default: 30 | return '1.3'; 31 | } 32 | }; 33 | 34 | 35 | export const StyledCaption = styled.p` 36 | color: ${props => props.textColor && props.textColor}; 37 | font-size: ${props => getFontSize(props.size)}; 38 | line-height: ${props => getLineHeight(props.size)}; 39 | position: ${props => props.position && props.position}; 40 | font-family: 'Epilogue', sans-serif; 41 | font-weight: 700; 42 | `; 43 | -------------------------------------------------------------------------------- /src/components/shared/typography/heading/Headings.tsx: -------------------------------------------------------------------------------- 1 | import React, { ComponentType, DetailedHTMLProps, HTMLAttributes } from 'react'; 2 | import { StyledH1, StyledH2, StyledH3, StyledH4, StyledH5, StyledHeadingProps, StyledH6 } from './Headings.styled'; 3 | 4 | interface HeadingProps { 5 | text: string; 6 | color?: string; 7 | isDangerouslySet?: boolean; 8 | position?: string; 9 | } 10 | 11 | type PropsHeading = StyledHeadingProps & DetailedHTMLProps, HTMLHeadingElement>; 12 | 13 | const handleHeadingWrapping = (Component: ComponentType, { text, color, isDangerouslySet, ...props }: HeadingProps) => { 14 | return isDangerouslySet ? ( 15 | 16 | ) : ( 17 | 18 | {text} 19 | 20 | ); 21 | }; 22 | 23 | export const Heading1 = (props: HeadingProps) => handleHeadingWrapping(StyledH1, props); 24 | export const Heading2 = (props: HeadingProps) => handleHeadingWrapping(StyledH2, props); 25 | export const Heading3 = (props: HeadingProps) => handleHeadingWrapping(StyledH3, props); 26 | export const Heading4 = (props: HeadingProps) => handleHeadingWrapping(StyledH4, props); 27 | export const Heading5 = (props: HeadingProps) => handleHeadingWrapping(StyledH5, props); 28 | export const Heading6 = (props: HeadingProps) => handleHeadingWrapping(StyledH6, props); 29 | -------------------------------------------------------------------------------- /src/components/shared/button/Button.parameters.tsx: -------------------------------------------------------------------------------- 1 | import { colors } from "parameters"; 2 | import { TemplateButtonColors } from "./Button.model"; 3 | 4 | export const ButtonVariants: TemplateButtonColors = { 5 | primary: { 6 | background: { 7 | normal: colors.primaryGeeen1, 8 | hover: colors.primaryGeeen1, 9 | disabled: colors.primaryGeeen1 10 | }, 11 | outline: { 12 | normal: 'transparent', 13 | hover: 'transparent', 14 | disabled: 'transparent' 15 | }, 16 | text: { 17 | normal: colors.primaryBlue3, 18 | hover: colors.primaryBlue3, 19 | disabled: colors.primaryBlue3 20 | } 21 | }, 22 | secondary: { 23 | background: { 24 | normal: colors.white, 25 | hover: colors.primaryGeeen1, 26 | disabled: colors.primaryGeeen1 27 | }, 28 | outline: { 29 | normal: colors.primaryGeeen1, 30 | hover: colors.primaryGeeen1, 31 | disabled: 'transparent' 32 | }, 33 | text: { 34 | normal: colors.primaryBlue1, 35 | hover: colors.primaryBlue1, 36 | disabled: colors.primaryBlue3 37 | } 38 | }, 39 | tertiary: { 40 | background: { 41 | normal: colors.white, 42 | hover: colors.white, 43 | disabled: colors.white 44 | }, 45 | outline: { 46 | normal: 'transparent', 47 | hover: 'transparent', 48 | disabled: 'transparent' 49 | }, 50 | text: { 51 | normal: colors.primaryBlue1, 52 | hover: colors.primaryBlue1, 53 | disabled: colors.primaryBlue3 54 | } 55 | } 56 | }; 57 | -------------------------------------------------------------------------------- /src/components/shared/typography/paragraphs/Paragraphs.tsx: -------------------------------------------------------------------------------- 1 | import React, { ComponentType, DetailedHTMLProps, HTMLAttributes } from 'react'; 2 | import { StyledTag, StyledParagraph, StyledSubHeadline, StyledParagraphProps } from './Paragraphs.styled'; 3 | import { ParagraphsSize } from './Paragraphs.parameters'; 4 | 5 | interface HandleParagraphWrappingProps { 6 | text: string; 7 | color?: string; 8 | size?: ParagraphsSize; 9 | bold?: boolean; 10 | isDangerouslySet?: boolean; 11 | position?: string; 12 | } 13 | 14 | type PropsHeading = StyledParagraphProps & DetailedHTMLProps, HTMLParagraphElement>; 15 | 16 | const handleParagraphWrapping = ( 17 | Component: ComponentType, 18 | { text, color, size, bold, isDangerouslySet, ...props }: HandleParagraphWrappingProps 19 | ) => { 20 | return isDangerouslySet ? ( 21 | 22 | ) : ( 23 | 24 | {text} 25 | 26 | ); 27 | }; 28 | 29 | 30 | export const Paragraph = (props: HandleParagraphWrappingProps) => handleParagraphWrapping(StyledParagraph, props); 31 | 32 | /// 33 | export const Tag = (props: HandleParagraphWrappingProps) => handleParagraphWrapping(StyledTag, props); 34 | export const SubHeadline = (props: HandleParagraphWrappingProps) => handleParagraphWrapping(StyledSubHeadline, props); 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-template", 3 | "version": "0.0.0", 4 | "private": true, 5 | "dependencies": { 6 | "@emotion/react": "11.9.3", 7 | "@emotion/styled": "11.9.3", 8 | "axios": "0.27.2", 9 | "dotenv": "16.0.1", 10 | "emotion-normalize": "11.0.1", 11 | "emotion-reset": "3.0.1", 12 | "formidable": "2.0.1", 13 | "next": "12.1.6", 14 | "nodemailer": "6.7.5", 15 | "react": "18.2.0", 16 | "react-dom": "18.2.0", 17 | "react-toastify": "9.0.5" 18 | }, 19 | "devDependencies": { 20 | "@emotion/babel-plugin": "11.9.2", 21 | "@next/bundle-analyzer": "12.1.6", 22 | "@types/formidable": "^2.0.5", 23 | "@types/node": "18.0.0", 24 | "@types/nodemailer": "^6.4.4", 25 | "@types/react": "18.0.14", 26 | "@typescript-eslint/eslint-plugin": "5.29.0", 27 | "@typescript-eslint/parser": "5.29.0", 28 | "cross-env": "7.0.3", 29 | "eslint-config-prettier": "8.5.0", 30 | "eslint-plugin-jsx-a11y": "6.5.1", 31 | "eslint-plugin-prettier": "4.0.0", 32 | "eslint-plugin-react": "7.30.1", 33 | "eslint-plugin-react-hooks": "4.6.0", 34 | "husky": "8.0.1", 35 | "lint-staged": "13.0.2", 36 | "prettier": "2.7.1", 37 | "typescript": "4.7.4" 38 | }, 39 | "husky": { 40 | "hooks": { 41 | "pre-commit": "lint-staged" 42 | } 43 | }, 44 | "lint-staged": { 45 | "*": "prettier --write" 46 | }, 47 | "scripts": { 48 | "dev": "next dev", 49 | "build": "next build", 50 | "start": "next start -p 3400", 51 | "analyze": "cross-env ANALYZE=true next build", 52 | "analyze:server": "cross-env BUNDLE_ANALYZE=server next build", 53 | "analyze:browser": "cross-env BUNDLE_ANALYZE=browser next build", 54 | "lint": "prettier --check .", 55 | "format": "prettier --write ." 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import Document, { Html, Head, Main, NextScript, DocumentContext } from 'next/document'; 2 | 3 | class MyDocument extends Document { 4 | static async getInitialProps(ctx: DocumentContext) { 5 | const initialProps = await Document.getInitialProps(ctx); 6 | return { ...initialProps }; 7 | } 8 | 9 | render() { 10 | return ( 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | {/* Global Site Tag (gtag.js) - Google Analytics */} 20 | 35 | 36 | 37 | 42 |
43 | 44 | 45 | 46 | ); 47 | } 48 | } 49 | 50 | export default MyDocument; 51 | -------------------------------------------------------------------------------- /src/pages/rss.tsx: -------------------------------------------------------------------------------- 1 | import { IBlogPost } from 'models'; 2 | import { NextApiResponse } from 'next'; 3 | import React from 'react'; 4 | import { getBlog } from 'services'; 5 | 6 | interface Response { 7 | res: NextApiResponse; 8 | } 9 | 10 | const feed = () => <>; 11 | 12 | export default feed; 13 | 14 | export const getServerSideProps = async ({ res }: Response) => { 15 | const blogs: IBlogPost[] = await getBlog(); 16 | 17 | const title = 'Template Title'; 18 | const desc = 'The one-stop shop to design, develop and deploy your next digital project.'; 19 | 20 | // encode &, <, >, " and ' 21 | const encodeXML = (data: string) => 22 | data.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, '''); 23 | 24 | const items = blogs.map( 25 | (item: IBlogPost) => ` 26 | 27 | ${encodeXML(item.title)} 28 | ${encodeXML(`${item.author} <${item.authorEmail}>`)} 29 | ${process.env.APP_BASE_URL}/blog/post/${item.slug} 30 | ${new Date(item.dateWritten).toUTCString()} 31 | ${process.env.APP_BASE_URL}/blog/post/${item.slug} 32 | 33 | ` 34 | ); 35 | 36 | const xml: string = ` 37 | 38 | 39 | 40 | ${title} 41 | ${process.env.APP_BASE_URL}/rss 42 | ${desc} 43 | ${items.join('')} 44 | 45 | `; 46 | 47 | res.setHeader('Content-Type', 'text/xml'); 48 | res.write(xml); 49 | res.end(); 50 | 51 | return { 52 | props: {} 53 | }; 54 | }; 55 | -------------------------------------------------------------------------------- /src/components/shared/typography/paragraphs/Paragraphs.styled.tsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import { devices } from 'parameters'; 3 | import { ParagraphsSize, FontSizesParagraphs, LineHeightsParagraphs } from './Paragraphs.parameters'; 4 | 5 | export interface StyledParagraphProps { 6 | textColor?: string; 7 | size?: ParagraphsSize; 8 | bold?: boolean; 9 | position?: string; 10 | } 11 | 12 | const getFontSize = (size: ParagraphsSize | undefined): string => { 13 | switch (size) { 14 | case 's': 15 | return FontSizesParagraphs.S; 16 | case 'm': 17 | return FontSizesParagraphs.M; 18 | default: 19 | return '17px'; 20 | } 21 | }; 22 | 23 | const getLineHeight = (size: ParagraphsSize | undefined): string => { 24 | switch (size) { 25 | case 's': 26 | return LineHeightsParagraphs.S; 27 | case 'm': 28 | return LineHeightsParagraphs.M; 29 | default: 30 | return '1.7'; 31 | } 32 | }; 33 | 34 | 35 | export const StyledParagraph = styled.p` 36 | color: ${props => props.textColor && props.textColor}; 37 | font-size: ${props => getFontSize(props.size)}; 38 | line-height: ${props => getLineHeight(props.size)}; 39 | position: ${props => props.position && props.position}; 40 | font-family: 'Lato', sans-serif; 41 | `; 42 | 43 | //// 44 | 45 | export const StyledTag = styled.p` 46 | color: ${props => props.textColor && props.textColor}; 47 | font-weight: 800; 48 | font-size: 12px; 49 | line-height: 12px; 50 | text-transform: uppercase; 51 | font-family: 'Lato', sans-serif; 52 | `; 53 | 54 | 55 | export const StyledSubHeadline = styled.p` 56 | color: ${props => props.textColor && props.textColor}; 57 | font-weight: 600; 58 | font-size: 18px; 59 | line-height: 30px; 60 | 61 | @media ${devices.laptop} { 62 | font-size: 20px; 63 | line-height: 32px; 64 | } 65 | `; 66 | -------------------------------------------------------------------------------- /src/style/styled.tsx: -------------------------------------------------------------------------------- 1 | import { css, Global } from '@emotion/react'; 2 | import emotionReset from 'emotion-reset'; 3 | import emotionNormalize from 'emotion-normalize'; 4 | import { colors } from 'parameters'; 5 | 6 | export const globalStyles = ( 7 | <> 8 | only. Don't be confused */ 14 | ${emotionNormalize} 15 | 16 | 17 | html, 18 | body { 19 | color: ${colors.body}; 20 | font-family: 'Lato', sans-serif; 21 | } 22 | 23 | h1 { 24 | font-family: 'Epilogue', sans-serif; 25 | font-style: normal; 26 | font-size: 64px; 27 | line-height: 1.1; 28 | font-weight: 700; 29 | } 30 | 31 | h2 { 32 | font-family: 'Epilogue', sans-serif; 33 | font-style: normal; 34 | font-weight: 800; 35 | font-size: 32px; 36 | line-height: 1.1; 37 | font-weight: 700; 38 | } 39 | 40 | h3 { 41 | font-family: 'Epilogue', sans-serif; 42 | font-size: 27px; 43 | line-height: 1.25; 44 | font-weight: 700; 45 | } 46 | 47 | h4 { 48 | font-family: 'Epilogue', sans-serif; 49 | font-style: normal; 50 | font-size: 24px; 51 | line-height: 1.3; 52 | font-weight: 700; 53 | } 54 | 55 | h5 { 56 | font-family: 'Epilogue', sans-serif; 57 | font-style: normal; 58 | font-size: 20px; 59 | line-height: 1.3; 60 | font-weight: 700; 61 | } 62 | 63 | h6 { 64 | font-family: 'Epilogue', sans-serif; 65 | font-style: normal; 66 | font-size: 18px; 67 | line-height: 1.3; 68 | font-weight: 600; 69 | } 70 | 71 | a { 72 | color: ${colors.body}; 73 | } 74 | `} 75 | /> 76 | 77 | ); 78 | -------------------------------------------------------------------------------- /folder.structure.md: -------------------------------------------------------------------------------- 1 | .env.example 2 | .prettierrc 3 | .babelrc 4 | next.config.js 5 | tsconfig.json 6 | package.json 7 | src 8 | |--components 9 | | |--screens 10 | | | |--api 11 | | | | |--index.ts 12 | | | | |--api.models.ts 13 | | | |--index 14 | | | |--index.ts 15 | | | |--index.models.ts 16 | | | |--index.parameters.ts 17 | | | |--subcomponents 18 | | | |--welcome 19 | | | |--index.ts 20 | | | |--Welcome.ts 21 | | | |--Welcome.styled.ts 22 | | |--shared 23 | | |--index.ts 24 | | |--layout 25 | | | |--head 26 | | | | |--CustomHead.tsx 27 | | | |--layout 28 | | | | |--Layout.tsx 29 | | | |--layoutContainer 30 | | | |--LayoutContainer.tsx 31 | | | |--LayoutContainer.styled.tsx 32 | | |--typography 33 | | |--caption 34 | | | |--Caption.tsx 35 | | | |--Caption.parameters.tsx 36 | | | |--Caption.styled.tsx 37 | | |--heading 38 | | | |--Heading.tsx 39 | | | |--Heading.styled.tsx 40 | | |--navigation 41 | | | |--Navigation.tsx 42 | | | |--Navigation.styled.tsx 43 | | |--paragraphs 44 | | |--Paragraphs.tsx 45 | | |--Paragraphs.parameters.tsx 46 | | |--Paragraphs.styled.tsx 47 | |--hooks 48 | | |--useWindowSize.tsx 49 | |--pages 50 | | |--api 51 | | | |--email.ts 52 | | | |--rss-blogs.ts 53 | | | |--subscribe.ts 54 | | | |--unsubscribe.ts 55 | | |--_app.tsx 56 | | |--_document.tsx 57 | | |--index.tsx 58 | | |--rss.tsx 59 | |--parameters 60 | | |--index.ts 61 | | |--constants.ts 62 | | |--general.tsx 63 | | |--page.tsx 64 | |--services 65 | | |--index.ts 66 | | |--axios.ts 67 | | |--blog.ts 68 | | |--email.ts 69 | | |--ga.ts 70 | | |--mailchimp.ts 71 | |--style 72 | | |--index.ts 73 | | |--styled.tsx 74 | |--utils 75 | |--index.ts 76 | |--dashToUnderscore.ts 77 | |--getBase64.ts 78 | |--underscoreToDash.ts -------------------------------------------------------------------------------- /eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | es6: true 6 | }, 7 | parserOptions: { ecmaVersion: 8 }, // to enable features such as async/await 8 | ignorePatterns: ['node_modules/*', '.next/*', '.out/*', '!.prettierrc.js'], // We don't want to lint generated files nor node_modules, but we want to lint .prettierrc.js (ignored by default by eslint) 9 | extends: ['eslint:recommended'], 10 | overrides: [ 11 | // This configuration will apply only to TypeScript files 12 | { 13 | files: ['**/*.ts', '**/*.tsx'], 14 | parser: '@typescript-eslint/parser', 15 | settings: { react: { version: 'detect' } }, 16 | env: { 17 | browser: true, 18 | node: true, 19 | es6: true 20 | }, 21 | extends: [ 22 | 'eslint:recommended', 23 | 'plugin:@typescript-eslint/recommended', // TypeScript rules 24 | 'plugin:react/recommended', // React rules 25 | 'plugin:react-hooks/recommended', // React hooks rules 26 | 'plugin:jsx-a11y/recommended', // Accessibility rules 27 | 'prettier/@typescript-eslint', // Prettier plugin 28 | 'plugin:prettier/recommended' // Prettier recommended rules 29 | ], 30 | rules: { 31 | // We will use TypeScript's types for component props instead 32 | 'react/prop-types': 'off', 33 | 34 | // No need to import React when using Next.js 35 | 'react/react-in-jsx-scope': 'off', 36 | 37 | // This rule is not compatible with Next.js's components 38 | 'jsx-a11y/anchor-is-valid': 'off', 39 | 40 | // Why would you want unused vars? 41 | '@typescript-eslint/no-unused-vars': ['error'], 42 | 43 | // I suggest this setting for requiring return types on functions only where useful 44 | '@typescript-eslint/explicit-function-return-type': [ 45 | 'warn', 46 | { 47 | allowExpressions: true, 48 | allowConciseArrowFunctionExpressionsStartingWithVoid: true 49 | } 50 | ], 51 | 'prettier/prettier': ['error', {}, { usePrettierrc: true }] // Includes .prettierrc.js rules 52 | } 53 | } 54 | ] 55 | }; 56 | -------------------------------------------------------------------------------- /src/services/ga.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | interface Window { 3 | gtag: any; 4 | dataLayer: any; 5 | } 6 | } 7 | 8 | interface GaEvent { 9 | eventId: string; 10 | info: { 11 | event_category?: string; 12 | event_label?: string; 13 | pagePath?: string; 14 | pageTitle?: string; 15 | send_to?: string; 16 | action?: string; 17 | }; 18 | } 19 | 20 | declare const gtag: any; 21 | 22 | // log specific events happening. 23 | export const triggerEventGTM = (event: GaEvent) => { 24 | if (gtag) { 25 | gtag('event', event.eventId, event.info); 26 | } 27 | }; 28 | 29 | // get specific source. 30 | export const getSource = (query: string) => { 31 | let gaCategory: string = ''; 32 | let gaSource: string = ''; 33 | let cookiesAccepted: boolean = false; 34 | 35 | const queries = query.split('&'); 36 | 37 | for (let i = 0; i < queries.length; i++) { 38 | const query = queries[i]; 39 | 40 | const split = query.split('='); 41 | if (split.length) { 42 | if (split[0] === 'utm_source') { 43 | gaSource = split[1].toUpperCase(); 44 | gaCategory = 'LEAD'; 45 | } else if (split[0] === 'source') { 46 | gaSource = split[1].toUpperCase(); 47 | gaCategory = 'LEAD'; 48 | } else if (split[0] === 'fbclid') { 49 | gaSource = 'FACEBOOK'; 50 | gaCategory = 'LEAD'; 51 | } else if (split[0] === 'upwork') { 52 | // legacy 53 | gaSource = 'UPWORK'; 54 | gaCategory = 'LEAD'; 55 | } else { 56 | // if (!cookieService.get('ga_source') || !localStorage.getItem('ga_source')) { 57 | // gaSource = 'SOURCE_UNKNOWN' 58 | // gaCategory = 'VISITOR' 59 | // } 60 | } 61 | if (!!cookiesAccepted && !!gaSource) { 62 | sessionStorage.setItem('ga_source', gaSource); 63 | localStorage.setItem('ga_source', gaSource); 64 | } 65 | if (!!cookiesAccepted && !!gaCategory) { 66 | sessionStorage.setItem('ga_category', gaCategory); 67 | localStorage.setItem('ga_category', gaCategory); 68 | } 69 | 70 | // this.cookieService.setWithExpiryInYears('ga_source', this.gaSource, 1) 71 | } 72 | } 73 | }; 74 | -------------------------------------------------------------------------------- /src/components/shared/typography/navigation/Navigation.styled.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | import styled from '@emotion/styled'; 3 | import { colors } from 'parameters'; 4 | 5 | export interface StyleProps { 6 | isActive: boolean; 7 | textColor?: string; 8 | } 9 | 10 | export const StyledNavItem = styled.a` 11 | cursor: pointer; 12 | color: ${props => (props.textColor ? props.textColor : colors.primaryBlue1)}; 13 | font-family: 'Epilogue', sans-serif; 14 | font-style: normal; 15 | font-weight: 500; 16 | font-size: 18px; 17 | line-height: 40px; 18 | text-decoration: none; 19 | text-align: center; 20 | transition: color 0.2s ease-in-out; 21 | 22 | &:hover { 23 | ::after { 24 | content: ''; 25 | position: absolute; 26 | background: ${colors.primaryGeeen1}; 27 | max-width: 100%; 28 | width: 100%; 29 | height: 8px; 30 | left: 0; 31 | bottom: calc(-5px - 3px); 32 | } 33 | } 34 | 35 | ${props => 36 | props.isActive && 37 | css` 38 | ::after { 39 | content: ''; 40 | position: absolute; 41 | background: ${colors.primaryGeeen1}; 42 | max-width: 100%; 43 | width: 35px; 44 | height: 8px; 45 | left: 0; 46 | bottom: calc(-5px - 3px); 47 | } 48 | `} 49 | `; 50 | 51 | /// 52 | export const StyledSubNavItem = styled.a` 53 | color: ${props => (props.textColor ? props.textColor : colors.primaryBlue1)}; 54 | font-family: 'Epilogue'; 55 | font-style: normal; 56 | font-weight: 500; 57 | font-size: 18px; 58 | line-height: 40px; 59 | text-align: center; 60 | 61 | &:hover { 62 | color: ${colors.primaryBlue1}; 63 | } 64 | 65 | ${props => 66 | props.isActive && 67 | css` 68 | color: ${colors.primaryBlue1}; 69 | ::before { 70 | content: ''; 71 | position: absolute; 72 | background: ${colors.primaryBlue1}; 73 | max-width: 100%; 74 | width: 35px; 75 | height: 8px; 76 | left: 0; 77 | bottom: calc(-5px - 3px); 78 | } 79 | `} 80 | `; 81 | 82 | export const StyledFooterNavItem = styled.a` 83 | color: ${props => (props.textColor ? props.textColor : colors.primaryBlue1)}; 84 | text-decoration: none; 85 | transition: color 0.2s ease-in; 86 | font-weight: 600; 87 | font-size: 16px; 88 | line-height: 20px; 89 | `; 90 | -------------------------------------------------------------------------------- /src/services/axios.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosRequestConfig } from "axios"; 2 | import { STATUS_CODE_UNAUTHORIZED, STATUS_CODE_BAD_REQUEST } from "parameters"; 3 | 4 | const apiUrl = process.env.API_BASE_URL; 5 | const appUrl = process.env.APP_BASE_URL; 6 | 7 | export const localAxios = axios.create({ 8 | baseURL: appUrl, 9 | }); 10 | 11 | 12 | export const remoteAxios = axios.create({ 13 | baseURL: apiUrl, 14 | }); 15 | 16 | remoteAxios.interceptors.request.use( 17 | (config: AxiosRequestConfig) => { 18 | // const token = localStorage.getItem(ACCESS_TOKEN_NAME); 19 | // if (token !== null) { 20 | // console.log("Axios interceptor token: " + token); 21 | // config!.headers!.authorization = `JWT ${token}`; 22 | // } 23 | return config; 24 | }, 25 | (error) => { 26 | console.log("Axios interceptor error "); 27 | return Promise.reject(error); 28 | } 29 | ); 30 | 31 | // Add a response interceptor 32 | remoteAxios.interceptors.response.use( 33 | function (response) { 34 | console.log("Axios interceptor response " + response); 35 | // Any status code that lie within the range of 2xx cause this function to trigger 36 | // Do something with response data 37 | return response; 38 | }, 39 | function (error) { 40 | const originalRequest = error.config; 41 | if ( 42 | error?.response?.status === STATUS_CODE_UNAUTHORIZED && 43 | !originalRequest._retry 44 | ) { 45 | originalRequest._retry = true; 46 | // try refresh with token 47 | // RefreshTokenJWT(); 48 | // redirect to login page 49 | // RemoveUserDataFromStorage(); 50 | // history.push("/"); 51 | } else if (error?.response?.status === STATUS_CODE_BAD_REQUEST) { 52 | // handle bad request 53 | // let responseData = error.response.data; 54 | // responseData.forEach(function(message: any) { 55 | // let messageItem = message; 56 | // }) 57 | // toast.error(error.message, { 58 | // position: 'top-right', 59 | // autoClose: 5000, 60 | // hideProgressBar: true, 61 | // closeOnClick: true, 62 | // pauseOnHover: true, 63 | // draggable: false, 64 | // progress: undefined 65 | // }); 66 | } 67 | return Promise.reject(error); 68 | } 69 | ); 70 | -------------------------------------------------------------------------------- /src/components/shared/typography/heading/Headings.styled.tsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import { devices } from 'parameters'; 3 | 4 | export interface StyledHeadingProps { 5 | textColor?: string; 6 | position?: string; 7 | } 8 | 9 | export const StyledH1 = styled.h1` 10 | color: ${props => props.textColor && props.textColor}; 11 | font-family: 'Epilogue', sans-serif; 12 | font-size: 64px; 13 | line-height: 1.1; 14 | font-weight: 700; 15 | word-break: break-word; /* edge case in hero section */ 16 | 17 | @media ${devices.mobileM} { 18 | font-size: 46px; 19 | } 20 | `; 21 | export const StyledH2 = styled.h2` 22 | color: ${props => props.textColor && props.textColor}; 23 | font-family: 'Epilogue', sans-serif; 24 | font-size: 32px; 25 | line-height: 1.1; 26 | font-weight: 700; 27 | 28 | @media ${devices.mobileM} { 29 | font-size: 48px; 30 | line-height: 52.8px; 31 | } 32 | `; 33 | export const StyledH3 = styled.h3` 34 | color: ${props => props.textColor && props.textColor}; 35 | font-family: 'Epilogue', sans-serif; 36 | font-size: 27px; 37 | line-height: 1.25; 38 | font-weight: 700; 39 | 40 | position: ${props => props.position && props.position}; 41 | 42 | @media ${devices.mobileM} { 43 | font-size: 24px; 44 | line-height: 30px; 45 | } 46 | `; 47 | export const StyledH4 = styled.h4` 48 | color: ${props => props.textColor && props.textColor}; 49 | font-family: 'Epilogue', sans-serif; 50 | font-size: 24px; 51 | line-height: 1.3; 52 | font-weight: 700; 53 | 54 | @media ${devices.mobileM} { 55 | font-size: 24px; 56 | line-height: 31px; 57 | } 58 | `; 59 | export const StyledH5 = styled.h5` 60 | color: ${props => props.textColor && props.textColor}; 61 | font-family: 'Epilogue', sans-serif; 62 | font-size: 20px; 63 | line-height: 1.3; 64 | font-weight: 700; 65 | 66 | @media ${devices.mobileM} { 67 | font-size: 20px; 68 | line-height: 26px; 69 | } 70 | `; 71 | export const StyledH6 = styled.h6` 72 | color: ${props => props.textColor && props.textColor}; 73 | font-family: 'Epilogue', sans-serif; 74 | font-size: 18px; 75 | line-height: 1.3; 76 | font-weight: 600; 77 | 78 | @media ${devices.mobileM} { 79 | font-size: 20px; 80 | line-height: 26px; 81 | } 82 | `; 83 | -------------------------------------------------------------------------------- /src/components/shared/button/Button.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, ComponentType, HTMLAttributeAnchorTarget } from 'react'; 2 | import Link from 'next/link'; 3 | import { Text, ButtonLink, ButtonStyledProps, StyledButton, WrapperIcon } from './Button.style'; 4 | import { UrlObject } from 'url'; 5 | import { ButtonSize, ButtonVariant } from './Button.model'; 6 | import Image from 'next/image'; 7 | 8 | interface CommonProps { 9 | text: string; 10 | type?: 'button' | 'submit' | 'reset'; 11 | size?: ButtonSize; 12 | variant?: ButtonVariant; 13 | icon?: any; // need to use any to avoid potential conflicts with svgr-plugin 14 | iconFirst?: boolean; 15 | color?: string; 16 | disabled?: boolean; 17 | fullWidth?: boolean; 18 | disableMargin?: boolean; 19 | disablePadding?: boolean; 20 | } 21 | 22 | export type Props = 23 | | ({ onClick: () => void } & { href?: never; target?: never } & CommonProps) 24 | | ({ onClick?: never } & { href: UrlObject | string; target: React.AnchorHTMLAttributes['target'] } & CommonProps); 25 | 26 | type ComponentProps = ButtonStyledProps & React.ButtonHTMLAttributes; 27 | 28 | const handleButtonWrapping = (Component: ComponentType, props: Props) => { 29 | const { 30 | href, 31 | size, 32 | text, 33 | target, 34 | onClick, 35 | type, 36 | variant, 37 | icon, 38 | color, 39 | disabled, 40 | fullWidth = false, 41 | disableMargin, 42 | iconFirst, 43 | disablePadding 44 | } = props; 45 | const [currentIcon] = useState(icon); 46 | 47 | const button = ( 48 | { 59 | onClick && onClick(); 60 | }}> 61 | {text} 62 | {icon ? ( 63 | 64 | 65 | 66 | ) : null} 67 | 68 | ); 69 | 70 | if (href) { 71 | return ( 72 | 73 | {button} 74 | 75 | ); 76 | } 77 | return button; 78 | }; 79 | 80 | export const Button = (props: Props) => handleButtonWrapping(StyledButton, props); 81 | -------------------------------------------------------------------------------- /src/pages/api/subscribe.js: -------------------------------------------------------------------------------- 1 | const nodemailer = require('nodemailer'); 2 | import axios from 'axios'; 3 | 4 | function SendEmail(email) { 5 | const responseMailOptions = { 6 | from: 'Company ', 7 | to: [email], 8 | subject: 'Company has received your message :)', 9 | // tslint:disable-next-line: max-line-length quotemark 10 | html: `

WRITE EMAIL RESPONSE HERE

11 |
`, 12 | attachments: [ 13 | { 14 | filename: 'illustration.png', 15 | path: 'http://localhost:3000/email_image.png', 16 | cid: 'email_image' 17 | } 18 | ] 19 | }; 20 | 21 | transporter 22 | .sendMail(responseMailOptions) 23 | .then(info => { 24 | res.status(200).send({ message: 'ok' }); 25 | return; 26 | }) 27 | .catch(err => { 28 | res.status(500).send({ err }); 29 | return; 30 | }); 31 | } 32 | 33 | export default async function handler(req, res) { 34 | const mailchimpInstance = process.env.MAILCHIMP_ISTANCE; 35 | const listUniqueId = process.env.MAILCHIMP_ID; 36 | const mailchimpApiKey = process.env.MAILCHIMP_API_KEY; 37 | 38 | const axiosConfig = { 39 | headers: { 40 | Authorization: 'Basic ' + Buffer.from('randomstring:' + mailchimpApiKey).toString('base64'), 41 | Accept: 'application/json', 42 | 'Content-Type': 'application/json' 43 | } 44 | }; 45 | 46 | const postData = { 47 | email_address: req.body, 48 | status: 'subscribed' 49 | }; 50 | 51 | console.log('postData', postData); 52 | 53 | try { 54 | const mcResponse = await axios.post( 55 | 'https://' + mailchimpInstance + '.api.mailchimp.com/3.0/lists/' + listUniqueId + '/members/', 56 | postData, 57 | axiosConfig 58 | ); 59 | console.log('Mailchimp List Response: ', mcResponse.data); 60 | res.status(200).json(mcResponse.data); 61 | } catch (err) { 62 | const errdata = err['response']['data']; 63 | console.log('Mailchimp Error: ', errdata); 64 | res.status(errdata.status).json(errdata); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/pages/api/unsubscribe.js: -------------------------------------------------------------------------------- 1 | const nodemailer = require('nodemailer'); 2 | import axios from 'axios'; 3 | 4 | function SendEmail(email) { 5 | const responseMailOptions = { 6 | from: 'Company ', 7 | to: [email], 8 | subject: 'Company has received your message :)', 9 | // tslint:disable-next-line: max-line-length quotemark 10 | html: `

WRITE EMAIL RESPONSE HERE

11 |
`, 12 | attachments: [ 13 | { 14 | filename: 'illustration.png', 15 | path: 'http://localhost:3000/email_image.png', 16 | cid: 'email_image' 17 | } 18 | ] 19 | }; 20 | 21 | transporter 22 | .sendMail(responseMailOptions) 23 | .then(info => { 24 | res.status(200).send({ message: 'ok' }); 25 | return; 26 | }) 27 | .catch(err => { 28 | res.status(500).send({ err }); 29 | return; 30 | }); 31 | } 32 | 33 | export default async function handler(req, res) { 34 | const mailchimpInstance = process.env.MAILCHIMP_ISTANCE; 35 | const listUniqueId = process.env.MAILCHIMP_ID; 36 | const mailchimpApiKey = process.env.MAILCHIMP_API_KEY; 37 | 38 | const axiosConfig = { 39 | headers: { 40 | Authorization: 'Basic ' + Buffer.from('randomstring:' + mailchimpApiKey).toString('base64'), 41 | Accept: 'application/json', 42 | 'Content-Type': 'application/json' 43 | } 44 | }; 45 | 46 | const postData = { 47 | email_address: req.body, 48 | status: 'subscribed' 49 | }; 50 | 51 | console.log('postData', postData); 52 | 53 | try { 54 | const mcResponse = await axios.post( 55 | 'https://' + mailchimpInstance + '.api.mailchimp.com/3.0/lists/' + listUniqueId + '/members/', 56 | postData, 57 | axiosConfig 58 | ); 59 | console.log('Mailchimp List Response: ', mcResponse.data); 60 | res.status(200).json(mcResponse.data); 61 | } catch (err) { 62 | const errdata = err['response']['data']; 63 | console.log('Mailchimp Error: ', errdata); 64 | res.status(errdata.status).json(errdata); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/components/shared/button/Button.style.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react' 2 | import styled from '@emotion/styled' 3 | import { colors, devices } from 'parameters' 4 | import { ButtonSize, ButtonVariant } from './Button.model' 5 | import { ButtonVariants } from './Button.parameters' 6 | 7 | 8 | export interface ButtonStyledProps { 9 | size?: ButtonSize 10 | variant?: ButtonVariant 11 | disabled?: boolean 12 | fullWidth?: boolean 13 | disableMargin?: boolean 14 | disablePadding?: boolean 15 | iconFirst?: boolean 16 | } 17 | 18 | const getButtonTheme = (variant: ButtonVariant | undefined) => { 19 | switch (variant) { 20 | case 'primary': 21 | return ButtonVariants.primary 22 | case 'secondary': 23 | return ButtonVariants.secondary 24 | case 'tertiary': 25 | return ButtonVariants.tertiary 26 | default: 27 | break 28 | } 29 | } 30 | 31 | export const ButtonLink = styled.a` 32 | text-decoration: none; 33 | max-width: fit-content; 34 | ` 35 | 36 | const StyledBaseButton = styled.button` 37 | display: flex; 38 | justify-content: center; 39 | align-items: center; 40 | cursor: pointer; 41 | outline-color: ${colors.primaryGeeen1}; 42 | border: none; 43 | font-size: 1.3rem; 44 | line-height: 1.3; 45 | font-style: normal; 46 | font-weight: 700; 47 | border-radius: 0px; 48 | transition: all 0.2s ease-in; 49 | outline-style: solid; 50 | outline-width: 1px; 51 | padding-left: 30px; 52 | padding-right: 30px; 53 | padding-top: 16px; 54 | padding-bottom: 16px; 55 | ${props => 56 | props.iconFirst && 57 | css` 58 | flex-direction: row-reverse; 59 | gap: 1.2rem; 60 | ${WrapperIcon} { 61 | left: 0; 62 | } 63 | `} 64 | 65 | ${props => 66 | props.disabled && 67 | css` 68 | pointer-events: none; 69 | `} 70 | ${props => 71 | props.size === 'small' && 72 | css` 73 | padding-left: 14px; 74 | padding-right: 14px; 75 | padding-top: 12px; 76 | padding-bottom: 12px; 77 | `} 78 | 79 | ${props => props.size === 'big' && css``} 80 | 81 | ${props => 82 | props.disablePadding && 83 | css` 84 | padding: 0; 85 | `} 86 | 87 | ${props => 88 | css` 89 | color: ${props.disabled ? getButtonTheme(props.variant)?.text.disabled : getButtonTheme(props.variant)?.text.normal}; 90 | background-color: ${props.disabled ? getButtonTheme(props.variant)?.background.disabled : getButtonTheme(props.variant)?.background.normal}; 91 | outline-color: ${props.disabled ? getButtonTheme(props.variant)?.outline.disabled : getButtonTheme(props.variant)?.outline.normal}; 92 | ${Text} { 93 | font-size: ${props.variant === 'primary' ? '1.6rem' : '1.3rem'}; 94 | font-weight: 700; 95 | } 96 | ${WrapperIcon} { 97 | transition: all 0.1s ease-out; 98 | display: flex; 99 | } 100 | 101 | &:hover { 102 | color: ${getButtonTheme(props.variant)?.text.hover}; 103 | background-color: ${getButtonTheme(props.variant)?.background.hover}; 104 | outline-color: ${getButtonTheme(props.variant)?.outline.hover}; 105 | } 106 | `}; 107 | 108 | @media ${devices.laptop} { 109 | ${props => css` 110 | &:hover { 111 | ${WrapperIcon} { 112 | ${props.variant === 'tertiary' && 113 | !props.disableMargin && 114 | css` 115 | left: 2rem; 116 | transition: all 0.1s ease-out; 117 | `}; 118 | } 119 | } 120 | `} 121 | } 122 | ` 123 | export const StyledButton = styled(StyledBaseButton)`` 124 | 125 | export const WrapperIcon = styled.div` 126 | position: relative; 127 | left: 1.2rem; 128 | flex-shrink: 0; 129 | display: flex; 130 | ` 131 | 132 | export const Text = styled.p` 133 | pointer-events: none; 134 | ` -------------------------------------------------------------------------------- /src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import type { AppProps, NextWebVitalsMetric } from 'next/app'; 2 | import { useRouter } from 'next/router'; 3 | import { createContext, useEffect, useRef, useState } from 'react'; 4 | import { triggerEventGTM } from 'services'; 5 | import { globalStyles } from 'style'; 6 | 7 | interface AppContextInterface { 8 | isFirstLoad: boolean 9 | } 10 | 11 | export const AppContext = createContext({} as AppContextInterface) 12 | 13 | 14 | 15 | const TemplateApp = ({ Component, pageProps }: AppProps) => { 16 | const isBrowser = typeof window !== 'undefined' 17 | 18 | // hacky way to ensure page-tracking is called on initial page load: 19 | const [initialRouteTracked, setInitialRouteTracked] = useState(false) 20 | 21 | const router = useRouter() 22 | const [isFirstLoad, setIsFirstLoad] = useState(true) 23 | 24 | /* Check if is the first load */ 25 | 26 | const firstUpdate = useRef(true) 27 | useEffect(() => { 28 | if (firstUpdate.current) { 29 | firstUpdate.current = false 30 | return 31 | } 32 | setIsFirstLoad(false) 33 | }) 34 | 35 | const triggerGoogle = (url: string) => { 36 | console.log('trigger-g-event') 37 | setTimeout(() => { 38 | triggerEventGTM({ 39 | eventId: 'TemplatePageView', 40 | info: { 41 | pagePath: url, 42 | event_label: 'TemplatePageView', 43 | event_category: 'Page Statistics' 44 | } 45 | }) 46 | }, 100) 47 | setTimeout(() => { 48 | triggerEventGTM({ 49 | eventId: 'TemplateBouceTest5s', 50 | info: { 51 | pagePath: url, 52 | event_label: 'TemplateBouceTest5s', 53 | event_category: 'Page Statistics' 54 | } 55 | }) 56 | }, 5000) 57 | 58 | setTimeout(() => { 59 | triggerEventGTM({ 60 | eventId: 'TemplateBouceTest20s', 61 | info: { 62 | pagePath: url, 63 | event_label: 'TemplateBouceTest20s', 64 | event_category: 'Page Statistics' 65 | } 66 | }) 67 | }, 20000) 68 | } 69 | 70 | if (isBrowser && !initialRouteTracked && window.dataLayer && window.location.search === '') { 71 | triggerGoogle(window.location.href) 72 | setInitialRouteTracked(true) 73 | } 74 | 75 | useEffect(() => { 76 | const handleRouteChangeStart = (url: string) => { 77 | // console.log('handleRouteChangeStart url', url) 78 | } 79 | 80 | const handleRouteChangeComplete = (url: string) => { 81 | // console.log('handleRouteChangeComplete url', url) 82 | triggerGoogle(url) 83 | } 84 | 85 | const handleRouteChangeError = (err: any, url: string) => { 86 | console.error('handleRouteChangeError err', err) 87 | // console.log('handleRouteChangeError url', url) 88 | } 89 | //When the component is mounted, subscribe to router changes 90 | //and log those page views 91 | router.events.on('routeChangeStart', handleRouteChangeStart) 92 | router.events.on('routeChangeComplete', handleRouteChangeComplete) 93 | router.events.on('routeChangeError', handleRouteChangeError) 94 | 95 | // If the component is unmounted, unsubscribe 96 | // from the event with the `off` method 97 | return () => { 98 | router.events.off('routeChangeStart', handleRouteChangeStart) 99 | router.events.off('routeChangeComplete', handleRouteChangeComplete) 100 | router.events.off('routeChangeError', handleRouteChangeError) 101 | } 102 | }, [router]) 103 | 104 | return ( 105 | <> 106 | {globalStyles} 107 | 111 | 112 | 113 | 114 | ) 115 | } 116 | 117 | export function reportWebVitals(metric: NextWebVitalsMetric) { 118 | // console.log(metric) 119 | } 120 | 121 | export default TemplateApp; 122 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please first discuss the change you wish to make via issue, 4 | email, or any other method with the owners of this repository before making a change. 5 | 6 | Please note we have a code of conduct, please follow it in all your interactions with the project. 7 | 8 | ## Pull Request Process 9 | 10 | 1. Ensure any install or build dependencies are removed before the end of the layer when doing a 11 | build. 12 | 2. Update the README.md with details of changes to the interface, this includes new environment 13 | variables, exposed ports, useful file locations and container parameters. 14 | 3. Increase the version numbers in any examples files and the README.md to the new version that this 15 | Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/). 16 | 4. You may merge the Pull Request in once you have the sign-off of two other developers, or if you 17 | do not have permission to do that, you may request the second reviewer to merge it for you. 18 | 19 | ## Code of Conduct 20 | 21 | ### Our Pledge 22 | 23 | In the interest of fostering an open and welcoming environment, we as 24 | contributors and maintainers pledge to making participation in our project and 25 | our community a harassment-free experience for everyone, regardless of age, body 26 | size, disability, ethnicity, gender identity and expression, level of experience, 27 | nationality, personal appearance, race, religion, or sexual identity and 28 | orientation. 29 | 30 | ### Our Standards 31 | 32 | Examples of behavior that contributes to creating a positive environment 33 | include: 34 | 35 | * Using welcoming and inclusive language 36 | * Being respectful of differing viewpoints and experiences 37 | * Gracefully accepting constructive criticism 38 | * Focusing on what is best for the community 39 | * Showing empathy towards other community members 40 | 41 | Examples of unacceptable behavior by participants include: 42 | 43 | * The use of sexualized language or imagery and unwelcome sexual attention or 44 | advances 45 | * Trolling, insulting/derogatory comments, and personal or political attacks 46 | * Public or private harassment 47 | * Publishing others' private information, such as a physical or electronic 48 | address, without explicit permission 49 | * Other conduct which could reasonably be considered inappropriate in a 50 | professional setting 51 | 52 | ### Our Responsibilities 53 | 54 | Project maintainers are responsible for clarifying the standards of acceptable 55 | behavior and are expected to take appropriate and fair corrective action in 56 | response to any instances of unacceptable behavior. 57 | 58 | Project maintainers have the right and responsibility to remove, edit, or 59 | reject comments, commits, code, wiki edits, issues, and other contributions 60 | that are not aligned to this Code of Conduct, or to ban temporarily or 61 | permanently any contributor for other behaviors that they deem inappropriate, 62 | threatening, offensive, or harmful. 63 | 64 | ### Scope 65 | 66 | This Code of Conduct applies both within project spaces and in public spaces 67 | when an individual is representing the project or its community. Examples of 68 | representing a project or community include using an official project e-mail 69 | address, posting via an official social media account, or acting as an appointed 70 | representative at an online or offline event. Representation of a project may be 71 | further defined and clarified by project maintainers. 72 | 73 | ### Enforcement 74 | 75 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 76 | reported by contacting the project team at `cinnamondevelopers@gmail.com`. All 77 | complaints will be reviewed and investigated and will result in a response that 78 | is deemed necessary and appropriate to the circumstances. The project team is 79 | obligated to maintain confidentiality with regard to the reporter of an incident. 80 | Further details of specific enforcement policies may be posted separately. 81 | 82 | Project maintainers who do not follow or enforce the Code of Conduct in good 83 | faith may face temporary or permanent repercussions as determined by other 84 | members of the project's leadership. 85 | 86 | ### Attribution 87 | 88 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 89 | available at [http://contributor-covenant.org/version/1/4][version] 90 | 91 | [homepage]: http://contributor-covenant.org 92 | [version]: http://contributor-covenant.org/version/1/4/ 93 | -------------------------------------------------------------------------------- /src/pages/api/email.ts: -------------------------------------------------------------------------------- 1 | import nodemailer from 'nodemailer'; 2 | import formidable from 'formidable'; 3 | import { ContactFormFinalData, NodemailerFiles } from 'components/screens/api'; 4 | 5 | // this must be included, otherwise its not working, it disables nextjs parser 6 | export const config = { 7 | api: { 8 | bodyParser: false 9 | } 10 | }; 11 | 12 | export default async (req: any, res: any) => { 13 | if (req.method === 'POST') { 14 | // console.log('/body', req.body) 15 | 16 | const myFields: any = []; 17 | 18 | const myFiles: any = []; 19 | 20 | // i couldnt get it otherwise to wait till its completecd 21 | await new Promise(function (resolve, reject) { 22 | const form = new formidable.IncomingForm({ keepExtensions: true }); 23 | form.on('field', function (field: any, value: any) { 24 | myFields.push(value); 25 | }); 26 | 27 | form.on('file', function (field: any, file: any) { 28 | myFiles.push(file); 29 | }); 30 | 31 | form.parse(req, function (err: any, fields: any, files: any) { 32 | if (err) return reject(err); 33 | resolve({ fields, files }); 34 | }); 35 | }); 36 | 37 | //console.log('myFields', myFields) 38 | 39 | //console.log('myFiles', myFiles) 40 | 41 | const formData = !!myFields.length ? JSON.parse(myFields[0]) : ({} as ContactFormFinalData); 42 | 43 | const formFiles: NodemailerFiles[] = []; 44 | 45 | if (Array.isArray(myFiles)) { 46 | for (let i = 0; i < myFiles.length; i++) { 47 | const file: any = myFiles[i]; 48 | formFiles.push({ 49 | filename: file.name, 50 | path: file.path 51 | }); 52 | } 53 | } else { 54 | // formFiles.push({ 55 | // filename: files.name, 56 | // path: files.path 57 | // }) 58 | } 59 | 60 | // console.log('formData', formData) 61 | // console.log('formFiles', formFiles) 62 | 63 | const transporter = nodemailer.createTransport({ 64 | host: process.env.EMAIL_HOST, 65 | port: 465, 66 | secure: true, 67 | auth: { 68 | user: process.env.EMAIL_USERNAME, 69 | pass: process.env.EMAIL_PASSWORD 70 | }, 71 | tls: { 72 | rejectUnauthorized: false 73 | } 74 | }); 75 | 76 | const mailOptions = { 77 | from: formData.email, 78 | /* to: [ 79 | 'receiver@hello.com', 80 | ], */ 81 | to: [formData.email], 82 | subject: 'hello.com contact form', 83 | html: `

Email: ${formData.email}

Message: ${formData.message}

Company: ${formData.company}

84 |
`, 85 | attachments: [ 86 | { 87 | filename: 'illustration.png', 88 | path: `${process.env.HOST}/email_image.png`, 89 | cid: 'email_image' 90 | }, 91 | ...formFiles 92 | ] 93 | }; 94 | 95 | console.log('mailOptions 1'); 96 | 97 | await transporter 98 | .sendMail(mailOptions) 99 | .then((response: any) => { 100 | const responseMailOptions = { 101 | from: 'Company ', 102 | to: [formData.email], 103 | subject: 'Company has received your message :)', 104 | html: `

WRITE EMAIL RESPONSE HERE

105 |
`, 106 | attachments: [ 107 | { 108 | filename: 'illustration.png', 109 | path: `${process.env.HOST}/email_image.png`, 110 | cid: 'email_image' 111 | } 112 | ] 113 | }; 114 | 115 | transporter 116 | .sendMail(responseMailOptions) 117 | .then((info: any) => { 118 | res.status(200).send({ message: 'OK' }); 119 | return; 120 | }) 121 | .catch((err: Error) => { 122 | res.status(500).send({ err }); 123 | return; 124 | }); 125 | }) 126 | .catch((err: Error) => { 127 | res.status(500).send({ err }); 128 | return; 129 | }); 130 | } else { 131 | // Handle any other HTTP method 132 | res.status(200).json({ name: 'GET' }); 133 | } 134 | }; 135 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | hello@cinnamon.agency. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | --------------------------------------------------------------------------------