├── .gitignore ├── README.md ├── components ├── Toast.js └── ToastContainer.js ├── context └── ToastContext.js ├── hooks └── useToast.js ├── package-lock.json ├── package.json ├── pages ├── _app.js ├── api │ └── contact.js └── index.js ├── postcss.config.js ├── public ├── favicon.ico └── vercel.svg ├── styles ├── Home.module.css └── globals.css └── tailwind.config.js /.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.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | ``` 12 | 13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 14 | 15 | You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file. 16 | 17 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`. 18 | 19 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 20 | 21 | ## Learn More 22 | 23 | To learn more about Next.js, take a look at the following resources: 24 | 25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 27 | 28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 29 | 30 | ## Deploy on Vercel 31 | 32 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 33 | 34 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 35 | -------------------------------------------------------------------------------- /components/Toast.js: -------------------------------------------------------------------------------- 1 | import { useToastDispatchContext } from '../context/ToastContext'; 2 | 3 | export default function Toast({ type, message, id }) { 4 | const dispatch = useToastDispatchContext(); 5 | return ( 6 | <> 7 | {type == 'success' && ( 8 |
9 |
10 |
11 | 23 |
24 |
25 |

{message}

26 |
27 |
28 |
29 | 49 |
50 |
51 |
52 |
53 | )} 54 | {type == 'error' && ( 55 |
56 |
57 |
58 | 70 |
71 |
72 |

{message}

73 |
74 |
75 |
76 | 96 |
97 |
98 |
99 |
100 | )} 101 | {type == 'update' && ( 102 |
103 |
104 |
105 | 117 |
118 |
119 |

{message}

120 |
121 |
122 |
123 | 143 |
144 |
145 |
146 |
147 | )} 148 | {type == 'warning' && ( 149 |
150 |
151 |
152 | 164 |
165 |
166 |

{message}

167 |
168 |
169 |
170 | 190 |
191 |
192 |
193 |
194 | )} 195 | 196 | ); 197 | } 198 | -------------------------------------------------------------------------------- /components/ToastContainer.js: -------------------------------------------------------------------------------- 1 | import Toast from '../components/Toast'; 2 | import { useToastStateContext } from '../context/ToastContext'; 3 | 4 | export default function ToastContainer() { 5 | const { toasts } = useToastStateContext(); 6 | return ( 7 |
8 |
9 | {toasts && 10 | toasts.map((toast) => ( 11 | 17 | ))} 18 |
19 |
20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /context/ToastContext.js: -------------------------------------------------------------------------------- 1 | import { createContext, useContext, useReducer } from 'react'; 2 | 3 | const ToastStateContext = createContext({ toasts: [] }); 4 | const ToastDispatchContext = createContext(null); 5 | 6 | function ToastReducer(state, action) { 7 | switch (action.type) { 8 | case 'ADD_TOAST': { 9 | return { 10 | ...state, 11 | toasts: [...state.toasts, action.toast], 12 | }; 13 | } 14 | case 'DELETE_TOAST': { 15 | const updatedToasts = state.toasts.filter((e) => e.id != action.id); 16 | return { 17 | ...state, 18 | toasts: updatedToasts, 19 | }; 20 | } 21 | default: { 22 | throw new Error('unhandled action type'); 23 | } 24 | } 25 | } 26 | 27 | export function ToastProvider({ children }) { 28 | const [state, dispatch] = useReducer(ToastReducer, { 29 | toasts: [], 30 | }); 31 | return ( 32 | 33 | 34 | {children} 35 | 36 | 37 | ); 38 | } 39 | 40 | export const useToastDispatchContext = () => useContext(ToastDispatchContext); 41 | export const useToastStateContext = () => useContext(ToastStateContext); 42 | -------------------------------------------------------------------------------- /hooks/useToast.js: -------------------------------------------------------------------------------- 1 | import { useToastDispatchContext } from '../context/ToastContext'; 2 | 3 | export function useToast() { 4 | const dispatch = useToastDispatchContext(); 5 | 6 | function toast(type, message) { 7 | const id = Math.random().toString(36).substr(2, 9); 8 | dispatch({ type: 'ADD_TOAST', toast: { type, message, id } }); 9 | 10 | setTimeout(() => { 11 | dispatch({ type: 'DELETE_TOAST', id: id }); 12 | }, 4000); 13 | } 14 | 15 | return toast; 16 | } 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "forms", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start" 9 | }, 10 | "dependencies": { 11 | "axios": "^0.21.1", 12 | "next": "10.0.7", 13 | "nodemailer": "^6.4.18", 14 | "react": "17.0.1", 15 | "react-dom": "17.0.1", 16 | "react-hook-form": "^6.15.3" 17 | }, 18 | "devDependencies": { 19 | "autoprefixer": "^10.2.4", 20 | "postcss": "^8.2.6", 21 | "tailwindcss": "^2.0.3" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /pages/_app.js: -------------------------------------------------------------------------------- 1 | import 'tailwindcss/tailwind.css'; 2 | import ToastContainer from '../components/ToastContainer'; 3 | import { ToastProvider } from '../context/ToastContext'; 4 | function MyApp({ Component, pageProps }) { 5 | return ( 6 |
7 | 8 | 9 | 10 | 11 |
12 | ); 13 | } 14 | 15 | export default MyApp; 16 | -------------------------------------------------------------------------------- /pages/api/contact.js: -------------------------------------------------------------------------------- 1 | import nodemailer from 'nodemailer'; 2 | export default async (req, res) => { 3 | const { name, email, message, phone } = req.body; 4 | 5 | const transporter = nodemailer.createTransport({ 6 | host: 'smtp.gmail.com', 7 | port: 465, 8 | secure: true, 9 | auth: { 10 | user: process.env.user, 11 | pass: process.env.pass, 12 | }, 13 | }); 14 | 15 | try { 16 | // const emailRes = await transporter.sendMail({ 17 | // from: email, 18 | // to: 'adaamr@gmail.com', 19 | // subject: `Contact form submission from ${name}`, 20 | // html: `

You have a new contact form submission


21 | //

Name: ${name}


22 | //

Phone: ${phone}


23 | //

Message: ${message}


24 | 25 | // `, 26 | // }); 27 | 28 | console.log('Message Sent'); 29 | } catch (err) { 30 | console.log(err); 31 | } 32 | 33 | res.status(200).json(req.body); 34 | }; 35 | -------------------------------------------------------------------------------- /pages/index.js: -------------------------------------------------------------------------------- 1 | import { useForm } from 'react-hook-form'; 2 | import axios from 'axios'; 3 | import { useRouter } from 'next/router'; 4 | import { useToast } from '../hooks/useToast'; 5 | 6 | export default function Home() { 7 | const toast = useToast(); 8 | const { register, handleSubmit, errors, reset } = useForm(); 9 | const router = useRouter(); 10 | async function onSubmitForm(values) { 11 | let config = { 12 | method: 'post', 13 | url: `${process.env.NEXT_PUBLIC_API_URL}/api/contact`, 14 | headers: { 15 | 'Content-Type': 'application/json', 16 | }, 17 | data: values, 18 | }; 19 | 20 | try { 21 | const response = await axios(config); 22 | console.log(response); 23 | if (response.status == 200) { 24 | reset(); 25 | toast( 26 | 'success', 27 | 'Thank you for contacting us, we will be in touch soon.' 28 | ); 29 | } 30 | } catch (err) {} 31 | } 32 | 33 | return ( 34 |
35 |
36 |
39 |
40 | 43 | 57 | 58 | {errors?.name?.message} 59 | 60 |
61 |
62 | 65 | 91 | 92 | {errors?.email?.message} 93 | 94 |
95 |
96 | 99 | 106 |
107 |
108 | 111 | 132 | 133 | {errors?.message?.message} 134 | 135 |
136 |
137 | 142 |
143 |
144 |
145 |
146 | ); 147 | } 148 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamrichardson14/forms/7f259f5cf0d416d11292a25997eaa23194a6ce2b/public/favicon.ico -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | min-height: 100vh; 3 | padding: 0 0.5rem; 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: center; 7 | align-items: center; 8 | } 9 | 10 | .main { 11 | padding: 5rem 0; 12 | flex: 1; 13 | display: flex; 14 | flex-direction: column; 15 | justify-content: center; 16 | align-items: center; 17 | } 18 | 19 | .footer { 20 | width: 100%; 21 | height: 100px; 22 | border-top: 1px solid #eaeaea; 23 | display: flex; 24 | justify-content: center; 25 | align-items: center; 26 | } 27 | 28 | .footer img { 29 | margin-left: 0.5rem; 30 | } 31 | 32 | .footer a { 33 | display: flex; 34 | justify-content: center; 35 | align-items: center; 36 | } 37 | 38 | .title a { 39 | color: #0070f3; 40 | text-decoration: none; 41 | } 42 | 43 | .title a:hover, 44 | .title a:focus, 45 | .title a:active { 46 | text-decoration: underline; 47 | } 48 | 49 | .title { 50 | margin: 0; 51 | line-height: 1.15; 52 | font-size: 4rem; 53 | } 54 | 55 | .title, 56 | .description { 57 | text-align: center; 58 | } 59 | 60 | .description { 61 | line-height: 1.5; 62 | font-size: 1.5rem; 63 | } 64 | 65 | .code { 66 | background: #fafafa; 67 | border-radius: 5px; 68 | padding: 0.75rem; 69 | font-size: 1.1rem; 70 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, 71 | Bitstream Vera Sans Mono, Courier New, monospace; 72 | } 73 | 74 | .grid { 75 | display: flex; 76 | align-items: center; 77 | justify-content: center; 78 | flex-wrap: wrap; 79 | max-width: 800px; 80 | margin-top: 3rem; 81 | } 82 | 83 | .card { 84 | margin: 1rem; 85 | flex-basis: 45%; 86 | padding: 1.5rem; 87 | text-align: left; 88 | color: inherit; 89 | text-decoration: none; 90 | border: 1px solid #eaeaea; 91 | border-radius: 10px; 92 | transition: color 0.15s ease, border-color 0.15s ease; 93 | } 94 | 95 | .card:hover, 96 | .card:focus, 97 | .card:active { 98 | color: #0070f3; 99 | border-color: #0070f3; 100 | } 101 | 102 | .card h3 { 103 | margin: 0 0 1rem 0; 104 | font-size: 1.5rem; 105 | } 106 | 107 | .card p { 108 | margin: 0; 109 | font-size: 1.25rem; 110 | line-height: 1.5; 111 | } 112 | 113 | .logo { 114 | height: 1em; 115 | } 116 | 117 | @media (max-width: 600px) { 118 | .grid { 119 | width: 100%; 120 | flex-direction: column; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | a { 10 | color: inherit; 11 | text-decoration: none; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | purge: [], 3 | darkMode: false, // or 'media' or 'class' 4 | theme: { 5 | extend: {}, 6 | }, 7 | variants: { 8 | extend: {}, 9 | }, 10 | plugins: [], 11 | } 12 | --------------------------------------------------------------------------------