├── .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 |
15 |
19 |
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 |
15 |
19 |
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 |
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 |
15 |
19 |
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 |
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 |
15 |
19 |
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 |
15 |
19 |
20 |
21 |
22 | Back To Products
23 |
24 |
25 |
Single Product
26 |
27 |
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 |
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 |
17 |
18 |
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 |
22 |
26 |
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 |
15 |
19 |
20 |
21 |
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 |
15 |
19 |
20 |
21 |
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 |
15 |
19 |
20 |
21 |
22 | Send a Message
23 |
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 |
18 |
22 |
23 |
24 |
25 | Stripe Payment
26 |
27 |
28 |
29 | Test Card Number: 4242 4242 4242 4242
30 |
31 |
32 |
33 |
34 | Pay
35 |
36 |
37 |
38 | Payment succeeded, see the result in your
39 | Stripe dashboard. Refresh the page to
40 | pay again.
41 |
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 |
12 |
16 |
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 |
--------------------------------------------------------------------------------