├── examples
├── 0-starter
│ ├── app.js
│ ├── styles.css
│ └── index.html
├── 1-hello
│ ├── styles.css
│ ├── app.js
│ └── index.html
├── 3-airtable
│ ├── product.css
│ ├── app.js
│ ├── styles.css
│ ├── product.js
│ ├── index.html
│ └── product.html
├── 2-basic-api
│ ├── styles.css
│ ├── app.js
│ ├── index.html
│ ├── global.css
│ └── logo.svg
├── 6-newsletter
│ ├── app.js
│ ├── index.html
│ └── styles.css
├── 5-weather
│ ├── app.js
│ ├── styles.css
│ └── index.html
├── 7-email
│ ├── app.js
│ ├── styles.css
│ └── index.html
├── 4-survey
│ ├── styles.css
│ ├── index.html
│ └── app.js
└── 8-stripe
│ ├── index.html
│ ├── styles.css
│ └── app.js
├── assets
├── product-1.jpg
├── product-2.jpeg
├── product-3.jpeg
├── data.js
└── logo.svg
├── netlify.toml
├── functions
├── 1-hello.js
├── 2-basic-api.js
├── 3-airtable.js
├── 5-weather.js
├── 3-product.js
├── 6-newsletter.js
├── 8-stripe.js
├── 3-z-complete.js
├── 7-email.js
└── 4-survey.js
├── .gitignore
├── package.json
├── index.html
└── global.css
/examples/0-starter/app.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/0-starter/styles.css:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/1-hello/styles.css:
--------------------------------------------------------------------------------
1 | .content h1 {
2 | margin-bottom: 2rem;
3 | }
4 |
--------------------------------------------------------------------------------
/assets/product-1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/john-smilga/serverless-functions-complete-project/HEAD/assets/product-1.jpg
--------------------------------------------------------------------------------
/assets/product-2.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/john-smilga/serverless-functions-complete-project/HEAD/assets/product-2.jpeg
--------------------------------------------------------------------------------
/assets/product-3.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/john-smilga/serverless-functions-complete-project/HEAD/assets/product-3.jpeg
--------------------------------------------------------------------------------
/netlify.toml:
--------------------------------------------------------------------------------
1 | [build]
2 | functions = "./functions"
3 |
4 | [[redirects]]
5 | from = "/api/*"
6 | to = "/.netlify/functions/:splat"
7 | status = 200
8 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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*
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/assets/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/examples/2-basic-api/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------