├── .gitignore ├── assets ├── data.js ├── logo.svg ├── product-1.jpg ├── product-2.jpeg └── product-3.jpeg ├── examples ├── 0-starter │ ├── app.js │ ├── index.html │ └── styles.css ├── 1-hello │ ├── app.js │ ├── index.html │ └── styles.css ├── 2-basic-api │ ├── app.js │ ├── global.css │ ├── index.html │ ├── logo.svg │ └── styles.css ├── 3-airtable │ ├── app.js │ ├── index.html │ ├── product.css │ ├── product.html │ ├── product.js │ └── styles.css ├── 4-survey │ ├── app.js │ ├── index.html │ └── styles.css ├── 5-weather │ ├── app.js │ ├── index.html │ └── styles.css ├── 6-newsletter │ ├── app.js │ ├── index.html │ └── styles.css ├── 7-email │ ├── app.js │ ├── index.html │ └── styles.css └── 8-stripe │ ├── app.js │ ├── index.html │ └── styles.css ├── functions ├── 1-hello.js ├── 2-basic-api.js ├── 3-airtable.js ├── 3-product.js ├── 3-z-complete.js ├── 4-survey.js ├── 5-weather.js ├── 6-newsletter.js ├── 7-email.js └── 8-stripe.js ├── global.css ├── index.html ├── netlify.toml ├── package-lock.json └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | .env 3 | 4 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 5 | 6 | # dependencies 7 | /.pnp 8 | .pnp.js 9 | 10 | # testing 11 | /coverage 12 | 13 | # production 14 | /build 15 | 16 | # misc 17 | .DS_Store 18 | .env.local 19 | .env.development.local 20 | .env.test.local 21 | .env.production.local 22 | 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* -------------------------------------------------------------------------------- /assets/data.js: -------------------------------------------------------------------------------- 1 | const items = [ 2 | { 3 | id: 'recmg2a1ctaEJNZhu', 4 | name: 'utopia sofa', 5 | image: { 6 | url: 7 | 'https://dl.airtable.com/.attachments/6ac7f7b55d505057317534722e5a9f03/9183491e/product-3.jpg', 8 | }, 9 | price: 39.95, 10 | }, 11 | { 12 | id: 'recvKMNR3YFw0bEt3', 13 | name: 'entertainment center', 14 | image: { 15 | url: 16 | 'https://dl.airtable.com/.attachments/da5e17fd71f50578d525dd5f596e407e/d5e88ac8/product-2.jpg', 17 | }, 18 | price: 29.98, 19 | }, 20 | { 21 | id: 'recxaXFy5IW539sgM', 22 | name: 'albany sectional', 23 | image: { 24 | url: 25 | 'https://dl.airtable.com/.attachments/05ecddf7ac8d581ecc3f7922415e7907/a4242abc/product-1.jpeg', 26 | }, 27 | price: 10.99, 28 | }, 29 | { 30 | id: 'recyqtRglGNGtO4Q5', 31 | name: 'leather sofa', 32 | image: { 33 | url: 34 | 'https://dl.airtable.com/.attachments/3245c726ee77d73702ba8c3310639727/f000842b/product-5.jpg', 35 | }, 36 | price: 9.99, 37 | }, 38 | ] 39 | 40 | module.exports = items 41 | -------------------------------------------------------------------------------- /assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /assets/product-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-smilga/serverless-functions-complete-project/d9fbbba8b8c32f03023cae34240ebc628d833b89/assets/product-1.jpg -------------------------------------------------------------------------------- /assets/product-2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-smilga/serverless-functions-complete-project/d9fbbba8b8c32f03023cae34240ebc628d833b89/assets/product-2.jpeg -------------------------------------------------------------------------------- /assets/product-3.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-smilga/serverless-functions-complete-project/d9fbbba8b8c32f03023cae34240ebc628d833b89/assets/product-3.jpeg -------------------------------------------------------------------------------- /examples/0-starter/app.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-smilga/serverless-functions-complete-project/d9fbbba8b8c32f03023cae34240ebc628d833b89/examples/0-starter/app.js -------------------------------------------------------------------------------- /examples/0-starter/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Example Starter 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 20 | 21 |
22 |

Example Starter

23 |
24 | 25 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /examples/0-starter/styles.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-smilga/serverless-functions-complete-project/d9fbbba8b8c32f03023cae34240ebc628d833b89/examples/0-starter/styles.css -------------------------------------------------------------------------------- /examples/1-hello/app.js: -------------------------------------------------------------------------------- 1 | const result = document.querySelector('.result') 2 | 3 | const fetchData = async () => { 4 | try { 5 | // const { data } = await axios.get('/.netlify/functions/1-hello') 6 | const { data } = await axios.get('/api/1-hello') 7 | result.textContent = data 8 | } catch (error) { 9 | // console.log(error.response) 10 | result.textContent = error.response.data 11 | } 12 | } 13 | 14 | fetchData() 15 | -------------------------------------------------------------------------------- /examples/1-hello/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | First Example 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 20 | 21 |
22 |

first example

23 |

24 |
25 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /examples/1-hello/styles.css: -------------------------------------------------------------------------------- 1 | .content h1 { 2 | margin-bottom: 2rem; 3 | } 4 | -------------------------------------------------------------------------------- /examples/2-basic-api/app.js: -------------------------------------------------------------------------------- 1 | const result = document.querySelector('.result') 2 | 3 | const fetchData = async () => { 4 | try { 5 | const { data } = await axios.get('/api/2-basic-api') 6 | const products = data 7 | .map((product) => { 8 | const { 9 | image: { url }, 10 | name, 11 | price, 12 | } = product 13 | return `
14 | ${name} 15 |
16 |
${name}
17 |
$${price}
18 |
19 |
` 20 | }) 21 | .join('') 22 | result.innerHTML = products 23 | } catch (error) { 24 | result.innerHTML = `

There was an error. Please try again later

` 25 | } 26 | } 27 | 28 | fetchData() 29 | -------------------------------------------------------------------------------- /examples/2-basic-api/global.css: -------------------------------------------------------------------------------- 1 | /* 2 | =============== 3 | Variables 4 | =============== 5 | */ 6 | 7 | :root { 8 | /* dark shades of primary color*/ 9 | --clr-primary-1: #282466; 10 | --clr-primary-2: #36328c; 11 | --clr-primary-3: #4540b3; 12 | --clr-primary-4: #544dd9; 13 | /* primary/main color */ 14 | --clr-primary-5: hsl(243, 100%, 68%); 15 | /* lighter shades of primary color */ 16 | --clr-primary-6: hsl(243, 100%, 73%); 17 | --clr-primary-7: hsl(243, 100%, 77%); 18 | --clr-primary-8: hsl(243, 100%, 82%); 19 | --clr-primary-9: hsl(244, 100%, 87%); 20 | --clr-primary-10: hsl(243, 100%, 92%); 21 | /* darkest grey - used for headings */ 22 | --clr-grey-1: hsl(209, 61%, 16%); 23 | --clr-grey-2: hsl(211, 39%, 23%); 24 | --clr-grey-3: hsl(209, 34%, 30%); 25 | --clr-grey-4: hsl(209, 28%, 39%); 26 | /* grey used for paragraphs */ 27 | --clr-grey-5: hsl(210, 22%, 49%); 28 | --clr-grey-6: hsl(209, 23%, 60%); 29 | --clr-grey-7: hsl(211, 27%, 70%); 30 | --clr-grey-8: hsl(210, 31%, 80%); 31 | --clr-grey-9: hsl(212, 33%, 89%); 32 | --clr-grey-10: hsl(210, 36%, 96%); 33 | --clr-white: #fff; 34 | --clr-red-dark: hsl(360, 67%, 44%); 35 | --clr-red-light: hsl(360, 71%, 66%); 36 | --clr-green-dark: hsl(125, 67%, 44%); 37 | --clr-green-light: hsl(125, 71%, 66%); 38 | --clr-black: #222; 39 | --transition: all 0.3s linear; 40 | --spacing: 0.1rem; 41 | --radius: 0.25rem; 42 | --light-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); 43 | --dark-shadow: 0 5px 15px rgba(0, 0, 0, 0.2); 44 | --max-width: 1170px; 45 | --fixed-width: 620px; 46 | } 47 | /* 48 | =============== 49 | Global Styles 50 | =============== 51 | */ 52 | 53 | *, 54 | ::after, 55 | ::before { 56 | margin: 0; 57 | padding: 0; 58 | box-sizing: border-box; 59 | } 60 | body { 61 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, 62 | Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 63 | background: var(--clr-grey-10); 64 | color: var(--clr-grey-1); 65 | line-height: 1.5; 66 | font-size: 0.875rem; 67 | } 68 | ul { 69 | list-style-type: none; 70 | } 71 | a { 72 | text-decoration: none; 73 | } 74 | hr { 75 | border: none; 76 | border-top: 1px solid var(--clr-grey-8); 77 | } 78 | h1, 79 | h2, 80 | h3, 81 | h4, 82 | h5 { 83 | letter-spacing: var(--spacing); 84 | text-transform: capitalize; 85 | line-height: 1.25; 86 | margin-bottom: 0.75rem; 87 | } 88 | h1 { 89 | font-size: 2.5rem; 90 | } 91 | h2 { 92 | font-size: 2rem; 93 | } 94 | h3 { 95 | font-size: 1.5rem; 96 | } 97 | h4 { 98 | font-size: 1.25rem; 99 | } 100 | h5 { 101 | font-size: 0.875rem; 102 | } 103 | p { 104 | margin-bottom: 1.25rem; 105 | color: var(--clr-grey-3); 106 | } 107 | @media screen and (min-width: 800px) { 108 | h1 { 109 | font-size: 3rem; 110 | } 111 | h2 { 112 | font-size: 2.5rem; 113 | } 114 | h3 { 115 | font-size: 2rem; 116 | } 117 | h4 { 118 | font-size: 1.5rem; 119 | } 120 | h5 { 121 | font-size: 1rem; 122 | } 123 | body { 124 | font-size: 1rem; 125 | } 126 | h1, 127 | h2, 128 | h3, 129 | h4 { 130 | line-height: 1; 131 | } 132 | } 133 | /* global classes */ 134 | 135 | /* section */ 136 | .section { 137 | padding: 5rem 0; 138 | } 139 | .section-center { 140 | width: 90vw; 141 | margin: 0 auto; 142 | max-width: var(--max-width); 143 | } 144 | 145 | @media screen and (min-width: 992px) { 146 | .section-center { 147 | width: 95vw; 148 | } 149 | } 150 | /* 151 | =============== 152 | Navbar 153 | =============== 154 | */ 155 | .nav { 156 | height: 5rem; 157 | display: flex; 158 | justify-content: center; 159 | align-items: center; 160 | background: var(--clr-primary-5); 161 | } 162 | .nav-header { 163 | width: 90vw; 164 | max-width: var(--max-width); 165 | display: flex; 166 | align-items: center; 167 | } 168 | .nav-header img { 169 | width: 10rem; 170 | margin-right: 5rem; 171 | } 172 | .nav-header a { 173 | display: block; 174 | letter-spacing: var(--spacing); 175 | color: var(--clr-white); 176 | } 177 | 178 | /* 179 | =============== 180 | Examples 181 | =============== 182 | */ 183 | .title { 184 | margin-bottom: 2rem; 185 | } 186 | .examples a { 187 | letter-spacing: var(--spacing); 188 | color: var(--clr-primary-5); 189 | transition: var(--transition); 190 | display: block; 191 | margin-bottom: 0.75rem; 192 | text-transform: capitalize; 193 | } 194 | 195 | .examples a:hover { 196 | color: var(--clr-primary-3); 197 | } 198 | -------------------------------------------------------------------------------- /examples/2-basic-api/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Basic API 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 20 | 21 |
22 |

basic API

23 |

Loading...

24 |
25 | 26 | 36 | 37 | 38 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /examples/2-basic-api/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /examples/2-basic-api/styles.css: -------------------------------------------------------------------------------- 1 | .result { 2 | padding: 5rem 0; 3 | } 4 | 5 | .product { 6 | margin-bottom: 2rem; 7 | } 8 | .product img { 9 | width: 100%; 10 | display: block; 11 | height: 300px; 12 | border-radius: var(--radius); 13 | object-fit: cover; 14 | } 15 | 16 | .info { 17 | padding: 1rem 0; 18 | display: flex; 19 | justify-content: space-between; 20 | } 21 | .price { 22 | color: var(--clr-primary-5); 23 | } 24 | @media screen and (min-width: 776px) { 25 | .result { 26 | display: grid; 27 | grid-template-columns: 1fr 1fr; 28 | column-gap: 2rem; 29 | } 30 | .product img { 31 | height: 200px; 32 | } 33 | } 34 | @media screen and (min-width: 992px) { 35 | .result { 36 | grid-template-columns: repeat(3, 1fr); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /examples/3-airtable/app.js: -------------------------------------------------------------------------------- 1 | const result = document.querySelector('.result') 2 | 3 | const fetchProducts = async () => { 4 | try { 5 | // const { data } = await axios.get('/api/3-airtable') 6 | const { data } = await axios.get('/api/3-z-complete') 7 | const products = data 8 | .map((product) => { 9 | const { id, url, name, price } = product 10 | return ` 11 | ${name} 12 |
13 |
${name}
14 |
$${price}
15 | 16 |
17 |
` 18 | }) 19 | .join('') 20 | result.innerHTML = products 21 | } catch (error) { 22 | result.innerHTML = '

There was an error

' 23 | } 24 | } 25 | 26 | fetchProducts() 27 | -------------------------------------------------------------------------------- /examples/3-airtable/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Airtable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 20 | 21 |
22 |

Airtable

23 |
24 |

Loading...

25 |
26 |
27 | 28 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /examples/3-airtable/product.css: -------------------------------------------------------------------------------- 1 | .result h1 { 2 | margin-top: 3rem; 3 | } 4 | .link { 5 | color: var(--clr-primary-5); 6 | } 7 | .product { 8 | margin-top: 3rem; 9 | display: grid; 10 | gap: 2rem; 11 | } 12 | .product-img { 13 | width: 100%; 14 | display: block; 15 | object-fit: cover; 16 | border-radius: var(--radius); 17 | } 18 | .price { 19 | color: var(--clr-primary-5); 20 | } 21 | 22 | @media screen and (min-width: 992px) { 23 | .product { 24 | grid-template-columns: 1fr 1fr; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/3-airtable/product.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Single Product 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 20 | 21 |
22 | Back To Products 23 |
24 | 25 |

Single Product

26 |
27 | utopia sofa 31 |
32 |
utopia sofa
33 |
$39.95
34 |

Lorem ipsum dolor sit amet consectetur, adipisicing elit. Consequatur, iusto. Corrupti excepturi quod in veniam laborum? Animi, labore excepturi nesciunt magni illum optio, sed qui deserunt ex amet adipisci cumque?

35 |
36 |
37 | 38 |
39 | 40 |
41 | 42 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /examples/3-airtable/product.js: -------------------------------------------------------------------------------- 1 | const result = document.querySelector('.result') 2 | 3 | const fetchProduct = async () => { 4 | result.innerHTML = `

Loading...

` 5 | try { 6 | // const id = '?id=1' 7 | const id = window.location.search 8 | // const { 9 | // data: { fields }, 10 | // } = await axios.get(`/api/3-product${id}`) 11 | const { 12 | data: { fields }, 13 | } = await axios.get(`/api/3-z-complete${id}`) 14 | const { name, desc, price, image } = fields 15 | result.innerHTML = `

${name}

16 |
17 | ${name} 21 |
22 |
${name}
23 |
$${price}
24 |

${desc}

25 |
26 |
` 27 | } catch (error) { 28 | result.innerHTML = `

${error.response.data}

` 29 | } 30 | } 31 | 32 | fetchProduct() 33 | -------------------------------------------------------------------------------- /examples/3-airtable/styles.css: -------------------------------------------------------------------------------- 1 | .result { 2 | padding: 5rem 0; 3 | } 4 | 5 | .product { 6 | display: block; 7 | margin-bottom: 3rem; 8 | color: var(--clr-grey-1); 9 | } 10 | .product img { 11 | width: 100%; 12 | display: block; 13 | height: 300px; 14 | border-radius: var(--radius); 15 | object-fit: cover; 16 | } 17 | 18 | .info { 19 | padding: 1rem 0; 20 | display: flex; 21 | justify-content: space-between; 22 | } 23 | .price { 24 | color: var(--clr-primary-5); 25 | } 26 | @media screen and (min-width: 776px) { 27 | .result { 28 | display: grid; 29 | grid-template-columns: 1fr 1fr; 30 | column-gap: 2rem; 31 | } 32 | .product img { 33 | height: 200px; 34 | } 35 | } 36 | @media screen and (min-width: 992px) { 37 | .result { 38 | grid-template-columns: repeat(3, 1fr); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /examples/4-survey/app.js: -------------------------------------------------------------------------------- 1 | const title = document.querySelector('.title h2') 2 | const result = document.querySelector('.result') 3 | 4 | const fetchData = async () => { 5 | try { 6 | const { data } = await axios.get('/api/4-survey') 7 | const response = data 8 | .map((vote) => { 9 | const { room, votes, id } = vote 10 | return `
  • 11 |
    ${room.toUpperCase().substring(0, 2)}
    12 |
    13 |

    ${room}

    14 |

    ${votes} votes

    15 |
    16 | 19 |
  • ` 20 | }) 21 | .join('') 22 | result.innerHTML = response 23 | } catch (error) { 24 | result.innerHTML = `

    There was an error

    ` 25 | } 26 | } 27 | 28 | window.addEventListener('load', () => { 29 | fetchData() 30 | }) 31 | 32 | result.addEventListener('click', async function (e) { 33 | if (e.target.classList.contains('fa-vote-yea')) { 34 | const btn = e.target.parentElement 35 | const id = btn.dataset.id 36 | const voteNode = result.querySelector(`.vote-${id}`) 37 | const votes = voteNode.dataset.votes 38 | const newVotes = await modifyData(id, votes) 39 | title.textContent = 'Survey' 40 | if (newVotes) { 41 | voteNode.textContent = `${newVotes} votes` 42 | voteNode.dataset.votes = newVotes 43 | } 44 | } 45 | }) 46 | // modify data 47 | async function modifyData(id, votes) { 48 | title.textContent = 'Loading...' 49 | try { 50 | const { data } = await axios.put(`/api/4-survey`, { id, votes }) 51 | const newVotes = data.fields.votes 52 | return newVotes 53 | } catch (error) { 54 | console.log(error.response) 55 | return null 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /examples/4-survey/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Survey 7 | 8 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 27 | 28 |
    29 |
    30 |

    Survey

    31 |
    32 |

    Most important room in the house?

    33 |
    34 | 35 |
    36 | 37 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /examples/4-survey/styles.css: -------------------------------------------------------------------------------- 1 | .title { 2 | text-align: center; 3 | } 4 | .title-underline { 5 | width: 5rem; 6 | height: 0.25rem; 7 | background: var(--clr-primary-5); 8 | margin: 0 auto; 9 | margin-bottom: 1.5rem; 10 | } 11 | .title h4 { 12 | text-transform: none; 13 | font-weight: 400; 14 | color: var(--clr-grey-5); 15 | } 16 | .result { 17 | margin-top: 4rem; 18 | } 19 | 20 | ul { 21 | margin-top: 2rem; 22 | display: grid; 23 | gap: 2rem; 24 | grid-gap: 2rem; 25 | } 26 | @media screen and (min-width: 992px) { 27 | ul { 28 | grid-template-columns: 1fr 1fr; 29 | } 30 | } 31 | @media screen and (min-width: 1200px) { 32 | ul { 33 | grid-template-columns: 1fr 1fr 1fr; 34 | } 35 | } 36 | li { 37 | background: var(--clr-grey-10); 38 | border-radius: var(--radius); 39 | padding: 0.75rem 1rem; 40 | display: grid; 41 | grid-template-columns: auto 1fr auto; 42 | gap: 0 3rem; 43 | grid-gap: 0 3rem; 44 | align-items: center; 45 | } 46 | .key { 47 | color: var(--clr-white); 48 | font-size: 1.5rem; 49 | background: var(--clr-primary-7); 50 | padding: 0.5rem 1rem; 51 | border-radius: var(--radius); 52 | } 53 | p { 54 | margin-bottom: 0; 55 | color: var(--clr-grey-5); 56 | letter-spacing: var(--spacing); 57 | } 58 | h4 { 59 | margin-bottom: 0; 60 | } 61 | button { 62 | background: transparent; 63 | border-color: transparent; 64 | font-size: 2rem; 65 | cursor: pointer; 66 | color: var(--clr-black); 67 | } 68 | -------------------------------------------------------------------------------- /examples/5-weather/app.js: -------------------------------------------------------------------------------- 1 | // it takes few minutes 2 | 3 | const form = document.querySelector('.form') 4 | const input = document.querySelector('.form-input') 5 | const alert = document.querySelector('.alert') 6 | const result = document.querySelector('.result') 7 | alert.style.display = 'none' 8 | 9 | form.addEventListener('submit', (event) => { 10 | event.preventDefault() 11 | const city = input.value 12 | if (city) { 13 | getWeatherData(city) 14 | } 15 | }) 16 | 17 | async function getWeatherData(city) { 18 | alert.style.display = 'none' 19 | try { 20 | const { data } = await axios.post('/api/5-weather', { city }) 21 | const { name } = data 22 | const { country } = data.sys 23 | const { temp_max: max, temp_min: min, feels_like } = data.main 24 | const { description } = data.weather[0] 25 | result.innerHTML = ` 26 |
    27 |

    ${name},${country}

    28 |

    ${description}

    29 |

    min temp : ${min}℉

    30 |

    min temp : ${max}℉

    31 |

    feels like : ${feels_like}℉

    32 |
    33 | 34 | ` 35 | } catch (error) { 36 | // console.log(error.response) 37 | alert.style.display = 'block' 38 | alert.textContent = `Can not find weather data for city : "${city}"` 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /examples/5-weather/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Open Weather API 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 20 | 21 |
    22 |
    23 |

    open weather API

    24 |
    25 | 26 | 27 |
    28 |

    display alert here

    29 | 30 |
    31 |
    32 |
    33 | 34 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /examples/5-weather/styles.css: -------------------------------------------------------------------------------- 1 | .form { 2 | margin-top: 2rem; 3 | background: var(--clr-white); 4 | max-width: 600px; 5 | padding: 2rem 2.5rem; 6 | border-radius: var(--radius); 7 | } 8 | label { 9 | display: block; 10 | text-transform: capitalize; 11 | margin-top: 1.5rem; 12 | margin-bottom: 0.5rem; 13 | color: var(--clr-grey-5); 14 | font-size: 1.15rem; 15 | } 16 | input[type='text'] { 17 | width: 100%; 18 | background: var(--clr-grey-10); 19 | border-color: transparent; 20 | padding: 0.35rem 0.75rem; 21 | border-radius: var(--radius); 22 | font-size: 1.25rem; 23 | } 24 | .submit-btn { 25 | width: 100%; 26 | background: var(--clr-primary-5); 27 | margin-top: 1.5rem; 28 | text-transform: capitalize; 29 | letter-spacing: var(--spacing); 30 | border-radius: var(--radius); 31 | border-color: transparent; 32 | font-size: 1rem; 33 | color: var(--clr-white); 34 | padding: 0.25rem 0.5rem; 35 | cursor: pointer; 36 | transition: var(--transition); 37 | } 38 | .submit-btn:hover { 39 | background: var(--clr-primary-3); 40 | } 41 | 42 | .result { 43 | margin-top: 3rem; 44 | } 45 | .result h3 { 46 | margin-bottom: 1.5rem; 47 | } 48 | .result p { 49 | text-transform: capitalize; 50 | margin-bottom: 0.5rem; 51 | } 52 | .alert { 53 | color: var(--clr-red-dark); 54 | margin-bottom: 0; 55 | margin-top: 1rem; 56 | /* display: none; */ 57 | } 58 | -------------------------------------------------------------------------------- /examples/6-newsletter/app.js: -------------------------------------------------------------------------------- 1 | const form = document.querySelector('.form') 2 | const emailInput = document.querySelector('.email-input') 3 | const alert = document.querySelector('.alert') 4 | alert.style.display = 'none' 5 | 6 | form.addEventListener('submit', async function (e) { 7 | e.preventDefault() 8 | form.classList.add('loading') 9 | alert.style.display = 'none' 10 | const email = emailInput.value 11 | try { 12 | await axios.post('/api/6-newsletter', { email }) 13 | form.innerHTML = 14 | '

    Success! Please check your email

    ' 15 | } catch (error) { 16 | console.log(error.response) 17 | alert.style.display = 'block' 18 | alert.textContent = 'Something went wrong. Please try again' 19 | } 20 | form.classList.remove('loading') 21 | }) 22 | -------------------------------------------------------------------------------- /examples/6-newsletter/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Newsletter 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 20 | 21 |
    22 |
    23 |

    Join the newsletter

    24 |
    25 | 26 | 27 |
    28 |

    display alert here

    29 | 30 |
    31 |
    32 | 33 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /examples/6-newsletter/styles.css: -------------------------------------------------------------------------------- 1 | .form { 2 | margin-top: 2rem; 3 | background: var(--clr-white); 4 | max-width: 600px; 5 | padding: 2.5rem 3rem; 6 | border-radius: var(--radius); 7 | } 8 | .form h2 { 9 | margin-bottom: 2rem; 10 | text-transform: none; 11 | } 12 | .form-control { 13 | margin-bottom: 1rem; 14 | } 15 | label { 16 | display: block; 17 | text-transform: uppercase; 18 | margin-bottom: 0.5rem; 19 | color: var(--clr-grey-5); 20 | font-size: 1.15rem; 21 | letter-spacing: var(--spacing); 22 | } 23 | input { 24 | width: 100%; 25 | background: var(--clr-grey-10); 26 | border-color: transparent; 27 | padding: 0.35rem 0.75rem; 28 | border-radius: var(--radius); 29 | font-size: 1.25rem; 30 | } 31 | .submit-btn { 32 | width: 100%; 33 | background: var(--clr-primary-5); 34 | text-transform: uppercase; 35 | letter-spacing: var(--spacing); 36 | border-radius: var(--radius); 37 | border-color: transparent; 38 | font-size: 1.25rem; 39 | font-weight: 700; 40 | margin-top: 1rem; 41 | color: var(--clr-white); 42 | padding: 0.25rem 0.5rem; 43 | cursor: pointer; 44 | transition: var(--transition); 45 | } 46 | .submit-btn:hover { 47 | background: var(--clr-primary-3); 48 | } 49 | .alert { 50 | color: var(--clr-red-dark); 51 | margin-bottom: 0; 52 | margin-top: 1rem; 53 | } 54 | .success { 55 | color: var(--clr-green-dark); 56 | margin-bottom: 0; 57 | } 58 | 59 | .form.loading { 60 | position: relative; 61 | } 62 | .form.loading::before { 63 | background: var(--clr-white); 64 | position: absolute; 65 | content: ''; 66 | height: 100%; 67 | width: 100%; 68 | top: 0; 69 | left: 0; 70 | border-radius: var(--radius); 71 | opacity: 0.8; 72 | } 73 | @keyframes spinner { 74 | to { 75 | transform: rotate(360deg); 76 | } 77 | } 78 | 79 | .form.loading::after { 80 | content: ''; 81 | position: absolute; 82 | top: calc(50% - 3rem); 83 | left: calc(50% - 3rem); 84 | width: 6rem; 85 | height: 6rem; 86 | border-radius: 50%; 87 | border: 3px solid #ccc; 88 | border-top-color: var(--clr-primary-5); 89 | animation: spinner 0.6s linear infinite; 90 | } 91 | -------------------------------------------------------------------------------- /examples/7-email/app.js: -------------------------------------------------------------------------------- 1 | const nameInput = document.querySelector('.name-input') 2 | const emailInput = document.querySelector('.email-input') 3 | const subjectInput = document.querySelector('.subject-input') 4 | const messageInput = document.querySelector('.message-input') 5 | const form = document.querySelector('.form') 6 | const btn = document.querySelector('.submit-btn') 7 | const alert = document.querySelector('.alert') 8 | const title = document.querySelector('.title') 9 | alert.style.display = 'none' 10 | 11 | form.addEventListener('submit', async function (e) { 12 | e.preventDefault() 13 | alert.style.display = 'none' 14 | btn.disabled = true 15 | btn.innerHTML = '' 16 | 17 | const name = nameInput.value 18 | const email = emailInput.value 19 | const subject = subjectInput.value 20 | const message = messageInput.value 21 | 22 | try { 23 | await axios.post('/api/7-email', { 24 | name, 25 | email, 26 | subject, 27 | message, 28 | }) 29 | nameInput.value = '' 30 | emailInput.value = '' 31 | subjectInput.value = '' 32 | messageInput.value = '' 33 | title.textContent = 'Message Sent' 34 | setTimeout(() => { 35 | title.textContent = 'Send a Message' 36 | }, 3000) 37 | } catch (error) { 38 | alert.style.display = 'block' 39 | alert.textContent = error.response.data 40 | } 41 | btn.disabled = true 42 | btn.innerHTML = 'Send' 43 | }) 44 | -------------------------------------------------------------------------------- /examples/7-email/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Send Email 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 20 | 21 |
    22 |

    Send a Message

    23 |
    24 |
    25 | 26 | 27 |
    28 |
    29 | 30 | 31 |
    32 |
    33 | 34 | 35 |
    36 |
    37 | 38 | 39 |
    40 |

    display alert here

    41 | 42 |
    43 |
    44 | 45 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /examples/7-email/styles.css: -------------------------------------------------------------------------------- 1 | .form { 2 | margin-top: 2rem; 3 | background: var(--clr-white); 4 | max-width: 600px; 5 | padding: 2.5rem 3rem; 6 | border-radius: var(--radius); 7 | } 8 | 9 | .form-control { 10 | margin-bottom: 1rem; 11 | } 12 | label { 13 | display: block; 14 | text-transform: uppercase; 15 | margin-bottom: 0.5rem; 16 | color: var(--clr-grey-5); 17 | font-size: 1.15rem; 18 | letter-spacing: var(--spacing); 19 | } 20 | input { 21 | width: 100%; 22 | background: var(--clr-grey-10); 23 | border-color: transparent; 24 | padding: 0.35rem 0.75rem; 25 | border-radius: var(--radius); 26 | font-size: 1.25rem; 27 | } 28 | textarea { 29 | width: 100%; 30 | background: var(--clr-grey-10); 31 | border-color: transparent; 32 | padding: 0.35rem 0.75rem; 33 | border-radius: var(--radius); 34 | font-size: 1rem; 35 | } 36 | .submit-btn { 37 | width: 100%; 38 | background: var(--clr-primary-5); 39 | text-transform: uppercase; 40 | letter-spacing: var(--spacing); 41 | border-radius: var(--radius); 42 | border-color: transparent; 43 | font-size: 1.25rem; 44 | font-weight: 700; 45 | margin-top: 1rem; 46 | color: var(--clr-white); 47 | padding: 0.25rem 0.5rem; 48 | cursor: pointer; 49 | transition: var(--transition); 50 | } 51 | .submit-btn:hover { 52 | background: var(--clr-primary-3); 53 | } 54 | .alert { 55 | color: var(--clr-red-dark); 56 | margin-bottom: 0; 57 | margin-top: 1rem; 58 | } 59 | .success { 60 | color: var(--clr-green-dark); 61 | margin-bottom: 0; 62 | } 63 | @keyframes spinner { 64 | to { 65 | transform: rotate(360deg); 66 | } 67 | } 68 | .sending { 69 | display: inline-block; 70 | width: 1.25rem; 71 | height: 1.25rem; 72 | border-radius: 50%; 73 | border: 3px solid #ccc; 74 | border-top-color: var(--clr-primary-5); 75 | animation: spinner 0.6s linear infinite; 76 | } 77 | -------------------------------------------------------------------------------- /examples/8-stripe/app.js: -------------------------------------------------------------------------------- 1 | const purchase = [ 2 | { id: '1', name: 't-shirt', price: 1999 }, 3 | { id: '2', name: 'shoes', price: 4999 }, 4 | ] 5 | const total_amount = 10998 6 | const shipping_fee = 1099 7 | var stripe = Stripe( 8 | 'pk_test_51I87djFp5pnuKUXgBVIHiR36vVAWyfuyb7ckrhgyDNA1kM0GWHas9ZGUAgwJSFNUxrbyE6NwlMNmls1iGSfzHDdE00DQB3y6AH' 9 | ) 10 | 11 | // The items the customer wants to buy 12 | 13 | // Disable the button until we have Stripe set up on the page 14 | document.querySelector('button').disabled = true 15 | fetch('/api/8-stripe', { 16 | method: 'POST', 17 | headers: { 18 | 'Content-Type': 'application/json', 19 | }, 20 | body: JSON.stringify({ purchase, total_amount, shipping_fee }), 21 | }) 22 | .then(function (result) { 23 | return result.json() 24 | }) 25 | .then(function (data) { 26 | var elements = stripe.elements() 27 | 28 | var style = { 29 | base: { 30 | color: '#32325d', 31 | fontFamily: 'Arial, sans-serif', 32 | fontSmoothing: 'antialiased', 33 | fontSize: '16px', 34 | '::placeholder': { 35 | color: '#32325d', 36 | }, 37 | }, 38 | invalid: { 39 | fontFamily: 'Arial, sans-serif', 40 | color: '#fa755a', 41 | iconColor: '#fa755a', 42 | }, 43 | } 44 | 45 | var card = elements.create('card', { style: style }) 46 | // Stripe injects an iframe into the DOM 47 | card.mount('#card-element') 48 | 49 | card.on('change', function (event) { 50 | // Disable the Pay button if there are no card details in the Element 51 | document.querySelector('button').disabled = event.empty 52 | document.querySelector('#card-error').textContent = event.error 53 | ? event.error.message 54 | : '' 55 | }) 56 | 57 | var form = document.getElementById('payment-form') 58 | form.addEventListener('submit', function (event) { 59 | event.preventDefault() 60 | // Complete payment when the submit button is clicked 61 | payWithCard(stripe, card, data.clientSecret) 62 | }) 63 | }) 64 | 65 | // Calls stripe.confirmCardPayment 66 | // If the card requires authentication Stripe shows a pop-up modal to 67 | // prompt the user to enter authentication details without leaving your page. 68 | var payWithCard = function (stripe, card, clientSecret) { 69 | loading(true) 70 | stripe 71 | .confirmCardPayment(clientSecret, { 72 | payment_method: { 73 | card: card, 74 | }, 75 | }) 76 | .then(function (result) { 77 | if (result.error) { 78 | // Show error to your customer 79 | showError(result.error.message) 80 | } else { 81 | // The payment succeeded! 82 | orderComplete(result.paymentIntent.id) 83 | } 84 | }) 85 | } 86 | 87 | /* ------- UI helpers ------- */ 88 | 89 | // Shows a success message when the payment is complete 90 | var orderComplete = function (paymentIntentId) { 91 | loading(false) 92 | document 93 | .querySelector('.result-message a') 94 | .setAttribute( 95 | 'href', 96 | 'https://dashboard.stripe.com/test/payments/' + paymentIntentId 97 | ) 98 | document.querySelector('.result-message').classList.remove('hidden') 99 | document.querySelector('button').disabled = true 100 | } 101 | 102 | // Show the customer the error from Stripe if their card fails to charge 103 | var showError = function (errorMsgText) { 104 | loading(false) 105 | var errorMsg = document.querySelector('#card-error') 106 | errorMsg.textContent = errorMsgText 107 | setTimeout(function () { 108 | errorMsg.textContent = '' 109 | }, 4000) 110 | } 111 | 112 | // Show a spinner on payment submission 113 | var loading = function (isLoading) { 114 | if (isLoading) { 115 | // Disable the button and show a spinner 116 | document.querySelector('button').disabled = true 117 | document.querySelector('#spinner').classList.remove('hidden') 118 | document.querySelector('#button-text').classList.add('hidden') 119 | } else { 120 | document.querySelector('button').disabled = false 121 | document.querySelector('#spinner').classList.add('hidden') 122 | document.querySelector('#button-text').classList.remove('hidden') 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /examples/8-stripe/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Stripe Payment 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 23 | 24 |
    25 |

    Stripe Payment

    26 | 27 |
    28 | 29 |

    Test Card Number: 4242 4242 4242 4242

    30 | 31 |
    32 | 36 | 37 | 42 |
    43 |
    44 | 45 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /examples/8-stripe/styles.css: -------------------------------------------------------------------------------- 1 | h1 { 2 | margin-bottom: 2rem; 3 | } 4 | form { 5 | width: 30vw; 6 | min-width: 500px; 7 | align-self: center; 8 | box-shadow: 0px 0px 0px 0.5px rgba(50, 50, 93, 0.1), 9 | 0px 2px 5px 0px rgba(50, 50, 93, 0.1), 0px 1px 1.5px 0px rgba(0, 0, 0, 0.07); 10 | border-radius: 7px; 11 | padding: 40px; 12 | } 13 | 14 | input { 15 | border-radius: 6px; 16 | margin-bottom: 6px; 17 | padding: 12px; 18 | border: 1px solid rgba(50, 50, 93, 0.1); 19 | height: 44px; 20 | font-size: 16px; 21 | width: 100%; 22 | background: white; 23 | } 24 | 25 | .result-message { 26 | line-height: 22px; 27 | font-size: 16px; 28 | } 29 | 30 | .result-message a { 31 | color: rgb(89, 111, 214); 32 | font-weight: 600; 33 | text-decoration: none; 34 | } 35 | 36 | .hidden { 37 | display: none; 38 | } 39 | 40 | #card-error { 41 | color: rgb(105, 115, 134); 42 | text-align: left; 43 | font-size: 13px; 44 | line-height: 17px; 45 | margin-top: 12px; 46 | } 47 | 48 | #card-element { 49 | border-radius: 4px 4px 0 0; 50 | padding: 12px; 51 | border: 1px solid rgba(50, 50, 93, 0.1); 52 | height: 44px; 53 | width: 100%; 54 | background: white; 55 | } 56 | 57 | #payment-request-button { 58 | margin-bottom: 32px; 59 | } 60 | 61 | /* Buttons and links */ 62 | button { 63 | background: #5469d4; 64 | color: #ffffff; 65 | font-family: Arial, sans-serif; 66 | border-radius: 0 0 4px 4px; 67 | border: 0; 68 | padding: 12px 16px; 69 | font-size: 16px; 70 | font-weight: 600; 71 | cursor: pointer; 72 | display: block; 73 | transition: all 0.2s ease; 74 | box-shadow: 0px 4px 5.5px 0px rgba(0, 0, 0, 0.07); 75 | width: 100%; 76 | } 77 | button:hover { 78 | filter: contrast(115%); 79 | } 80 | button:disabled { 81 | opacity: 0.5; 82 | cursor: default; 83 | } 84 | 85 | /* spinner/processing state, errors */ 86 | .spinner, 87 | .spinner:before, 88 | .spinner:after { 89 | border-radius: 50%; 90 | } 91 | .spinner { 92 | color: #ffffff; 93 | font-size: 22px; 94 | text-indent: -99999px; 95 | margin: 0px auto; 96 | position: relative; 97 | width: 20px; 98 | height: 20px; 99 | box-shadow: inset 0 0 0 2px; 100 | -webkit-transform: translateZ(0); 101 | -ms-transform: translateZ(0); 102 | transform: translateZ(0); 103 | } 104 | .spinner:before, 105 | .spinner:after { 106 | position: absolute; 107 | content: ''; 108 | } 109 | .spinner:before { 110 | width: 10.4px; 111 | height: 20.4px; 112 | background: #5469d4; 113 | border-radius: 20.4px 0 0 20.4px; 114 | top: -0.2px; 115 | left: -0.2px; 116 | -webkit-transform-origin: 10.4px 10.2px; 117 | transform-origin: 10.4px 10.2px; 118 | -webkit-animation: loading 2s infinite ease 1.5s; 119 | animation: loading 2s infinite ease 1.5s; 120 | } 121 | .spinner:after { 122 | width: 10.4px; 123 | height: 10.2px; 124 | background: #5469d4; 125 | border-radius: 0 10.2px 10.2px 0; 126 | top: -0.1px; 127 | left: 10.2px; 128 | -webkit-transform-origin: 0px 10.2px; 129 | transform-origin: 0px 10.2px; 130 | -webkit-animation: loading 2s infinite ease; 131 | animation: loading 2s infinite ease; 132 | } 133 | 134 | @-webkit-keyframes loading { 135 | 0% { 136 | -webkit-transform: rotate(0deg); 137 | transform: rotate(0deg); 138 | } 139 | 100% { 140 | -webkit-transform: rotate(360deg); 141 | transform: rotate(360deg); 142 | } 143 | } 144 | @keyframes loading { 145 | 0% { 146 | -webkit-transform: rotate(0deg); 147 | transform: rotate(0deg); 148 | } 149 | 100% { 150 | -webkit-transform: rotate(360deg); 151 | transform: rotate(360deg); 152 | } 153 | } 154 | 155 | @media only screen and (max-width: 600px) { 156 | form { 157 | width: 80vw; 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /functions/1-hello.js: -------------------------------------------------------------------------------- 1 | // domain/.netlify/functions/1-hello 2 | // exports 3 | // const person = { name: 'john' } 4 | 5 | exports.handler = async (event, context, cb) => { 6 | return { 7 | statusCode: 200, 8 | body: 'Our First Netlify Function Example', 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /functions/2-basic-api.js: -------------------------------------------------------------------------------- 1 | const items = require('../assets/data') 2 | 3 | exports.handler = async (event, context, cb) => { 4 | return { 5 | headers: { 6 | 'Access-Control-Allow-Origin': '*', 7 | }, 8 | statusCode: 200, 9 | body: JSON.stringify(items), 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /functions/3-airtable.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | const Airtable = require('airtable-node') 3 | 4 | const airtable = new Airtable({ apiKey: process.env.AIRTABLE_API_KEY }) 5 | .base('appRpwOOjOyiPQHi9') 6 | .table('products') 7 | 8 | exports.handler = async (event, context, cb) => { 9 | try { 10 | const { records } = await airtable.list() 11 | const products = records.map((product) => { 12 | const { id } = product 13 | const { name, image, price } = product.fields 14 | const url = image[0].url 15 | return { id, name, url, price } 16 | }) 17 | return { 18 | statusCode: 200, 19 | body: JSON.stringify(products), 20 | } 21 | } catch (error) { 22 | return { 23 | statusCode: 500, 24 | body: 'Server Error', 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /functions/3-product.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | const Airtable = require('airtable-node') 3 | 4 | const airtable = new Airtable({ apiKey: process.env.AIRTABLE_API_KEY }) 5 | .base('appRpwOOjOyiPQHi9') 6 | .table('products') 7 | exports.handler = async (event, context, cb) => { 8 | const { id } = event.queryStringParameters 9 | if (id) { 10 | try { 11 | const product = await airtable.retrieve(id) 12 | if (product.error) { 13 | return { 14 | statusCode: 404, 15 | body: `No product with id: ${id}`, 16 | } 17 | } 18 | return { 19 | statusCode: 200, 20 | body: JSON.stringify(product), 21 | } 22 | } catch (error) { 23 | return { 24 | statusCode: 500, 25 | body: `Server Error`, 26 | } 27 | } 28 | } 29 | return { 30 | statusCode: 400, 31 | body: 'Please provide product id', 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /functions/3-z-complete.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | const Airtable = require('airtable-node') 3 | 4 | const airtable = new Airtable({ apiKey: process.env.AIRTABLE_API_KEY }) 5 | .base('appRpwOOjOyiPQHi9') 6 | .table('products') 7 | 8 | exports.handler = async (event, context, cb) => { 9 | const { id } = event.queryStringParameters 10 | if (id) { 11 | try { 12 | const product = await airtable.retrieve(id) 13 | if (product.error) { 14 | return { 15 | statusCode: 404, 16 | body: `No product with id: ${id}`, 17 | } 18 | } 19 | return { 20 | statusCode: 200, 21 | body: JSON.stringify(product), 22 | } 23 | } catch (error) { 24 | return { 25 | statusCode: 500, 26 | body: `Server Error`, 27 | } 28 | } 29 | } 30 | try { 31 | const { records } = await airtable.list() 32 | const products = records.map((product) => { 33 | const { id } = product 34 | const { name, image, price } = product.fields 35 | const url = image[0].url 36 | return { id, name, url, price } 37 | }) 38 | return { 39 | statusCode: 200, 40 | body: JSON.stringify(products), 41 | } 42 | } catch (error) { 43 | return { 44 | statusCode: 500, 45 | body: 'Server Error', 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /functions/4-survey.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | const Airtable = require('airtable-node') 3 | 4 | const airtable = new Airtable({ apiKey: process.env.AIRTABLE_API_KEY }) 5 | .base('appRpwOOjOyiPQHi9') 6 | .table('survey') 7 | 8 | exports.handler = async (event, context, cb) => { 9 | const method = event.httpMethod 10 | if (method === 'GET') { 11 | try { 12 | const { records } = await airtable.list() 13 | const survey = records.map((item) => { 14 | const { id } = item 15 | const { room, votes } = item.fields 16 | return { id, room, votes } 17 | }) 18 | return { 19 | statusCode: 200, 20 | body: JSON.stringify(survey), 21 | } 22 | } catch (error) { 23 | return { 24 | statusCode: 500, 25 | body: 'Server Error', 26 | } 27 | } 28 | } 29 | if (method === 'PUT') { 30 | try { 31 | const { id, votes } = JSON.parse(event.body) 32 | if (!id || !votes) { 33 | return { 34 | statusCode: 400, 35 | body: 'Please provide id and votes values', 36 | } 37 | } 38 | const fields = { votes: Number(votes) + 1 } 39 | const item = await airtable.update(id, { fields }) 40 | console.log(item) 41 | if (item.error) { 42 | return { 43 | statusCode: 400, 44 | body: JSON.stringify(item), 45 | } 46 | } 47 | return { 48 | statusCode: 200, 49 | body: JSON.stringify(item), 50 | } 51 | } catch (error) { 52 | return { 53 | statusCode: 400, 54 | body: 'Please provide id and votes values', 55 | } 56 | } 57 | } 58 | // Default Response 59 | return { 60 | statusCode: 405, 61 | body: 'Only GET and PUT Requests Allowed', 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /functions/5-weather.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | const axios = require('axios') 3 | 4 | const url = `http://api.openweathermap.org/data/2.5/weather?appid=${process.env.OPEN_WEATHER_API_KEY}&units=imperial&q=` 5 | 6 | exports.handler = async (event, context, cb) => { 7 | const method = event.httpMethod 8 | 9 | if (method !== 'POST') { 10 | return { 11 | statusCode: 405, 12 | body: 'Only POST Requests Allowed', 13 | } 14 | } 15 | 16 | const { city } = JSON.parse(event.body) 17 | try { 18 | const resp = await axios.get(`${url}${city}`) 19 | return { 20 | statusCode: 200, 21 | body: JSON.stringify(resp.data), 22 | } 23 | } catch (error) { 24 | return { 25 | statusCode: 404, 26 | body: JSON.stringify(error), 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /functions/6-newsletter.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | const axios = require('axios') 3 | const url = 'https://api.buttondown.email/v1/subscribers' 4 | 5 | exports.handler = async (event, context, cb) => { 6 | const method = event.httpMethod 7 | if (method !== 'POST') { 8 | return { 9 | statusCode: 405, 10 | body: 'Only POST Requests Allowed', 11 | } 12 | } 13 | const { email } = JSON.parse(event.body) 14 | if (!email) { 15 | return { 16 | statusCode: 400, 17 | body: 'Please provide email value', 18 | } 19 | } 20 | try { 21 | const data = await axios.post( 22 | url, 23 | { email }, 24 | { 25 | headers: { 26 | Authorization: `Token ${process.env.EMAIL_KEY}`, 27 | }, 28 | } 29 | ) 30 | console.log(data) 31 | return { 32 | statusCode: 201, 33 | body: 'Success', 34 | } 35 | } catch (error) { 36 | return { 37 | statusCode: 400, 38 | body: JSON.stringify(error.response.data), 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /functions/7-email.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | const nodemailer = require('nodemailer') 3 | 4 | const { EMAIL_HOST, EMAIL_PORT, EMAIL_USER, EMAIL_PASSWORD } = process.env 5 | const transporter = nodemailer.createTransport({ 6 | host: EMAIL_HOST, 7 | port: EMAIL_PORT, 8 | secure: false, // true for 465, false for other ports 9 | auth: { 10 | user: EMAIL_USER, // generated ethereal user 11 | pass: EMAIL_PASSWORD, // generated ethereal password 12 | }, 13 | }) 14 | 15 | exports.handler = async (event, context, cb) => { 16 | const method = event.httpMethod 17 | if (method !== 'POST') { 18 | return { 19 | statusCode: 405, 20 | body: 'Only POST Requests Allowed', 21 | } 22 | } 23 | const { name, email, subject, message } = JSON.parse(event.body) 24 | if (!name || !email || !subject || !message) { 25 | return { 26 | statusCode: 400, 27 | body: 'Please Provide All Values', 28 | } 29 | } 30 | const data = { 31 | from: 'John Doe ', 32 | to: `${name} <${email}>`, 33 | subject: subject, 34 | html: `

    ${message}

    `, 35 | } 36 | try { 37 | await transporter.sendMail({ ...data }) 38 | return { 39 | statusCode: 200, 40 | body: 'Success', 41 | } 42 | } catch (error) { 43 | return { 44 | statusCode: 400, 45 | body: JSON.stringify(error.message), 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /functions/8-stripe.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | const stripe = require('stripe')(process.env.STRIPE_KEY) 3 | 4 | exports.handler = async (event, context, cb) => { 5 | const method = event.httpMethod 6 | if (method !== 'POST') { 7 | return { 8 | statusCode: '405', 9 | body: 'Only Accepts POST Requests', 10 | } 11 | } 12 | const { purchase, total_amount, shipping_fee } = JSON.parse(event.body) 13 | const calculateOrderAmount = () => { 14 | // Replace this constant with a calculation of the order's amount 15 | // Calculate the order total on the server to prevent 16 | // people from directly manipulating the amount on the client 17 | return shipping_fee + total_amount 18 | } 19 | try { 20 | const paymentIntent = await stripe.paymentIntents.create({ 21 | amount: calculateOrderAmount(), 22 | currency: 'usd', 23 | }) 24 | return { 25 | statusCode: 200, 26 | body: JSON.stringify({ clientSecret: paymentIntent.client_secret }), 27 | } 28 | } catch (error) { 29 | return { 30 | statusCode: 500, 31 | body: JSON.stringify({ error: error.message }), 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /global.css: -------------------------------------------------------------------------------- 1 | /* 2 | =============== 3 | Variables 4 | =============== 5 | */ 6 | 7 | :root { 8 | /* dark shades of primary color*/ 9 | --clr-primary-1: #282466; 10 | --clr-primary-2: #36328c; 11 | --clr-primary-3: #4540b3; 12 | --clr-primary-4: #544dd9; 13 | /* primary/main color */ 14 | --clr-primary-5: hsl(243, 100%, 68%); 15 | /* lighter shades of primary color */ 16 | --clr-primary-6: hsl(243, 100%, 73%); 17 | --clr-primary-7: hsl(243, 100%, 77%); 18 | --clr-primary-8: hsl(243, 100%, 82%); 19 | --clr-primary-9: hsl(244, 100%, 87%); 20 | --clr-primary-10: hsl(243, 100%, 92%); 21 | /* darkest grey - used for headings */ 22 | --clr-grey-1: hsl(209, 61%, 16%); 23 | --clr-grey-2: hsl(211, 39%, 23%); 24 | --clr-grey-3: hsl(209, 34%, 30%); 25 | --clr-grey-4: hsl(209, 28%, 39%); 26 | /* grey used for paragraphs */ 27 | --clr-grey-5: hsl(210, 22%, 49%); 28 | --clr-grey-6: hsl(209, 23%, 60%); 29 | --clr-grey-7: hsl(211, 27%, 70%); 30 | --clr-grey-8: hsl(210, 31%, 80%); 31 | --clr-grey-9: hsl(212, 33%, 89%); 32 | --clr-grey-10: hsl(210, 36%, 96%); 33 | --clr-white: #fff; 34 | --clr-red-dark: hsl(360, 67%, 44%); 35 | --clr-red-light: hsl(360, 71%, 66%); 36 | --clr-green-dark: hsl(125, 67%, 44%); 37 | --clr-green-light: hsl(125, 71%, 66%); 38 | --clr-black: #222; 39 | --transition: all 0.3s linear; 40 | --spacing: 0.1rem; 41 | --radius: 0.25rem; 42 | --light-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); 43 | --dark-shadow: 0 5px 15px rgba(0, 0, 0, 0.2); 44 | --max-width: 1170px; 45 | --fixed-width: 620px; 46 | } 47 | /* 48 | =============== 49 | Global Styles 50 | =============== 51 | */ 52 | 53 | *, 54 | ::after, 55 | ::before { 56 | margin: 0; 57 | padding: 0; 58 | box-sizing: border-box; 59 | } 60 | body { 61 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, 62 | Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 63 | background: var(--clr-grey-10); 64 | color: var(--clr-grey-1); 65 | line-height: 1.5; 66 | font-size: 0.875rem; 67 | } 68 | ul { 69 | list-style-type: none; 70 | } 71 | a { 72 | text-decoration: none; 73 | } 74 | hr { 75 | border: none; 76 | border-top: 1px solid var(--clr-grey-8); 77 | } 78 | h1, 79 | h2, 80 | h3, 81 | h4, 82 | h5 { 83 | letter-spacing: var(--spacing); 84 | text-transform: capitalize; 85 | line-height: 1.25; 86 | margin-bottom: 0.75rem; 87 | } 88 | h1 { 89 | font-size: 2.5rem; 90 | } 91 | h2 { 92 | font-size: 2rem; 93 | } 94 | h3 { 95 | font-size: 1.5rem; 96 | } 97 | h4 { 98 | font-size: 1.25rem; 99 | } 100 | h5 { 101 | font-size: 0.875rem; 102 | } 103 | p { 104 | margin-bottom: 1.25rem; 105 | color: var(--clr-grey-3); 106 | } 107 | @media screen and (min-width: 800px) { 108 | h1 { 109 | font-size: 3rem; 110 | } 111 | h2 { 112 | font-size: 2.5rem; 113 | } 114 | h3 { 115 | font-size: 2rem; 116 | } 117 | h4 { 118 | font-size: 1.5rem; 119 | } 120 | h5 { 121 | font-size: 1rem; 122 | } 123 | body { 124 | font-size: 1rem; 125 | } 126 | h1, 127 | h2, 128 | h3, 129 | h4 { 130 | line-height: 1; 131 | } 132 | } 133 | /* global classes */ 134 | 135 | /* section */ 136 | .section { 137 | padding: 5rem 0; 138 | } 139 | .section-center { 140 | width: 90vw; 141 | margin: 0 auto; 142 | max-width: var(--max-width); 143 | } 144 | 145 | @media screen and (min-width: 992px) { 146 | .section-center { 147 | width: 95vw; 148 | } 149 | } 150 | /* 151 | =============== 152 | Navbar 153 | =============== 154 | */ 155 | .nav { 156 | height: 5rem; 157 | display: flex; 158 | justify-content: center; 159 | align-items: center; 160 | background: var(--clr-primary-5); 161 | } 162 | .nav-header { 163 | width: 90vw; 164 | max-width: var(--max-width); 165 | display: flex; 166 | align-items: center; 167 | } 168 | .nav-header img { 169 | width: 10rem; 170 | margin-right: 5rem; 171 | } 172 | .nav-header a { 173 | display: block; 174 | letter-spacing: var(--spacing); 175 | color: var(--clr-white); 176 | } 177 | 178 | /* 179 | =============== 180 | Examples 181 | =============== 182 | */ 183 | .title { 184 | margin-bottom: 2rem; 185 | } 186 | .examples h1 { 187 | margin-bottom: 3rem; 188 | } 189 | .examples a { 190 | letter-spacing: var(--spacing); 191 | color: var(--clr-primary-5); 192 | transition: var(--transition); 193 | display: block; 194 | margin-bottom: 0.75rem; 195 | text-transform: capitalize; 196 | } 197 | 198 | .examples a:hover { 199 | color: var(--clr-primary-3); 200 | } 201 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Starter 7 | 8 | 9 | 10 | 11 | 17 | 18 |
    19 |

    netlify serverless functions

    20 |

    Examples

    21 | 47 |
    48 | 49 | 50 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | functions = "./functions" 3 | 4 | [[redirects]] 5 | from = "/api/*" 6 | to = "/.netlify/functions/:splat" 7 | status = 200 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "netlify-functions", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "directories": { 7 | "example": "examples" 8 | }, 9 | "scripts": { 10 | "netlify": "netlify dev" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "ISC", 15 | "dependencies": { 16 | "airtable-node": "^0.1.20", 17 | "axios": "^0.21.1", 18 | "dotenv": "^8.2.0", 19 | "mailgun-js": "^0.22.0", 20 | "nodemailer": "^6.4.17", 21 | "stripe": "^8.131.1" 22 | }, 23 | "devDependencies": { 24 | "netlify-cli": "^3.4.5" 25 | } 26 | } 27 | --------------------------------------------------------------------------------